Changelog

All notable changes from the current minor cycle. Format loosely follows Keep a Changelog. For pre-v0.9.0 history, see the tagged CHANGELOG.md at each tag (e.g. git show v0.8.0:CHANGELOG.md); the project trimmed older sections during v0.11.0-prep to focus on the road to 1.0.

[Unreleased]

v0.11.x — Sun’s Stingray (patch line). Coupled cluster (canonical CCSD(T)) remains roadmapped for v0.14.0.

[v0.11.2] — 2026-06-04

Docs-only patch on Sun’s Stingray — removes MyST {contents} directives that rendered as red ERROR boxes on the published vibe-qc.com site. No code changes. Inherits the v0.11.0 codename.

Fixed — docs: drop rejected {contents} TOC directives

13 tutorial and user-guide pages carried a MyST {contents} local table-of-contents block that the vibeqc_cite.py Sphinx extension rejects, rendering a red ERROR box on the Furo site. Furo already provides a per-page TOC in the right sidebar, so the directive was redundant. Removed the opening fence + options + closing fence from each affected page; prose is otherwise untouched. Local sphinx-build confirmed: warning count unchanged (222 → 222), no new errors. (Cherry-picked ab77f423.)

[v0.11.1] — 2026-06-03

Correctness patch on Sun’s Stingray — EWALD_3D Hartree J ω-invariance. One cherry-picked fix from main. Inherits the v0.11.0 codename.

Fixed — EWALD_3D Hartree J now ω-invariant (F2 closure)

build_j_ewald_3d now defaults to an analytic AO-pair-FT Hartree matrix in the periodic G≠0 Coulomb convention, replacing the FFT-Poisson collocation long-range half. The FFT-Poisson path was FFT-grid-error-dominated (error spread 137 Ha at 16³ grid → <1 Ha at 48³; the auto-grid is only 20³), and the 1/ω² leak coefficient was density-shape-dependent (MgO: 1.07·A_pred, LiH: 0.656·A_pred — no single additive N_e formula could cancel both). The analytic path uses the integrals chat’s ao_pair_fourier_transform_bloch_cxx C++ kernel (74f3eb4a s-shell tier + 590d022d McMurchie-Davidson general-L) — the same kernel as the multi-k GDF and V_ne analytic-FT fixes. Inline code comments pin the validation targets per CLAUDE.md §8: PySCF KRHF Γ exxdiv='ewald' MgO/STO-3G −271.04994 Ha and LiH/STO-3G −8.33527 Ha. The legacy FFT-Poisson path remains reachable via VIBEQC_J_EWALD3D_BACKEND=grid for bisection. (Cherry-picked 821ced25.)

[v0.11.0] — 2026-06-03 — Sun’s Stingray

Periodic GDF chain closure — the fourth-cycle defer finally closes (v0.8.0 → v0.9.0 → v0.10.0 → v0.11.0). See the preamble above for the full narrative.

v0.11.0 — Sun’s Stingray (codename locked 2026-05-29). Reserved for the periodic-GDF chain — the fourth-cycle defer (v0.8.0 → v0.9.0 → v0.10.0 → v0.11.0) finally closes. The GDF chain’s RSGDF path reaches µHa parity vs pyscf.pbc.df.GDF / pyscf.pbc.df.RSDF on the canonical ionic-crystal test case (LiH primitive FCC / sto-3g / def2-svp-jk) at a converged lattice cutoff (30 bohr) — Γ-only: -0.5 µHa at ke=800, +27 µHa at ke=200; multi-k (2,2,2): -1.0 µHa at ke=200 — both within PySCF’s own GDF↔RSDF noise floor (~1 µHa). The multi-k −2495 Ha bug (per-q cderi gauge + primitive-cell exxdiv Madelung) is fixed — see below.

Γ-only V_long root cause closed: the v0.10.x compute_nuclear_lattice_ewald integrated V_long on a real-space Becke-Lebedev molecular grid (Treutler-Ahlrichs radial × Lebedev- Laikov angular × Becke fuzzy-cell partition). That grid does not exactly preserve the crystal point-group symmetry of a non-cubic primitive cell (FCC, hexagonal, …), so Hcore elements that must vanish by site symmetry pick up a finite, non-symmetry-equivalent per-element residue. On LiH primitive FCC this produced +1.66e-2 errors on individual Hcore elements (e.g. (Li-1s, Li-pz) should be ~0 by atomic-on-atom symmetry; the grid gave +0.017). Self- consistency compounded that per-element symmetry break into a ~4 mHa SCF total-energy residue vs PySCF. The 2026-05-29 retraction of the earlier cc5bbfb0 ruling (where a V_ne = 1000·I monkey-patch incorrectly cleared V_ne as the culprit — that probe couldn’t detect a symmetry-break bug because diagonal matrices are invariant under rotation) led to the systematic Lippert 1997 (DOI 10.1080/002689797170220), Sun-Berkelbach-McClain-Chan 2017 (DOI 10.1063/1.4998644) and McClain-Sun-Chan-Berkelbach 2017 (DOI 10.1021/acs.jctc.7b00049) re-read that fixed it.

The Γ-only fix (in python/vibeqc/periodic_v_ne.py, compute_v_ne_ewald_3d_ft_gamma): replace the molecular-grid V_long quadrature with the canonical analytic reciprocal-space Ewald-split formula. V_short uses the existing libint erfc-screened analytical lattice sum (compute_nuclear_erfc_lattice, shipped pre-v0.11.0, exact). V_long is computed as (1/Ω)·Σ_{G≠0} v_long(G)·conj(ρ̂_μν(G)) where v_long(G) = -Σ_I Z_I·(4π/G²)·exp(-G²/(4α²))·exp(-iG·R_I) and ρ̂_μν reuses the integrals chat’s ao_pair_fourier_transform_bloch_cxx (commits 74f3eb4a s-only tier + 590d022d McMurchie-Davidson general-L tier). The G=0 mode is dropped per PySCF’s convention (jellium-neutralised implicit background); a finite v_short(G=0) tail is subtracted from V_short to maintain PySCF parity. The reciprocal-space sum preserves crystal symmetry exactly — closes the FCC C3 break to machine precision.

Scope of the V_long fix — Γ-only RSGDF path only. The new compute_v_ne_ewald_3d_ft_gamma is wired into run_pbc_gdf_rhf’s gdf_method='rsgdf' branch. The same Becke-Lebedev molecular-grid V_long that this fix replaces still backs compute_nuclear_lattice_ewald, which compute_nuclear_lattice_dispatch feeds to every EWALD_3D periodic driver (RHF/RKS/UHF/UKS Γ-only + multi-k, BIPOLE, GAPW). Those drivers therefore retain the same ~mHa-scale site-symmetry- break on non-cubic primitive cells (FCC, hexagonal, rhombohedral). It went unnoticed because the Ewald-3D stack was validated on conventional cubic cells (e.g. LiH rocksalt at 859efe0a), whose symmetry the molecular grid does respect.

Resolved dispatch-wide by the PBC chat (v0.11.x follow-up). compute_nuclear_lattice_dispatch’s EWALD_3D branch now defaults to a per-cell analytical-FT V_long (compute_v_ne_ewald_3d_ft_lattice in python/vibeqc/periodic_v_ne.py) — the lattice (LatticeMatrixSet) generalisation of compute_v_ne_ewald_3d_ft_gamma. It is a true drop-in: each per-cell block is the same real-space object the grid quadrature approximated (V_short via libint erfc + V_long via the symmetric reciprocal-space FT + the G=0 tail correction), computed analytically instead of by the symmetry-breaking molecular grid, so it Bloch-sums correctly at every k. One change point therefore fixes the V_ne site-symmetry break for RHF/RKS/UHF/UKS (Γ-only + multi-k), BIPOLE and GAPW. On LiH primitive FCC the (Li-1s|V_ne|Li-px/py/pz) block is restored from a 2.2e-2 spread to ~3e-17 (machine precision), and bloch_sum at Γ reproduces compute_v_ne_ewald_3d_ft_gamma to 1.3e-13. The break is driven by per-atom L≥1 site symmetry, not cell shape: it appears on the FCC primitive cell AND on the conventional cubic LiH rocksalt cell (rocksalt (Li-1s|V_ne|Li-p) restored 1.1e-2 spread → 3e-18) — the earlier “non-cubic only” framing was imprecise. Systems with L≥1 atoms therefore receive a ~mHa V_ne symmetry correction that stays within existing test tolerances (crystal-bulk benchmarks + 230+ periodic consumer tests green; the only periodic reds are pre-existing and unrelated — the F2/F3 EWALD_3D Madelung/exxdiv gap; the F4 e_nuclear gauge bug is now fixed, see the F4 entry below). s-only systems (e.g. H₂ vacuum box) are unchanged to ~1.8e-10 (no L≥1 ⇒ the grid is already symmetric for s·s pairs). The legacy molecular-grid quadrature stays reachable via VIBEQC_VNE_EWALD3D_BACKEND=grid for bisection; a C++ port of the per-cell FT contraction is the remaining perf follow-up. Regression gate: tests/test_periodic_v_ne_ewald_ft.py.

Multi-k GDF — M2 landed. The 2026-05-30 audit found the multi-k GDF driver (run_krhf_periodic_gdf, use_compcell=True) converged to a non-physical −2495 Ha on LiH primitive FCC at kmesh=(2,2,2) (PySCF: −7.92 Ha). Two independent root causes, both now fixed:

  1. Per-q Lpq cache used the wrong cderi gauge. The periodic GDF cderi ρ̂_μν^{k_i,k_j}(G+q) = Σ_R e^{+ik_j·R} ∫χ_μ(r)χ_ν(r−R)e^{−i(G+q)·r}dr depends on the pair (k_i, k_j), not the momentum transfer q = k_j k_i alone — the inter-cell (R≠0) terms carry the ket momentum k_j. A q-only cache (and a single broadcast Hartree J) is exact only in the vacuum-box limit, which is why the H₂ multi-k smoke gates passed while LiH FCC was catastrophic. Fixed by build_lpq_bloch_native_fft (python/vibeqc/aux_basis.py) — the (k_i,k_j)-resolved all-FT generalisation of the Γ-only build_lpq_native_fft — with a per-k Hartree J and per-pair exchange K in run_krhf_periodic_gdf.

  2. exxdiv='ewald' used the primitive-cell Madelung. The finite- k-mesh exchange-divergence correction must use the Born–von-Kármán supercell Madelung (ξ for the (n₁,n₂,n₃) MP mesh, lattice A·diag(n)), not the primitive cell’s — the latter over-counts by Nk^(1/3) and over-bound LiH (2,2,2) by −592 mHa. Fixed via _madelung_for_kmesh (matches PySCF tools.pbc.madelung(cell, kpts)).

LiH primitive FCC (2,2,2) reaches µHa parity: at cutoff 30 bohr / ke=200 Ha it lands at −7.92200 Ha, −1.0 µHa of PySCF KRHF.density_fit() (exxdiv='ewald') — within PySCF’s own GDF↔RSDF noise (~1–14 µHa here). At the cheaper production default (cutoff 15 bohr / ke=200) it is −7.92040 Ha, +1.6 mHa (chemical accuracy). The accuracy knob is the lattice cutoff (the Bloch-pair cell list), not a DF-method floor: the tight-cell Bloch pair density extends over many cells and the default 15-bohr list truncates it (Γ-only shows the same +4.7 mHa → +27 µHa → −0.5 µHa progression with cutoff/ke). No compensated-charge / MDF builder is needed for LiH. The CLAUDE.md §7 energy-sanity guard stays a silent backstop. Regression gate flipped from pinning the guard to asserting PySCF parity (chem-acc at the default + µHa at cutoff 30): tests/test_audit_20260530_periodic.py::test_multik_compcell_gdf_lih_physical.

MgO known limit (Sun-Berkelbach 2017 §III all-electron GDF accuracy floor): the steep Mg-1s core × Mg-1s pair-FT exceeds ke=200’s Gmax bandwidth. LiH (light atoms) reaches µHa at the converged cutoff/ke; MgO lands at −515 mHa at ke=200 and ~−25 mHa at ke=800 (~5× per ke-doubling), and µHa would need ke ≈ 6400 (hours per SCF). Sun-Berkelbach 2017 documents this saturation and motivates the MDF (Mixed Density Fitting) extension. MgO µHa ships v0.12.0 with MDF; v0.11.0 demonstrates µHa on the canonical Sun-2017 light-atom test (LiH FCC) at both Γ-only and multi-k (2,2,2).

The integrals chat has accelerated the C++ AO-pair-FT kernel (74f3eb4a s-shell tier, 590d022d general-L McMurchie-Davidson) that was originally scoped for v0.11.x — closes the MgO 8-atom OOM ceiling.

Open scope still tracked under the v0.11.0 contributor matrix: BIPOLE L_max≥2 + Madelung Ewald rebases (PBC/BIPOLE chat), ωB97X-D close (XC chat, 2f577cf5 D2/CHG citation cited), D4 native-default flip decision, GAPW M3d/M3e + multi-k GPW + lmax≥4 spherical-harmonic compensators (GAPW chat, also already on v0.12-prep — restart format + DOS/band helpers + D3 dispersion). Solvation analytic gradients (CPCM + COSMO), DFT+U Increments 3 + 4d-bipole, and the semiempirical UPM6 open-shell NDDO driver all closed end-to-end on main.

Docs housekeeping: the vq remote-job tutorial was renumbered 43 → 48 (docs/tutorial/48_vq_queue_remote_job.md) to clear a duplicate 43_ prefix it shared with the XSF/BXSF visualization tutorial; inbound links updated across the tutorial index, quickstart, and the ORCA/CRYSTAL/Gaussian migration guides.

Coupled cluster (canonical CCSD(T)) remains roadmapped for v0.14.0; CC2 / CC3 for v0.15.0; FNO + DLPNO-CCSD(T) for v0.17.0 (see docs/roadmap.md § Wavefunction-method ladder).

Fixed — periodic EWALD_3D e_nuclear gauge consistency on open-shell + multi-k drivers (handover F4)

The Γ-only UKS and all multi-k (RKS / UHF / UKS) EWALD_3D SCF drivers computed the nuclear-repulsion term e_nuclear in the molecular gauge (the bare 1/d sum) while their Hartree J was built in the Ewald-3D gauge. With a default options object (DIRECT_TRUNCATED), nuclear_repulsion_per_cell returned the molecular value and madelung_energy_correction_for_lat added the bare-gauge +α_M·Q_e²/2L leak correction; the two only partially cancelled, leaving a ~0.74 mHa residual on H₂/STO-3G/30-bohr (CLAUDE.md §7: an inconsistent periodic energy is a bug, not a convergence artefact). The closed-shell RHF/RKS Γ drivers and the RHF multi-k driver already forced coulomb_method = EWALD_3D at entry; the open-shell Γ and the RKS/UHF/UKS multi-k drivers did not.

  • Fix. Force coulomb_method = EWALD_3D at driver entry — gated on system.dim == 3, since the EWALD_3D V_ne dispatch is implemented only for 3D (1D/2D cells keep DIRECT_TRUNCATED) — in periodic_uks_ewald.py, periodic_uhf_ewald.py, periodic_rks_multi_k_ewald.py, periodic_uhf_multi_k_ewald.py and periodic_uks_multi_k_ewald.py. This aligns V_ne (compute_nuclear_lattice_dispatch), e_nuclear (nuclear_repulsion_per_cell → the periodic Ewald ion-ion sum) and the Madelung term (now 0 for EWALD_3D) with the Ewald-3D Hartree J — one gauge for all three Coulomb pieces, matching run_rks_periodic_gamma_ewald3d.

  • Effect. On H₂/STO-3G/30-bohr (ω = 0.5) the Γ-only UKS and the multi-k RKS totals move from the molecular-gauge −1.150390 Ha to the periodic −1.151133 Ha — now bit-identical to the Γ-only RKS driver (|ΔE| ≤ 1e-13 at PBE and B3LYP). User-facing open-shell / multi-k energies shift by ~mHa only where a default (DIRECT_TRUNCATED) options object was passed directly to one of these drivers; the run_*_periodic_scf dispatcher and the ASE calculator already set EWALD_3D, so those paths are unchanged.

  • run_uhf_periodic_gamma_ewald3d is included even though its closed- shell-equals-RHF test passed: that test only passed because the preceding shared-opts RHF call forced the gauge first; a standalone call with default opts returned a molecular-gauge result.

  • The dim == 3 gate was also added to the RHF multi-k driver’s existing F1 force block: that unconditional force had begun raising on 1D chains once the analytic-FT V_ne landed its dim == 3 guard (commit 00f321e6), reding the dim=1 accelerator-uniformity tests; gating restores their DIRECT_TRUNCATED gauge.

  • Tests. Fixes the two pre-existing reds (test_periodic_uks_ewald.py::test_closed_shell_uks_matches_rks_to_microhartree and test_periodic_rks_multi_k_ewald.py::test_b3lyp_path_uses_hf_exchange). The two affected test_energy_decomposition_consistent tests (UKS Γ + RKS multi-k) were migrated to the EWALD path (E_total = E_elec + E_nuc, no Madelung term) and the now-bit-equivalent test_multi_k_at_gamma_mesh_matches_gamma_driver tolerance tightened 2 mHa → 1e-10 — exactly as the A1 milestone did for the RHF/RKS Γ drivers. The dim=1 accelerator-uniformity multi-k tests are green again.

Fixed — SAD initial guess: per-l occupation pinning for transition metals

The SAD (superposition-of-atomic-densities) initial guess assigned the wrong d-shell occupation for every 3d transition metal. The isolated-atom SCF distributed electrons by a single global eigenvalue sort, and the 3d/4s/4p near-degeneracy makes that bare-atom order disagree with the physical configuration — with nothing to correct it. The whole 3d row came out wrong in both directions: Sc/Ti emptied their 3d entirely and spilled the electrons into an empty 4p, while Ni over-filled to 3d¹⁰ 4s⁰ instead of 3d⁸ 4s². SAD is the AUTO-resolved default guess for every periodic system and every open-shell / transition-metal molecule (GuessEngine::resolve_auto), so every transition-metal job on defaults seeded from a ±1–2 d-electron-wrong density (degrading convergence; found during NiO/STO-3G validation).

  • Fix (cpp/src/guess.cpp): atomic SAD occupations are now pinned per angular-momentum channel from the neutral-atom ground-state configuration instead of by a global eigenvalue sort. The spherically-averaged atomic Fock is block-diagonal in l, so each MO is a pure-l function; within each l-block the lowest-energy orbitals are Aufbau-filled up to the tabulated per-l electron count, with degenerate orbitals sharing fractional occupation (the atomic density stays spherical). The configuration table is the Madelung order plus the standard neutral-atom anomalies (Cr, Cu, Nb, Mo, Ru, Rh, Pd, Ag, La, Ce, Gd, Pt, Au, Ac, Th, Pa, U, Np, Cm, Lr) — what PySCF (scf.atom_hf), Psi4 and ORCA do for their atomic guesses. Sc–Zn now reproduce the experimental d-population (1, 2, 3, 5, 5, 6, 7, 8, 10, 10); main-group SAD is byte-identical; the electron count trace(D·S) = Z is preserved. If a basis is too small to hold the tabulated configuration (valence-only ECP bases), the build falls back to the previous global-Aufbau fill, so ECP guesses are unchanged.

  • Regression tests (tests/test_guess.py) pin the d-population for Sc/Ti (under-fill), Ni (over-fill) and the Cr/Cu anomalies, the absence of spurious 4p occupation on Sc/Ti, and main-group invariance.

  • Neutral-atom ground-state configurations are CODATA/NIST shared-background data with no single citable publication, so no citation-database entry is required (CLAUDE.md §8).

Fixed — pob-TZVP sulfur d-shell SCAL typo + hardened CRYSTAL parser

  • The Bredow pob-TZVP / pob-TZVP-rev2 sulfur sources (16_S) mis-keyed the d-polarization SCAL value 1.0 as two tokens 1 0 (0 3 1 0.0 1 0, the decimal point typed as a space). The in-package parser vibeqc.basis_crystal.parse_crystal_atom_basis read the header as exactly five tokens, so the stray 0 leaked into the primitive stream: it became the d-shell exponent and the real exponent (0.5207… for pob-TZVP, 0.4107… for pob-TZVP-rev2) the coefficient — a physically invalid exp=0 Gaussian, produced silently with no error. Fixed the typo in both source files and hardened the parser: the shell-header read is now line-aware and tolerates the 1 01.0 typo exactly the way the canonical scripts/basisset_dev/pob_basis_verify.py does (the two parsers had diverged), rejects any other stray header token, and rejects any primitive with a non-positive Gaussian exponent. Latent: the shipped pob-tzvp.g94 was already correct (regenerated by the tolerant verify tool), so .g94 source-vs-shipped parity is unchanged and no regeneration was needed — the bug bit only direct vibeqc.basis_crystal use or a re-fetch from the still-typo’d upstream archive. Regression tests added in tests/test_basis_crystal.py. (1a68a7da)

Fixed — optimizer / NEB robustness (silent-flag / convergence-gate audit)

The 2026-05-31 end-to-end audit confirmed the geometry-optimizer and NEB physics is correct (geometries ≤2e-6 bohr vs geomeTRIC; thermo = PySCF; NEB barrier 8e-13 vs PySCF; DFT+U gradient 6.9e-9 vs FD). These four were silent-flag / robustness / edge bugs around it:

  • optimize_molecule reported converged=True at a non-stationary geometry. scipy’s L-BFGS-B sets res.success on either its gradient (gtol) or its energy-reduction (ftol) criterion, and the driver mapped converged straight from res.success. With a loose conv_tol_energy the ftol stop fired while the forces were still large (e.g. H₂O/STO-3G, conv_tol_grad=1e-6, conv_tol_energy=1e-3converged=True at max-force 0.023 Ha/bohr, >20000× the request). Convergence is now gated on the actual max-component force via a shared _gradient_converged helper, applied identically to optimize_molecule and the BIPOLE relaxers (relax_atoms, relax_cell_gradient).

  • A non-converged NEB image aborted the band with a cryptic C++ error. _evaluate_image ran the per-image SCF then unconditionally built the gradient, which raised RuntimeError: ... not converged from the C++ gradient code. It now raises a clear, image-named vibeqc.neb.NEBImageSCFError (image index + geometry + remediation) and the same guard protects _evaluate_image_periodic.

  • NEB cold-start used a weaker guess than the single-point drivers. Cold images seeded the low-level *_scf_with_jk entry with an Hcore fallback, so they needed many more SCF iterations than run_uhf / run_rks (AUTO→SAD) and could hit max_iter where an equivalent single point converges. Cold starts now seed SAD (closed- and open-shell), matching the public driver; warm-start (cached density) is unchanged.

  • optimize_molecule(method="rks", functional=None) raised a TypeError. An operator-precedence bug in the SCF dispatch ran opts.functional = None against the default-"LDA" options, which the pybind str setter rejects. The guard is now parenthesised correctly.

Reported |grad| in the optimizer progress lines is now the max-component (inf-norm) force, matching the gtol convergence metric (was the 2-norm). Regression coverage: tests/test_optimize_neb_robustness.py.

Perf — vibe-view: mesh cache, glyph instancing, pre-rendered energy frames (A7)

  • Colormap and opacity sliders no longer re-run marching cubes (A7-02). The last-marched isosurface mesh is cached in ViewerState keyed by (isovalue, replication); slider changes that don’t affect the mesh topology hit the cache and skip straight to plotter.add_mesh with updated appearance. Only an isovalue or replication change invalidates the cache. Cache is cleared automatically when a new file is loaded.

  • Replicated periodic cells render with glyph instancing (A7-08). Previously a 3×3×3 NaCl supercell (54 atoms) issued 54 individual plotter.add_mesh calls. The structure renderer now groups atoms by (element, radius) and issues one glyph call per element type — typically 2–10 draw calls regardless of supercell size. The molecular (no-replication) path keeps per-atom named actors so the charge-overlay and CPK-restore paths can address individual atoms by name.

  • Trajectory / reaction-path energy plots are pre-rendered (A7-04). Previously every frame step called matplotlib to re-render the full energy curve. All N frame variants (differing only in where the frame-indicator dot sits) are now rendered once at section load and cached; per-frame updates are O(1) base64 lookups.

Fixed — vibe-view audit: MO normalization, vibrations, plot units, state lifecycle

A full audit of the viewer (vibe-view/AUDIT_2026-05-31.md) fixed a cluster of scientific-correctness and state bugs:

  • Molecular-orbital isosurfaces were shape-distorted. The on-the-fly GTO evaluator double-applied the spherical-harmonic normalization (AO self-overlap was (2l+1)/4π instead of 1), an l-dependent error that mis-scaled s/p/d/f contributions relative to each other. The Cartesian path was wrong in the opposite direction ( self-overlap 2, 6). Both now yield unit-normalized AOs; the wavefunction.gto re-evaluation now matches the stored volume.orbital cube. (renderers/wavefunction.py)

  • Vibrations animated at the world origin. The writer hard-coded position: [0,0,0] / atomic_number: 0 for every atom in the vibrations section, so the viewer collapsed the molecule onto the origin. The writer now emits the real equilibrium geometry + atomic numbers, and stores un-mass-weighted Cartesian displacements (was shipping the mass-weighted eigenvector, giving wrong relative atomic amplitudes). The viewer also falls back to the structure section’s geometry for older archives. (output/formats/qvf.py, renderers/vibrations.py)

  • Unrestricted MO picker could render the wrong-spin orbital (alpha/beta both keyed on bare integer index); now keyed on a composite spin:index value with energy / occupation / HOMO-LUMO labels.

  • DOS ignored the Fermi level (unreadable on absolute-energy output); now Fermi-references and draws an E_F marker when E_F is in range.

  • Band-structure axis was mislabeled “Energy (a.u.)” when fermi == 0.0 although eigenvalues are always eV; ECD/VCD spectra dropped every negative Cotton band (intensity > 1e-10abs(...)).

  • Heavy elements rendered as hot-pink with a flat radius (atomic number was re-derived from a symbol table that stopped at Ba, Z=56); now uses the parsed atomic number across all CPK render paths (colours reach Z=96).

  • Clipping a signed volume (volume.difference / volume.spin) dropped the negative lobe — the clip path only contoured the +isovalue. Both lobes (blue +, red −) are now clipped and shown.

  • Clip planes appeared to do nothing (live-review finding): enabling a clip built a clipped overlay but never hid the full isosurface, so the un-clipped volume stayed on top. Enabling clip now hides the full volume (showing only the clipped slice); disabling restores it; and an isovalue/colormap change while clipping re-applies the clip.

  • Clip Enable switch raised a server error on every toggle (found with a trusted-event Playwright pass): the switch passed its new value to a zero-argument toggle_clip handler, which raised TypeError server-side — the flag flipped via the two-way binding so the switch looked toggled, but the clip never actually applied. The handler now accepts the value and sets (rather than re-flips) it, matching the other display switches, and reports “Clip plane enabled/disabled” in the status bar.

  • Atom-charge labels lingered after leaving Atomic Properties (live-review finding): switching to a 2D side panel (citations / SCF history / symmetry / bands / DOS / spectra / NMR) removed the charge billboards server-side but never pushed the cleaned 3D scene, so with client-side VtkLocalView the stale labels stayed visible until a 3D section forced a re-sync. The section-switch dispatch now pushes the viewport for those 2D-panel kinds.

  • Periodic structures drew no bonds. Explicit bonds-section connectivity is now rendered for periodic cells too (minimum-image endpoint, so a bond across a cell face renders short); inferred covalent-radius bonds stay molecular-only.

  • Bands + DOS now render as a single figure with a shared energy axis (band structure left, DOS rotated right, one Fermi line) — the standard solid-state plot — instead of two independent side-by-side charts.

  • Bond order (the writer’s per-pair order) was discarded, so double / aromatic bonds rendered as single. The reader now threads (i, j, order) and the structure renderer draws double/triple bonds as parallel cylinders.

  • QVF spec: wavefunction.gto now officially allows periodic systems at the Γ-point (k=0) — the writer already emitted Γ-point crystalline orbitals for periodic runs while the spec forbade it. The spec is corrected (§ 4.6), and the writer records an optional mo_metadata["k_point"] so the archive is self-describing. Full k-resolved Bloch wavefunctions stay out of scope.

  • CI: a new vibe-view-test job runs the viewer suite (pip-only, under xvfb, allow_failure, scoped to vibe-view/** changes) so the 47 viewer test files are exercised in CI without bloating the native build-test job.

  • Sidebar / panel labels (live-review polish): several kinds showed their raw kind string in the sidebar — dos.projected, the ECD/VCD/generic spectra, and the spin/ELF/difference volumes had no friendly title, and spectra.uvvis was mis-keyed as spectra.uv so UV-Vis never matched. All now have titles (guarded by a completeness test). The bands/DOS bottom panel was hardcoded “Band Structure” even when showing DOS only; it’s now titled for what it shows (“Density of States” / “Projected DOS” / “Band Structure & DOS”).

  • Multi-file mode broke the entire app bar. The Files dropdown bound the _active_file_idx state key, but Vue 3 refuses to expose _-prefixed identifiers to template expressions — so opening more than one file threw ReferenceError on every render and wiped out the app bar (Files dropdown + screenshot / export / camera / measure buttons). Renamed the key to active_file_idx; a guard test now rejects any _-prefixed template-bound key. (Found by live-browser review — headless tests never render the Vue bindings.)

  • Stale state across file/section switches: a running vibration animation, the MO picker, atom selections, clip planes, and the static-structure ghost during trajectory/vibration playback are now reset/managed correctly.

  • Reader resilience: a forward-compat/reserved section kind no longer rejects the whole archive (it opens, that section is skipped); the per-section / root critical: true flag is now enforced (refuse to open rather than silently render a partial view). Decoded binary members are cached so isovalue/clip sliders and animation playback stop re-reading + re-hashing the blob every tick.

  • Banner no longer mislabels a consumed bonds section as “skipped, unsupported”; a writer↔viewer kind-drift test now guards the registry.

Fixed — BIPOLE periodic gradient: production forces use FD; multi-k Pulay fold

The 2026-05-31 end-to-end audit found the analytic BIPOLE atomic gradient (vibeqc.bipole_gradient.compute_bipole_gradient_{rhf,uhf,rks,uks}) is gauge-inconsistent with the BIPOLE energy and wrong even at Γ: the energy is built in CRYSTAL’s Ewald gauge (E_nn via ewald_nuclear_repulsion, screened-erfc + reciprocal-FT V_ne, J = J_SR(ω) + J_LR(ω), post-SCF spheropole), but the analytic gradient assembled truncated direct-space full-Coulomb E_nn/V_ne/J derivatives (no J^LR-reciprocal or spheropole derivative). The residual of those ~1.5 Ha/bohr terms had the wrong sign. Only the kinetic and overlap (Pulay) terms were gauge-correct. (CLAUDE.md §7: a gradient that disagrees with the exact reference is a bug, not a tolerance knob.)

  • Production forces / stress / relaxation now route through the exact finite-difference gradient. vibeqc.bipole_optimize (relax_atoms, relax_cell_gradient, relax_full) and therefore run_periodic_job(optimize=True) default to force_mode="fd", which central-differences the real total energy (correct by construction; ~6N SCFs per gradient). This matches what the periodic NEB driver already does. Pass force_mode="analytic" only for research on the analytic gradient itself.

  • compute_bipole_gradient_fd generalised to all four methods (method= + functional=), so the exact path is available for UHF/RKS/UKS, not just RHF.

  • The analytic drivers are now gated as a research preview — they emit a UserWarning and document the gauge incompleteness.

  • Multi-k Pulay bug fixed. The energy-weighted-density (Pulay) overlap-derivative term broadcast W(Γ) into every lattice cell (correct only at Γ; wrong-sign multi-k forces). It now uses the proper inverse-Bloch fold W(g) = Σ_k w_k Re[e^{-ik·g} W(k)] (the same convention as real_space_density_from_kpoints and the non-BIPOLE multi-k gradient), enabled by passing kmesh=.

  • Tests tightened + expanded (tests/test_bipole_gradient.py): the old <1.0 Ha/bohr analytic-vs-FD assertion is replaced by a fold equivalence unit test, per-method FD coverage (RHF/UHF/RKS/UKS), translational-invariance and multi-k step-stability checks (<1e-3), and an xfail tripwire pinning the analytic↔FD gauge gap.

A correct analytic BIPOLE gradient needs five new derivative kernels (Ewald nuclear-repulsion, Ewald-V_ne reciprocal, screened J_SR, J_LR reciprocal, spheropole) — a new-physics milestone proposed in HANDOVER_GRADIENT_BUGS.md for the PBC/BIPOLE chat. Kernels 1–2 of 5 (+ the shared FT-gradient linchpin) have landed (2026-05-31):

  • ewald_nuclear_repulsion_gradient (new C++ in cpp/src/ewald.cpp) differentiates the Ewald E_nn, FD-validated <1e-6 Ha/bohr.

  • The AO-pair Fourier-transform atomic-position derivative (vibeqc._aopair_ft.*_grad*) — the linchpin reused by the reciprocal kernels — FD-validated to ~1e-10.

  • _v_ne_ewald_gradient differentiates the Ewald V_ne (erfc V_short via new C++ nuclear_erfc_lattice_gradient_contribution, reciprocal V_long via the linchpin + nuclear structure factor, and background·S), FD-validated to 4e-10 Ha/bohr at fixed density.

  • _j_long_range_ewald_gradient differentiates the long-range Hartree energy ½ Σ_K kernel(K)|ρ̂(K)|² (kernel 4), reusing the FT-gradient linchpin with the electronic structure factor; FD-validated to 3e-13 Ha/bohr.

  • eri_lattice_gradient_contribution gains j_scale (J-suppression → exchange-only) and omega (erfc-screened operator) params (kernel 3), so the Ewald-gauge 2e gradient is assembled as ∂J_SR + ∂J^LR − ½∂K_full. Bit-identical to the old call at j_scale=1, ω=0; the screened J_SR branch is FD-validated to 3e-11.

  • The spheropole (EXT EL-SPHEROPOLE) gradient (kernel 5): the C++ compute_ext_el_spheropole_gradient_lattice differentiates the exact emultipole2 bond-symmetrised second moment K_μν = 2Tr⟨r²⟩ 2(A+B)·⟨r⟩ + (|A|²+|B|²)⟨1⟩ fully analytically — the explicit A/B coefficients plus the moment-integral derivatives ∂⟨r^n⟩/∂R from libint emultipole2 deriv_order=1 (requires libint built with LIBINT2_DISABLE_ONEBODY_PROPERTY_DERIVS=OFF) — and _spheropole_ewald_gradient scatters them onto each AO’s home atom; validated to ~1e-11 vs a central FD of the energy on MgO (s,p) and NiO (d). (This supersedes the earlier CJAT33 complex-step kernel, removed with the rest of the CJAT33 spheropole code.) The J^LR electron–electron jellium background derivative was corrected (Coulomb ½ factor; the old V_bg·S block was −2× off), FD-validated (1e-12).

The full analytic BIPOLE RHF gradient at Γ now matches the exact FD gradient to ~1e-7 Ha/bohr (test_analytic_gradient_matches_fd_gamma, formerly xfail, now passing), with machine-precision translational invariance. Beyond the five electrostatic kernels this required two further fixes:

  • Local-energy Pulay. BIPOLE diagonalises the Bloch-summed F(Γ) but evaluates the energy as a LOCAL home-cell contraction Tr[D(0)·H(0)] (the Γ-only _zero_cross_cell_density projection), so the energy-weighted density must use ∂E/∂P(0) (the home-cell Fock the local energy differentiates), not the mo_energies (_corrected_w_gamma_closed / _bipole_de_dp_home_block; the occ-virt block of ∂E/∂P vanishes, so no CPHF is needed).

  • J^LR mixed convention. The SCF builds J^LR with rho_hat from the Bloch k-density but contracts against the local density, so E_jlr = ½ Σ_K kernel·Re[ρ̂_bloch·ρ̂_local*] (verified to 1e-16), not ½|ρ̂_local|²; kernel 4 + the ∂E/∂P J^LR term adopt this (Γ-only).

  • The spheropole energy was reimplemented exactly via libint emultipole2 (commit fcd16eb5); the spheropole gradient now tracks that energy (∂E/∂P spheropole analytic from the moments; the HF gradient by FD of the cheap post-SCF spheropole energy).

The analytic drivers stay gated as a research preview: UHF/RKS/UKS Pulay and multi-k are not yet converted to the local-energy ∂E/∂P scheme, and the path is uncertified. Production forces remain the exact FD path throughout.

Fixed — BIPOLE multipole interaction tensor: exact Cartesian derivatives (L≥2 convention)

The 2026-05-31 end-to-end audit flagged the BIPOLE multipole-far-field interaction tensor (vibeqc.bipole_multipole.multipole_interaction_tensor) as wrong for L 2, with three pure-Python tests red on main.

  • Root cause. The tensor scaled each Cartesian derivative ∂^γ(1/|R|) by a single per-L factor K_L (a {0:1, 1:−½, 2:4⁄3, 3:−4} table). One scalar per L cannot simultaneously normalise the diagonal (∂_xx) and off-diagonal (∂_xz) derivatives, so the recovered derivatives were individually wrong (even ∂_z was off by 2×). The monopole–monopole and an x-dipole·z-dipole case came out right only because the spherical pseudoinverse happened to cancel the errors there; the quadrupole–monopole channel was off by a clean −1/3.

  • Fix. K_L is replaced by the exact closed-form Cartesian derivatives of 1/|R| for orders 0–3 (_cartesian_derivative_inv_r, the standard Coulomb-kernel gradients, Jackson §4.1). The proven Cartesian-Taylor → spherical pseudoinverse conversion is unchanged, so the result is now exact to the documented L = l₁+l₂ 3 truncation: dipole–dipole reproduces −2μ²/R³ and quadrupole–monopole converges as (size/R)² with no systematic error.

  • Scope. The multipole far-field is opt-in and off by default (use_multipole_far_field=False in all four run_pbc_bipole_* drivers), so default-path SCF energies are unaffected; this corrects the experimental far-field J-build (build_j_far_field_multipole) and restores a green main.

  • Cleanup. Removed the dead, stale-convention _wigner_3j_real helper (no longer referenced since the Cartesian-Taylor rework) and its scipy.special.factorial import.

  • Tests (tests/test_bipole_multipole.py, tests/test_bipole_cell_moments.py). The dipole–dipole and quadrupole–monopole “calibrations” were both degenerate (zero energy by geometry, so a 0 == 0 check masked the bug); they are rebuilt as non-degenerate direct-Coulomb calibrations with a convergence check that distinguishes truncation from a systematic error. test_Z_1m_dipole_components updated to the live Stone (√2) convention; the stale cartesian_component_indices(0) expectation updated (L=0 is valid).

Fixed — BIPOLE EXT EL-SPHEROPOLE: exact bond-symmetrised second moment for all l

compute_ext_el_spheropole (vibeqc.bipole_ext_el_pole) is added to the BIPOLE production E_total, so its accuracy matters for total energies and CRYSTAL parity. The prior per-(l_a, l_b) CJAT33 C++ kernel (cpp/src/spheropole.cpp) had two correctness gaps: its p-p term used a hard-coded empirical fudge (7.2 · γ² · , “calibrated on MgO/STO-3G”) and its d/f shells fell back to the s-s formula.

  • Fix. The per-pair spheropole kernel is exactly the bond-symmetrised second moment ⟨φ_μ|(r−A)²+(r−B−g)²|φ_ν⟩, now computed directly — for arbitrary angular momentum — from libint emultipole2 via the existing compute_multipole_moments_lattice. The assembly is l-agnostic (no per-(l_a, l_b) cases), so s/p/d/f are all exact; the empirical fudge and the d/f fallback are gone. Prefactor π·N_e/(6·V) (the Ewald reciprocal K→0 coefficient × CRYSTAL’s TOTCHR), pinned against the sealed CRYSTAL reference.

  • Accuracy. Reproduces CRYSTAL’s sealed MgO/STO-3G CYC 0 spheropole (+4.1191890 Ha) to < 0.02 mHa, vs ~7 mHa for the old fudged p-p kernel — so MgO E_total moves ~+7 mHa toward CRYSTAL. d-containing systems (previously s-s-fallback) are now correct.

  • Implementation. Python-only — no C++ rebuild. The now-unused C++ kernel (spheropole.cpp, compute_spheropole_kernel_lattice, compute_spheropole_bare_integrals) is left in place pending a clean-tree removal (its binding sits in a file under concurrent edit).

  • Tests (tests/test_bipole_ext_el_pole.py). MgO parity tightened from ±20 % to a tight bound the old fudge fails; added an NiO/STO-3G d-shell regression guard. (Note: NiO is not a CRYSTAL parity check — vibe-qc’s SAD fills Ni 3d¹⁰ vs CRYSTAL’s d⁸ initial occupation, so the input densities differ; the kernel is validated exact on MgO.)

Added — relaxed-scan visualization polish (writer + vibe-view)

  • Reaction-coordinate axis labels. ScanResult.write_qvf now records a human-readable coordinate label + unit (e.g. bond 0–1 / bohr) on the reaction.path section, and vibe-view labels the energy-plot x-axis with them ({label} ({unit})). Additive, optional metadata — no qvf_version bump; v1/v2 readers that don’t know the fields ignore them. Surfaced through write_reaction_path_qvf(..., reaction_coordinate_label=, reaction_coordinate_unit=).

  • Verified transition-state tagging. ScanResult.write_qvf(..., transition_state_frames=[i, ...]) tags caller-confirmed frames with a transition_state waypoint (red TS marker) and suppresses the generic point waypoint at those frames. The energy maximum is never auto-promoted to a TS — a scan max is not a verified saddle (CLAUDE.md §7).

Added — per-frame densities along reaction paths (writer + vibe-view)

  • Animated density isosurface on reaction.path. ScanResult.write_qvf(..., emit_volumes_every=N) attaches the electron density on a shared real-space grid for every N-th scan frame; vibe-view’s ReactionPathRenderer morphs the isosurface as the frame changes. Cubes are opt-in + decimated (one extra single-point SCF per emitted frame); the box auto-sizes to the union of emitted geometries. Molecular scans only — periodic raises NotImplementedError.

  • QVF schema (v1 + v2) SectionReactionPath gains two optional members — frame_volumes (a new Volume4DBinary, [n_emitted, nx, ny, nz]) + volume_grid — plus volume_frame_index / volume_label / volume_isovalue in the section metadata. Additive + optional; old readers ignore them, no archives invalidated. Low-level API: write_reaction_path_qvf(..., frame_volumes=, volume_grid=, volume_frame_index=, volume_label=, volume_isovalue=).

  • Shared isosurface builder. The marching-cubes core was factored out of VolumeRenderer.make_mesh into vibeview.renderers.volume.build_isosurface_mesh(...), reused by the reaction-path per-frame overlay.

Added — 2D relaxed scans + scan.surface QVF kind (writer + vibe-view)

  • vibeqc.relaxed_scan_2d(...) drives two internal coordinates over a grid, relaxing all other DOFs at each node (warm-started row by row), and returns a ScanResult2D with a [nA, nB] energy grid + per-node geometries. Both molecular and periodic.

  • New scan.surface QVF kind (registered in v1 + v2 schemas + vibe-view). Carries axis_a / axis_b (1D coordinate values), the 2D energies grid, per-axis label + unit, and optional flattened per-node geometries. High-level writer write_scan_surface_qvf(...); ScanResult2D.write_qvf(..., include_geometries=).

  • vibe-view ScanSurfaceRenderer renders a filled-contour energy map (matplotlib) with the minimum starred, a current-node crosshair, and both axes labelled from the coordinate names/units; reader read_scan_surfaceScanSurfaceData; registered in kinds.py, the renderer map, and the app activation + title.

Fixed — vibe-view test coverage gap for basis.ao

  • test_all_supported_kinds_have_renderers lacked a basis.ao fixture after that kind was added to SUPPORTED_KINDS (b9e97161); added one so the dispatch-coverage gate is green again.

Fixed — vibe-view variable-dim reaction-path wrapping

  • ReactionPathRenderer now wraps each frame by its own dimensionality via a new dim_for_frame(index) helper that honours dim_per_frame. Previously get_frame keyed off the scalar dim only, so a variable-dim path (scalar dim absent) silently fell back to dim=3 and mis-wrapped slab frames’ vacuum axis. Latent until a variable-cell/variable-dim producer lands, but now correct.

Fixed: ωB97X-D citation now carries the Grimme-2006 DFT-D2 reference

  • The Chai-Head-Gordon dispersion kernel (compute_chg_dispersion) uses the Grimme-2006 DFT-D2 atomic C6 / vdW-radius tables, but the citation database did not carry the origin paper, so a ωB97X-D job under-attributed its dispersion. Added [entries.grimme_dftd2_2006] (S. Grimme, J. Comput. Chem. 27, 1787, 2006; DOI 10.1002/jcc.20495) and wired it into routes.methods."wb97x-d" alongside the existing Chai-Head-Gordon 2008 references. A ωB97X-D job now emits Chai-Head-Gordon 2008 plus Grimme-2006 D2 in its .bibtex / .references sidecars and the .out references block; the .system manifest carries all three. tests/test_citations.py coverage was extended to pin both keys in the assembled citations and the .bibtex body. (2f577cf5)

Fixed — periodic infrastructure (end-to-end audit, 2026-05-30)

  • KPoints.auto(length) generated ~1.76× too-dense meshes per axis (≈5.4× too many k-points): the bohr⁻¹→Å⁻¹ conversion kept the 2π factor, so auto(30) on Si primitive returned 18×18×18 instead of the intended ~11×11×11. Now consistent per-axis with from_kspacing(2π/length) (VASP’s Auto↔KSPACING equivalence). Pure over-spend of compute — not wrong physics — but auto() is an advertised entry point.

  • madelung_constant_for_cell now refuses dim<3. It is a 3D Ewald self-energy (3D neutralising background); for 1D/2D cells it silently returned a meaningless negative value that fed into the exxdiv='ewald' K-shift. Now raises NotImplementedError, mirroring the existing run_pbc_gdf_rhf and C++ compute_nuclear_lattice_ewald dim guards. Default exxdiv='none' low-dim cells are unaffected.

  • run_krhf_periodic_gdf now RAISES on a converged non-physical energy instead of silently returning it. The multi-k compcell GDF path converges to garbage on tight ionic crystals (LiH primitive FCC at kmesh=(2,2,2): −2495 Ha, or +8.485 Ha with apply_aft_correction=True, vs PySCF −7.92 Ha) — the multi-k parity fix is the GDF chat’s open M2 milestone. Until it lands, a post-SCF energy-sanity guard rejects both the runaway-negative (|E| > max(10·ΣZ², 100) Ha) and the positive/unbound (E beyond a ~1 Ha bound-cell slack) signatures (CLAUDE.md §7: impossible energy = bug — surface it, don’t paper over with damping/thresholds). The slack lets cramped Bravais smoke-test cells through; check_energy_sanity=False bypasses the guard for parity/debug scripts. The working periodic-GDF path remains the Γ-only run_pbc_gdf_rhf(gdf_method='rsgdf').

  • The four run_pbc_bipole_* drivers no longer crash on dim<3. The documented “direct-only path for dim<3 diagnostic runs” was killed by two bugs: compute_ext_el_spheropole (a 3D-Ewald reciprocal-space K=0-limit correction that raises for dim≠3) was called unconditionally from all four drivers, and RKS/UHF/UKS left e_j_multipole unbound in the direct (non-Ewald-split) Fock branch, raising UnboundLocalError before the spheropole was even reached (RHF already initialised it). The spheropole call is now dim==3-guarded — the term is identically zero in the direct gauge, so e_ext_el_spheropole is None for 1D/2D — and e_j_multipole is initialised on every branch. The dim<3 energy is the direct-truncated value, verified vacuum-independent and equal to the molecular RHF energy in the isolated-cell limit (the audit’s 1D/2D correctness criterion). use_ewald_j_split=True stays explicitly rejected for dim<3. Orthogonal to the open BIPOLE L_max≥2 + Madelung-Ewald work.

  • Non-converged UKS BIPOLE now reports the spheropole. run_pbc_bipole_uks omitted e_ext_el_spheropole from its per-iteration PBCBipoleEnergyComponents and hard-coded E_sphero_final = None in its non-converged post-loop branch, so a max_iter-capped 3D UKS run returned result.e_ext_el_spheropole = None even though the term was already folded into E_total (RHF/RKS/UHF all reported it; ~0.003768 Ha on H₂/STO-3G/8-bohr). Both sites now mirror the RKS/UHF siblings; None stays correct for dim<3. Regression: tests/test_pbc_bipole_uks.py.

  • Regression gates for the k-mesh and Madelung fixes, plus the multi-k- GDF guard (it must raise on the LiH catastrophe, not return it), in tests/test_audit_20260530_periodic.py; the BIPOLE dim<3 path (all four drivers, vacuum-independence, isolated-molecule limit) in tests/test_pbc_bipole_dim_lt3.py.

Added: CASSCF (orbital-optimized CASCI)

New method="casscf" and the low-level vibeqc.solvers.casscf: complete active space SCF, optimizing the active-space CI vector together with the molecular orbitals. Built on the validated CASCI + RDM engine; a two-step macro-iteration rotates the inactive/active/virtual orbitals to a stationary point using the non-symmetric generalized-Fock orbital gradient g_pq = 2(F_qp F_pq) (validated elementwise against finite differences of the CASCI energy) with a backtracked augmented-Hessian step. Matches PySCF mcscf.CASSCF to ≤1e-7 Ha on unique-minimum systems (H₂, LiH, HF, Be); the variational sandwich E_FCI E_CASSCF E_CASCI and a vanishing orbital gradient are the basis-independent correctness checks. As with every CASSCF the energy surface is non-convex, so the optimizer reaches the stationary point in the basin of the starting HF orbitals. NEVPT2 / CASPT2 can run on the CASSCF reference. Ungated (validated vs PySCF). §8 citation route to Roos/Taylor/Siegbahn 1980. For small active spaces + bases. See docs/handover_multiref.md.

Fixed — multireference methods CASCI / NEVPT2 / CASPT2 (audit 2026-05-30)

The CASCI, NEVPT2 and CASPT2 routines that landed in 0c8f0fcf were incorrect (CASCI ≈ −104 Ha and crashing for >1 same-spin electron; NEVPT2 wrong-sign; CASPT2 ≡ 0) and are rebuilt + validated against PySCF out-of-process:

  • CASCI — rebuilt on the validated unrestricted Slater–Condon FCI engine with a correct frozen-core dressing. Matches PySCF mcscf.CASCI to ≤1e-9 Ha (H₂O CAS(2,2)/(4,4)/(4,6), H₂, LiH); full active space reproduces FCI.

  • NEVPT2 — strongly-contracted NEVPT2 (Angeli 2001) computed from the perturber-projection definition. Matches PySCF mrpt.NEVPT per excitation class to ≤5e-9 Ha.

  • CASPT2 — strongly-contracted, generalized-Fock H₀ (Andersson 1990). Its MP2 limit and full-space→0 limit are validated; the active-space terms have no local reference (PySCF ships no CASPT2), so CASPT2 is gated behind VIBEQC_EXPERIMENTAL_MULTIREF=1 pending OpenMolcas/ORCA validation.

All three are available via run_job(method="casci"/"nevpt2"/"caspt2", active_space=(n_orb, n_elec)) and the low-level vibeqc.solvers API, with a new validated reduced-density-matrix module (solvers._rdm) and §8 citation routes (Roos 1980 / Angeli 2001 / Andersson 1990). For small active spaces + bases (the spin-orbital engine scales steeply). See docs/handover_multiref.md.

Fixed — GFN2-xTB gated experimental + structural correctness (audit 2026-05-31)

The 2026-05-31 end-to-end audit found the molecular GFN2-xTB method (run_job(method="gfn2_xtb"), shipped v0.10.0) returned silently-wrong results: H₂ at +0.106 Ha (positive total energy), H₂O at −103.5 Ha (~20× over-bound; xtb ref ≈ −5.07 Eh), wrong-sign Mulliken charges (O came out +1.37), and it was not size-consistent — two H₂ 40 bohr apart over-counted by +0.32 Ha vs 2×E(H₂). Root causes, all fixed:

  • Repulsion had the wrong functional form. k_ab·exp(−α·R) — with α taken from the chemical hardness and an arbitrary 0.1 scaling — decayed far too slowly (~0.08 Ha per H–H pair still left at 40 bohr), which was the size-inconsistency. Replaced with the GFN2 form (Zᴬ_eff·Zᴮ_eff/R)·exp(−√(αᴬ·αᴮ)·R^{3/2}) (Bannwarth-Ehlert-Grimme 2019, Eq. 6), wired to the actual per-element REPA(α)/REPB(Z_eff) that were previously parsed and discarded.

  • Core/valence confusion. The driver filled the valence basis using the all-electron count (mol.n_electrons()) and referenced charges against the shell capacity (2 for s, 6 for p). Now uses the neutral-atom valence electron count (Z − noble-gas core) with correct per-shell reference occupations; the global mean-subtraction that coupled non-interacting fragments is removed (Σ Δq = 0 now holds by construction).

  • Energy double-counting in the shell-resolved 2nd-order electrostatics is corrected (E = Σ 2εᵢ ½ Δq_shell·γ_shell·Δq_shell + E_rep), and the SCC potential sign convention (Δq = pop − n0, Elstner 1998) is fixed — the pre-fix n0 pop ran the SCC the wrong way.

  • PBE-D4 dispersion bolt-on removed. gfn2.py had added compute_d4_energy_total(..., functional="pbe") on top of the SCC energy, double-counting dispersion with the wrong damping; GFN2’s own self-consistent D4 is part of the deferred full-method work.

After the fix GFN2-xTB is size-consistent (E(2 far)−2·E(1) < 1e-9, with the residual water-dimer interaction decaying as the physical 1/R³ dipole tail) and has correct Mulliken charge signs (O⁻/H⁺, C⁺/O⁻). It is still not quantitative, for two reasons: the GFN2-defining physics — anisotropic electrostatics (AES), the 3rd-order on-site term, self-consistent D4 — is not implemented, and a separate, pre-existing bug in the GFN2 H⁰/overlap (build_gfn2_hamiltonian_zero + the STO-6G overlap) produces spurious deep occupied eigenvalues whenever more than the s shell is filled, so absolute energies run ~20× too deep (an isolated closed-shell Ne atom, Δq=0, comes out at −4974 Ha with four −621 Ha occupied states instead of ~−5 Ha; H₂O at −99.7 Ha). That deep-state bug is diagnosed but not fixed here — it is H⁰/basis-reimplementation work, tracked in HANDOVER_SEMIEMPIRICAL.md.

GFN2-xTB is therefore now gated experimental: every evaluation emits a GFN2ExperimentalWarning (the run_job path included) so an advertised method no longer returns wrong numbers silently (CLAUDE.md §14), and the §8 Bannwarth-2019 citation + route are wired. The runner now reports the real SCC iteration count (it previously hard-coded n_iter=1, the source of the “converged in 1 iterations” log). The unrestricted (run_ugfn2_xtb) and periodic (run_gfn2_xtb_gamma) variants share the model gaps and the deep-state bug and are not in the run_job path. Regression gates (size consistency, charge signs, repulsion decay, the experimental gate; quantitative targets xfail): tests/test_gfn2_xtb.py. Patch-candidate: v0.10.x.

Fixed — GPW periodic density collocation: periodic-image AO sum (audit 2026-05-31)

collocate_density_on_grid (and the Hartree-J builder it backs) evaluated the molecular AO accessor on the real-space FFT grid with no periodic-image summation, so an AO whose centre sits near a cell face leaked its Gaussian tail out of the box and the collocated density under-integrated. H₂/STO-3G in a 10-bohr cell with the atoms on the corner gave ∫ρ dV = 0.41 e against tr(D·S) = 2 e (atoms at the centre were fine at 2 e). Every GPW density / cube / ELF / QVF surface for a structure with atoms near a cell edge was wrong by that leak, and the docstring’s “integrates to tr(D·S)” claim held only for centred atoms (CLAUDE.md §7 — a source bug, not a quadrature problem). Root cause: a divergent re-implementation — the periodic stack already had the correct image-summed evaluator (vibeqc.ewald_j.evaluate_ao_periodic, the v0.6.x translation-invariance fix), which the M2 GPW module never used.

Fix (python/vibeqc/periodic_gapw_j.py): a new _ao_values_on_grid helper sums the AO over the 3³ nearest periodic images (χ_μ(r) = Σ_R χ_μ^mol(r R), _AO_IMAGE_RADIUS = 1) by shifting the sample points along the grid lattice — the grid-native twin of evaluate_ao_periodic, needing only the lattice (no PeriodicSystem). All three AO-on-grid sites now route through it: collocate_density_on_grid (density/cube/ELF/QVF), project_potential_to_ao (V→AO), and GpwJBuilder._chi (the cached χ that build_J uses for both its density collocation and its projection) — so the Hartree-J build no longer leaks charge for off-centre atoms either. After the fix the corner density integrates to 2 e, and both ∫ρ and the GPW Hartree energy ½tr(D·J) are translation-invariant to grid-quadrature noise (~1e-8). No-op for centred atoms in a vacuum-padded cell (the image sum is exp(−α·L²)), so the existing GPW suite is unchanged. New regression tests in tests/test_periodic_gapw_j.py: test_collocate_density_corner_atoms_integrate_to_n, test_collocate_density_is_translation_invariant, test_gpw_hartree_energy_is_translation_invariant.

Fixed — multi-k GPW energy breakdown sums to the total (audit 2026-05-31)

The second of the two v0.12.x GPW audit findings (the collocation fix above is the first). run_periodic_rks_gpw_multi_k reported the correct result.energy but rebuilt result.breakdown by re-evaluating the kinetic + nuclear-attraction terms at Γ (k = 0) only — tr(D_total · T^Γ) — dropping the per-k Bloch phases. breakdown.e_total then disagreed with result.energy by tens of mHa on a dispersive mesh (−60.8 mHa for [2,1,1] H₂/STO-3G in a 6-bohr cell), and the wrong decomposition was serialised into restarts. The breakdown is now accumulated from the per-k Bloch-summed Hcore(k) — the same sum that yields the reported total — so breakdown.e_total == result.energy. Experimental jk_method="gpw" path; molecular all-electron path unaffected. Regression test (tests/test_periodic_gapw_j.py): test_multi_k_gpw_breakdown_matches_total.

Fixed — periodic job crashed on any artifact-writer failure (UnboundLocalError); molden non-ASCII hardening (audit 2026-06-01)

run_periodic_job carried a redundant local re-import of OutputFailureKind / warn_output_failure inside one late except branch (from vibeqc.output.errors import ). Because Python binds a name function-local if it is assigned anywhere in the body, that one import shadowed the correct module-level import (from .output._errors import ) across the entire function — so every earlier warn_output_failure(...) call (the molden / extended-XYZ / POSCAR / CIF / XSF / population / density / manifest error handlers) raised UnboundLocalError: cannot access local variable 'warn_output_failure' before that late import line ran. A failure in any optional-artifact writer therefore crashed the whole periodic job after the SCF had already converged, instead of emitting the intended graceful vibe-qc output [optional_artifact] warning and continuing. The shadowing import was itself broken — vibeqc.output.errors does not exist (the module is vibeqc.output._errors, with the underscore), so the branch would have raised ModuleNotFoundError even if reached. Fix: delete the redundant local import; the module-level import already binds both names for the whole function.

Concrete real-world trigger: a non-ASCII output= stem. The molden writer opened its file encoding="ascii" and wrote the user-controlled [Title] (the output basename), so an accented stem (e.g. output="café") raised UnicodeEncodeError mid-write → the molden error handler fired → UnboundLocalError → the job died. Molden is now opened encoding="utf-8" and its [Title] is scrubbed through vibeqc.output._text_safety.scrub_output_text — the same Trojan-Source hardening v0.10.4 shipped for the Gaussian-cube header and the TOML manifests (see the v0.10.4 Security entry below). This is the molden sink that v0.10.4’s audit deferred as “reachable only via direct Python calls”: in fact both run_job and run_periodic_job feed the user-facing output basename into the molden title, so the widening to UTF-8 is paired with scrubbing to keep a bidi / control byte from leaking into the file. A non-ASCII periodic job now writes the molden artifact (UTF-8, accent preserved) instead of crashing or skipping it. Regression tests: tests/test_output_errors.py::test_periodic_scf_survives_writer_failure (the UnboundLocalError reproducer) and two new molden pins in tests/test_output_control_char_hardening.py (test_molden_header_scrubs_tainted_title, test_molden_file_accepts_accented_title_as_utf8). Patch-candidate for v0.10.x.

[v0.10.5] — 2026-06-01

Correctness patch on Pisani’s Penguin — composite -3c method total energies. One cherry-picked fix from main; no feature changes. Inherits the v0.10.0 codename.

Fixed — composite “-3c” method total energies (audit 2026-05-31)

The composite -3c keywords gave wrong total energies (live in v0.10.3). All fixes are validated against the grimme-lab/gcp (mctc-gcp) Fortran reference run out-of-process (CLAUDE.md § 10). The SCF + arithmetic was correct; the bugs were routing and the gCP/SRB correction terms.

  • hf-3c ran FCI instead of HF. Pure-HF composites (functional is None) routed through the wavefunction auto-ladder (≤ 4 e⁻ → FCI), so a turnkey hf-3c on H₂ silently ran fci(ndet=4). Pure-HF composites now resolve to rhf / uhf. (F1.1)

  • D3-BJ was dropped from the composite total whenever the SCF returned a SolverResult; the total now reads the D3-BJ term from a local rather than off the (unwrapped) result. (F1.2)

  • wb97x-3c downgraded RUNNABLEPENDING_ECP. vDZP is a valence-only basis whose small-core ECPs need the libecpint inline-primitive feed (Phase 14g, not on main); it was silently running all-electron (≈ −35.9 Ha on H₂O vs ≈ −76.4). It now raises CompositeUnavailable. hse-3c (all-electron def2-mSVP) is unaffected and stays RUNNABLE. (F1.3)

  • Short-range bond (SRB) correction rewritten to the canonical kind-discriminated HF-3c / B97-3c forms (gcp.f90 basegrad / srb_egrad2) using the bundled DFT-D3 r0ab radii (new vibeqc._d3_r0ab, extracted by scripts/extract_d3_r0ab.py). The previous single form was ≈ 2× too binding (−52 vs −22.2 kcal/mol SRB on H₂O for HF-3c). Matches mctc-gcp to < 1e-9 Ha. (F1.4)

  • r2scan-3c gCP corrected from +10.18 to +1.12 kcal/mol on H₂O. The def2-mtzvpp n_virt were the generic basis-function counts, not the r²SCAN-3c-specific virtual counts, and the gCP damping term (gcp.f90 damp=.true.) was absent. Added the damped gCP (opt-in damping= on compute_gcp; recipe-side GCPDamping) and corrected n_virt for def2-mTZVPP / def2-mSVP / MINIX. B97-3c’s spurious gCP removed — it is SRB-only (mctc-gcp computes SRB-only for b973c). (F1.5)

PBEh-3c / B97-3c / B3LYP-3c / r²SCAN-3c / HSE-3c composite totals shift by the corrected gCP/SRB terms; wb97x-3c now raises instead of returning a wrong number. Adds tight-tolerance per-composite component + total-assembly regression tests. (Cherry-picked 000c7f68.)

[v0.10.4] — 2026-06-01

Security + correctness patch on Pisani’s Penguin — two cherry-picked fixes from main, no feature changes. Inherits the v0.10.0 codename.

Security — output writers neutralise bidi / zero-width / control characters (audit 2026-06-01)

User-facing output writers interpolated user-controlled free text — the run_job(output=...) basename, the basis / functional / method names, SCF exception messages — verbatim into file headers and the hand-rolled .system / crash-.dump TOML manifests. A bidirectional format control such as U+202E RIGHT-TO-LEFT OVERRIDE written raw silently reverses how the file renders in terminals, editors, grep / diff viewers, and log-parsing agents — the “Trojan Source” display- deception class (CVE-2021-42574 / CWE-150). The manifests’ hand-rolled TOML quoter escaped C0 controls (ord < 0x20) but let every codepoint ≥ 0x20 through, so the bidi (U+202A–E, U+2066–9), zero-width (U+200B–F), BOM (U+FEFF) and C1 (U+0080–9F) classes passed unescaped; the Gaussian-cube header wrote its title line with no quoting at all. An audit of all ten output writers confirmed three sinks reachable from the public run_job surface — the cube header, the .system manifest, and the crash .dump; the remaining writers’ free-text parameters (QVF labels, molden / xyz / POSCAR comments) were deferred — see the follow-up entry below, which corrects this note: several are in fact reachable via run_job / run_periodic_job. New shared helper vibeqc.output._text_safety defines the dangerous-character class once and provides toml_escape_str (escapes it as lossless \uXXXXtomllib round-trips the value back) and scrub_output_text (visible \uXXXX tokens for the escape-less cube header); the three manifest _toml_str emitters now delegate to it and the cube writer scrubs its title / comment. Not a memory-safety / RCE issue — it matters mainly for multi-tenant vq queues, where one user’s job spec is read back in another’s output. Regression test: tests/test_output_control_char_hardening.py. (Cherry-picked 6267edcd.)

Security — periodic-structure + trajectory writers scrub user comments (2026-06-01 audit follow-up)

Completes the output-writer coverage from the audit above. The cube / .system / crash-.dump sinks were hardened in 6267edcd; molden was hardened alongside a periodic crash fix (Fixed entry above). This lands the remainder: the POSCAR, CIF, XYZ, extended-XYZ and multi-frame XYZ trajectory writers now scrub their free-text comment / header lines through vibeqc.output._text_safety.scrub_output_text. QVF was initially believed safe (JSON serialisation) — that call was wrong (it uses ensure_ascii=False); QVF is hardened in the follow-up entry below.

Reachability correction: the original audit deferred these as “reachable only via direct Python calls”. That was an under-call (the molden fix found the same). run_periodic_job embeds the functional and basis name — both user-controlled run_* arguments — into the POSCAR and CIF comment lines (periodic_runner.py), so those two are genuinely user-reachable, the same Trojan-Source display-deception class (CWE-150) as the cube header. The XYZ / extended-XYZ / trajectory comment parameters are not currently routed from user input by the runners, but are hardened for completeness so a future wiring cannot regress. Low severity; no API change. Regression tests added in tests/test_output_control_char_hardening.py (test_xyz_comment_scrubbed, test_extended_xyz_comment_scrubbed, test_poscar_comment_scrubbed, test_cif_comment_scrubbed, test_trajectory_comment_scrubbed).

Security — QVF archive neutralises bidi / control characters in JSON members + member names (2026-06-02)

Closes the last output-writer gap from the 2026-06-01 audit, and corrects an earlier wrong call: the QVF (.qvf) zip writer was assumed safe because it serialises metadata as JSON — but every json.dumps used ensure_ascii=False (28 sites). JSON only force-escapes C0 controls, so the bidi (U+202A–E, U+2066–9), zero-width (U+200B–F), BOM (U+FEFF) and C1 (U+0080–9F) classes in user-controlled labels / descriptions / coordinate units / provenance (method / functional / basis) were written raw into the archive’s JSON members — the same Trojan-Source display-deception class (CWE-150) as the other writers, reachable via write_qvf(...) provenance and the periodic runners. Separately, the generic-spectra section_id was interpolated into a zip member path (spectra/{section_id}.json) without the _slug() pass the volume labels use — a bidi and path-traversal gap.

Fix: a new vibeqc.output._text_safety.safe_json_bytes(obj, *, indent=) serialises with ensure_ascii=False (legitimate non-ASCII — accents, Greek — stays human-readable) but re-escapes only the dangerous class to a visible \uXXXX token; all 28 qvf json.dumps sites route through it, and section_id is now slugged. Volume / orbital labels were already slug-safe in member names. Low severity; output bytes are unchanged for payloads without dangerous characters, so existing QVF round-trip tests are unaffected. Regression tests: test_safe_json_bytes_neutralises_and_round_trips (tests/test_output_control_char_hardening.py) and TestProvenance::test_bidi_control_chars_neutralised_in_archive (tests/test_qvf_writer.py).

Fixed — periodic gauge consistency (audit F1/F3)

  • Multi-k Ewald / Γ-GDF gauge mismatch returned converged=True at a non-physical energy (CLAUDE.md § 7). run_rhf_periodic_multi_k_ewald3d hard-codes the Hartree J to the Ewald-3D builder but read V_ne / e_nuc from lat_opts.coulomb_method; a default PeriodicRHFOptions() (DIRECT_TRUNCATED) paired bare-gauge V_ne / e_nuc with the Ewald-3D J, so the Madelung / charged-cell self- energy did not cancel — LiH FCC primitive at Γ converged to −264.85 Ha (gauge-consistent: −7.52 Ha) with no warning. F1 forces EWALD_3D at entry with an INFO-log override; F3 matches the Γ-GDF molecular-limit / dim<3 nuclear cutoff to the integral cutoff. Only direct callers of the non-dispatcher drivers were exposed. (Cherry- picked a261613a.)

Not in v0.10.4

The remaining 2026-05-31 audit fixes — the BIPOLE analytic-gradient rebuild, GFN2-xTB experimental gating, BIPOLE dim<3 guards, the GDF Γ-only µHa V_long fix, and the CASCI / NEVPT2 / CASPT2 rebuild — are entangled with the post-v0.10.2 periodic / BIPOLE / GDF feature work and do not cherry-pick cleanly onto the v0.10.3 base. They ride v0.11.0 (Sun’s Stingray). The two hard v0.11.0 gates remain open: multi-k GDF µHa parity and the F2 charged-cell analytic-FT Hartree J.

[v0.10.3] — 2026-05-30

Docs-only patch on Pisani’s Penguin — release-process and contributor-discipline updates targeting the v0.11.0-prep audit cycle. Inherits the v0.10.0 codename. No code changes.

Added — contributor discipline

  • CLAUDE.md § 8 — inline formula + reference-value comments in code. New standing rule: when a chat implements math from a published paper, the formula and any published reference values used for validation go into inline comments at the relevant code sites. The maintainer keeps PDFs of cited papers in a gitignored references/ folder; the inline comments are the only public-facing record of what equation the code implements and how anyone can verify it without leaving the source file. Targets the v0.11.0-prep audit by external (including LLM-based) reviewers. (Cherry-picked 657a1644.)

Fixed — release-process playbook

  • docs/release_process.md — CHANGELOG carry-back step explicit. Closes a long-standing gap in the tag-day playbook: the patch-release flow tagged the new dated CHANGELOG section onto the hotfix branch (which became release tip) but never carried it back to main, leaving main’s CHANGELOG drifted by one section per patch cut. The v0.9.0 → v0.10.2 five-release reconciliation on 2026-05-29 traced to this missing step. Two playbook updates:

    • Minor release § 2 (“Bump the version”) now explicitly bundles the [Unreleased] [vX.Y.Z] CHANGELOG promotion

      • banner RELEASE_CODENAMES + CITATION.cff + docs/citing.md edits in the release: vX.Y.Z commit.

    • Patch release § 8 is now “carry the new [vX.Y.Z+1] dated section back to main” with the explicit mechanical recipe; steps 9-11 renumbered. (Cherry-picked 7ad77f10.)

Not in v0.10.3

The CHANGELOG reconciliation + trim on main (ce8e8785 + 55bb61f3) is deliberately not in v0.10.3 — those edits were constructed against main’s pre-reconciliation state and the cherry-pick would conflict. Release’s CHANGELOG carries the same [v0.9.0] / [v0.9.1] unpromoted-bucket gap until v0.11.0’s normal cut flow (now governed by the carry-back playbook from §1 above) lands a clean CHANGELOG on release at tag-day.

The installation.md banner-sample refresh (0737543f) is also not in v0.10.3 — it’s cosmetic and version-numbered for the v0.10.2 → v0.11.0 transition; rides v0.11.0.

[v0.10.2] — 2026-05-29

Patch release on top of Pisani’s Penguin — curated fixes only. Inherits the v0.10.0 codename.

Fixed

  • BIPOLE multipole far-field — nuclear-charge compensation added; multi-k + UHF tests pinned (e18e8f7e).

  • BIPOLE use_multipole_far_field default flipped from None to False so default behaviour is explicit (584679df).

  • Semiempirical periodic OMx parity gap closed via Loewdin eigenvalue flooring (3152672a). The molecular ↔ periodic agreement now matches on the regression suite.

  • GFN2-xTB default max_iter raised 100 → 200 to fix H₂O / CH₄ non-convergence in default-options runs (695bc7be).

Not in v0.10.2

Three further v0.10.0 follow-up commits hit cherry-pick conflicts on the v0.10.1 hotfix branch and are deferred to v0.10.3 or v0.11.0:

  • d7f4b3bd fix(madelung): proper Ewald sum replaces cubic Wigner shortcut LiH closes to 2 mHa (substantial physics fix; conflicted on tests/test_pbc_gdf_compcell.py).

  • ebe6ca95 fix(bipole): multipole L_max≥2 spherical conversion + RKS XC fixes + multi-iter test (conflicted on python/vibeqc/pbc_bipole_rks.py against the earlier-picked BIPOLE fixes).

  • f6e01bbe docs(v0.9.0): catch user-guide + API ref up to recent landings (same CHANGELOG + gapw conflict as v0.10.1 deferral).

The post-v0.10.0 GDF chemical-accuracy work (018784c0 all-FT-Bloch path for RSGDF + c949d748 modrho convention + the M1 investigation diagnostics) is deliberately not in v0.10.2 — that is the v0.11.0 (next minor) headline scope, not patch-grade material. DFT+U Increment 3 (analytic gradient — 27afac4c) and GPW M3b (68f986ee + companions) likewise stay queued for v0.11.0.

[v0.10.1] — 2026-05-29

Patch release on top of Pisani’s Penguin. Documentation-only — no code changes. Inherits the v0.10.0 codename.

Fixed

  • Landing-page admonition refresheddocs/index.md was still surfacing the v0.9.0 “Knowles’s Kingfisher” headline block at v0.10.0 tag time; swapped to v0.10.0 Pisani’s Penguin per the prep doc’s pre-staged Block 2 (fallback variant, since periodic GDF is deferred to v0.11.0).

Added — documentation

  • DFT+U user-guide page (docs/user_guide/dft_plus_u.md) with worked examples covering the molecular RHF/UHF/RKS/UKS + periodic Γ-only RHF/RKS surface that shipped in v0.10.0 (Increments 1–2 + 4a/4b/4d-light).

  • GAPW user-guide updates — runner dispatch + the M3b GAPW path added in v0.10.0.

  • User-guide + API-reference catch-up to the v0.9.0 surface, the long-deferred docs-sprint sweep that didn’t run after the v0.9.0 / v0.9.1 cuts (item #22 in the post-tag docs queue — partial close; the CHANGELOG carry-forward gap stays on the queue for v0.10.2 or later).

  • from_* crosswalk tables brought up to the v0.9.0 surface.

  • API reference index expanded to cover the v0.9.0 surface.

Not in v0.10.1

The post-v0.10.0 code fixes that landed on main (semiempirical OMx periodic parity, BIPOLE multipole far-field, Madelung Ewald sum proper, GDF chemical-accuracy improvements, DFT+U Increment 3 analytic gradient, GPW M3b) are not in v0.10.1 — they ride v0.10.2 (or v0.11.0). v0.10.1 is a strict docs-only patch to correct the stale landing-page surface; mixing in code fixes would have made it a fix-grade patch rather than a docs-grade one.

[v0.10.0] — 2026-05-27 — Pisani’s Penguin

Periodic-SCF maturity: multi-k EDIIS + Python accelerator family; SYM6 crystal symmetry (full character-tables + M3b per-cell-pair J/K kernel); BIPOLE L_max≥2 Cartesian→spherical multipole conversion; GPW M2-full iterative SCF; CRYSTAL-α / Ewald-ω gauge unification (the cluster-A1 fix at 859efe0a resolves +19 Ha translation-invariance violation on LiH rocksalt by enforcing EWALD_3D and matching CRYSTAL’s 2.8/V^(1/3) α formula); end-to-end CRYSTAL parity test suite (P1–P5 at 5231ce6c).

Semiempirical methods diversification: GFN2-xTB (full stack: parameter parser/fetcher, Fock builder, SCC driver, analytic gradient, periodic Γ, UGFN2-xTB, periodic gradient + stress); PM6 (NDDO Fock builder, STO-overlap resonance, FD gradient, MOPAC-matching core-core damping); OM1/OM2/OM3 (skeletons, parameter loaders, Loewdin-correction Fock builder, 77-test benchmark suite); architectural Phase-0 framework — method registry

  • shared parameter interfaces + Hamiltonian builders + CoreParameterSet basis builder.

DFT+U (Hubbard correction, Dudarev convention) — Increments 1, 2, 4a/4b/4d-light: occupation-matrix machinery; C++ Eigen kernel + pybind11 surface; RHF/RKS/UHF/UKS molecular SCF integration; periodic Γ-only RHF + RKS; run_periodic_job(..., dft_plus_u=[HubbardSite(...)]) high-level surface. Increment 3 (analytic gradient) is queued for v0.10.x — see Known limitations.

Nudged Elastic Band (NEB) — M1–M4 — path primitives (interpolate_linear + IDPP) → improved-tangent driver with joblib per-image SCFs → climbing-image NEB (Henkelman + Uberuaga + Jónsson 2000) → periodic dispatch via BIPOLE + FD gradient. Density warm- start across RHF/UHF/RKS/UKS drivers; end-to-end H + H₂ benchmark example.

Other v0.10.0 deliverables: PWPB95 double hybrid (first meta-GGA / SOS double hybrid in vibe-qc); periodic D3-BJ dispersion; native slab + adsorbate builder; relaxed coordinate scans (molecular + periodic); Fermi-Dirac smearing consolidation; QVF spec v1.1 (DOS, potential, RDG, Fermi surface, phonons, EOS, extension governance); vibe-view Phase A (bond angles, isosurfaces, bands+DOS panel, publication export); 87 BSE-fetched basis sets integrated under the §1 Feist clearance (docs/license.md § 3a + § 3e for the in-house semiempirical parameters).

Retraction

  • v0.9.0 Known-issues — XC residuals retracted. v0.9.0’s CHANGELOG flagged the polarised-GGA xc-kernel API + M06 meta-GGA UKS parity as residuals “tracked for 0.9.1.” Subsequent investigation by the XC chat confirmed both were already closed at the v0.9.0 cut commit by b34f051c — which bundled the XC fixes alongside the test-gate restore + the k-GDF rewrite, despite its misleading fix(release): subject. M06 drift was a vibe-qc/PySCF grid-mismatch artefact, resolved by pinning a matched fine grid (_fine_m06_grid: 120/23/46; PySCF atom_grid=(120,590), prune=None, use_newton). Tolerance kept at 2e-5 — CLAUDE.md § 8 discipline respected, not loosened. No code change in v0.10.0 — this entry retracts an overcautious release note.

Known limitations / deferrals

  • Periodic GDF (compcell + multi-k + AFT) — deferred to v0.11.0. GDF chat made incremental progress (dfa34b4a Bloch-summed lattice pair-FT fix in _compcell_aft_correction_3c; 871c5f4a G=0 Madelung correction with H₂ sub-µHa PySCF parity), but the M1 investigation (b8f1e509) characterised remaining bugs not closed in this cycle: RSGDF SR+LR calibration is 98.5% off for L=0 / 23.8% off for L=1 on periodic cells, and the compcell metric is near-singular (cond 1e10–1e14) for tight ionic crystals. Fourth-cycle GDF deferral (v0.8.0 → v0.9.0 → v0.10.0 → v0.11.0); see docs/roadmap.md for the honest pattern.

  • DFT+U Increment 3 (analytic gradient) — open math question flagged by the DFT+U chat in HANDOVER § 1i. The Increments that DID land (1, 2, 4a, 4b, 4d-light) ship in v0.10.0 — molecular RHF/UHF/ RKS/UKS + periodic Γ-only RHF/RKS. Numerical gradients still work; the analytic gradient lands in v0.10.x once the math question resolves.

  • Native D4 default-backend flip — stays opt-in. Available via backend="native"; the dftd4 LGPL Python dep remains the default until the production-dataset upgrade lands (queued for v0.10.x / v0.11.0).

  • ωB97X-D — still blocked on a D2/CHG dispersion reference.

  • Solvation analytic gradients — slipped from v0.9.1; v0.10.x.

[v0.9.1] — 2026-05-22

Patch release on top of Knowles’s Kingfisher. Curated fixes from the v0.9.0 stabilization window; no new features. Inherits the v0.9.0 codename.

Fixed

  • QVF (vibe-view archive format) — critical contract fix. Align the QVF writer schema with the vibe-view consumer; align the producer manifest with the consumer contract; unify voxel_vectors semantics (per-voxel step, not full span). Fixes silent producer/consumer drift that could mis-render density / orbital cubes in vibe-view.

  • Solvers — audit fixes. DMRG _build_full_hamiltonian now enforces particle-number conservation (N-electron sector). active_space, the DMRG particle-number gate, and the v2RDM Q/G rejection paths corrected.

  • BIPOLE periodic driver — initialise e_j_multipole in the non-Ewald-J-split path (no behaviour change in the split path).

  • Symmetry — handle already-primitive cells in symmetry=True, plus an M2a LMS fix.

  • ECP — corrected Si UHF reference in tests/test_ecp_scf.py to the post-899ab16a ³P ground state.

  • Bundled basis library — restored per-publication citation headers on pob-tzvp.g94 / pob-dzvp-rev2.g94 / pob-tzvp-rev2.g94 (Peintinger 2013 / Vilela Oliveira 2019).

Documented / hardening

  • CI deploy — pin vibe-qc.com SSH host keys instead of TOFU at job time (audit #6).

  • Privacy — scrub maintainer home paths from runtime defaults (audit #7).

  • Packaging — removed the broken viewer-gpu extra; the two-step install (pip install ./vibe-view then pip install vibe-qc) is now documented. A PEP 508 direct-URL restore is queued for v0.9.2.

  • DF + f-shell gradient bug — resolution recorded in the v0.8.0 notes.

Not in v0.9.1

The vq-queue security improvements (vq v0.6.44–v0.6.48), the periodic-Becke C++ default flip, the Ewald-ω cutoff derivation, and the remaining QVF density-grid unit refinement are deferred to v0.9.2 / v0.10.0 — these picks conflicted on the v0.9.0 hotfix branch and need either intermediate dependencies or a separate patch effort. They remain on main.

[v0.9.0] — 2026-05-22 — Knowles’s Kingfisher

Wavefunction methods (FCI / Selected-CI / DMRG / v2RDM), the native D4 dispersion backend, the semiempirical DFTB structure-optimization stack, M-series crystal symmetry, and the BIPOLE periodic driver.

Known issues

  • v0.9.0 follows a ship-then-iterate plan. It builds and imports cleanly and the ECP-SCF segfault regression is fixed and verified on the cut commit, but the full pytest -m 'not slow' suite had not completed a clean end-to-end run on the exact cut commit at tag time. Residual failures from the v0.9.0 stabilization (polarised-GGA XC kernel, M06 meta-GGA parity) are tracked for 0.9.1.

  • Periodic GDF and ωB97X-D are not in v0.9.0 — both deferred (see docs/roadmap.md).

Added — native D4 dispersion backend (Phase D4b complete)

  • Native D4 dispersion — MPL-2.0 implementation replacing the optional LGPL-licensed dftd4 dependency. All 8 Phase D4b milestones landed:

    • Imaginary-frequency CPHF polarizability (D4b-1)

    • Molecular C6 pipeline + DOSD validation (D4b-2)

    • Reference-system catalogue — 21 systems over period-2 (D4b-3)

    • CN + charge Gaussian-weighted C6 interpolation (D4b-4)

    • Two-body BJ damping + 51 per-functional parameter sets (D4b-5)

    • Three-body Axilrod-Teller-Muto term (D4b-6)

    • Analytical gradient (geometric + C6 chain rule) (D4b-7)

    • compute_d4(mol, func, backend="native") integration (D4b-8)

  • Reference datasetd4_reference_data.json (60 KB), computed from first principles at the def2-svp level. 8 elements (H-B-C-N-O-F-He-Ne), 21 reference systems. Upgrade path to aug-cc-pVDZ for production accuracy is documented.

  • Modules: vibeqc.dispersion_d4_model (14 public functions including compute_c9 and d4_atm_triple_energy), vibeqc.dispersion_d4_parameters (51 functionals), vibeqc.dispersion_d4_reference_data (D4ReferenceDataset), vibeqc.dispersion_d4_reference_systems (21 systems).

  • The dftd4 backend remains the default pending production dataset upgrade; switch via backend="native". See docs/handover_d4_native.md.

Fixed — D4 ATM three-body test API (1655013e)

  • Exported compute_c9 and d4_atm_triple_energy from vibeqc.dispersion_d4_model — the two factored helper functions the D4b-6 test suite expected but that were missing from the initial ATM three-body implementation. Refactored compute_d4_atm_energy to use them internally, removing duplicated math. (6 CI failures resolved.)

Fixed — ECP SCF segfault on molecules with fewer electrons than the ECP core

  • run_rhf / run_rks / run_uhf / run_uks segfaulted the process when an ECP replaced more core electrons than the molecule had: the valence count n_elec = n_electrons total_ncore went negative, producing a negative occupied-orbital count and a cblas_dgemm call with a negative dimension. The drivers now raise a clear std::invalid_argument that names the likely cause (a Molecule charge that pre-subtracts the ECP core). Regression introduced by the ECP-ncore propagation change (899ab16a).

  • Corrected three ECP tests (test_ecp_validation.py, test_ecp_scf.py, test_solvation_cpcm.py) that built Zn²⁺ with a pre-subtracted Molecule charge against the pre-899ab16a convention. They now pass the physical ionic charge (+2), so Molecule.n_electrons() is the full electron count the SCF expects before it subtracts the ECP core itself.

Added

  • Non-mean-field solvers (vibeqc.solvers): four wavefunction-based electronic-structure methods that do not conceptually rely on a Hartree–Fock reference state.

    • Selected CI (CIPSI-style): iterative determinant selection, variational diagonalization, PT2 correction.

    • DMRG: two-site MPS sweeps with bond-dimension scheduling and SVD truncation.

    • Variational 2-RDM: augmented-Lagrangian SDP with PQG N-representability constraints.

    • Transcorrelated Hamiltonian: similarity-transformed H via Gaussian/Jastrow correlators, consumable by CI/DMRG backends.

  • run_job(method="selected_ci" | "dmrg" | "v2rdm" | "transcorrelated_ci") — all four methods accessible through the standard runner.

  • Unrestricted Slater–Condon rules and exact FCI in the determinant basis (verified against PySCF FCI to machine precision).

  • vibeqc.solvers top-level namespace: solve_selected_ci, solve_dmrg, solve_v2rdm, build_transcorrelated_hamiltonian, Hamiltonian, etc.

  • Tutorial: docs/tutorial/non_hf_solvers.md.