Changelog¶
All notable changes are collected here. Format loosely follows Keep a Changelog.
[Unreleased]¶
Heading toward v0.7.x maintenance + v0.8.0. v0.7.x will close the tight-ionic-crystal multi-cell-density bug (LiH conventional and similar dense rocksalts); v0.8 is reserved for periodic stress tensor + slab builder + an external structure fetcher (
vqfetchCLI; pulls crystal & molecular structures from OPTIMADE / Materials Project / NOMAD / COD into ready-to-run vibe-qc inputs with full citation provenance — design contract onclaude/awesome-noether-2de783:docs/handover_structure_fetcher.md, implementation in flight on a separate branch).
[v0.7.3] — 2026-05-06 — Whitten’s Bridge — DF wiring into the regression runner¶
🎯 Tiny wiring patch + one bug discovery. The molecular regression runner (
examples/regression/core/runner_vibeqc.py) v0.7.1 had a placeholder that returnedstatus='unavailable'for every DF case; v0.7.2 landed thedensity_fit=True/aux_basis=...API on every SCF + post-HF options class. v0.7.3 closes the gap — the runner now drives the new API, with the SCF using the JK aux (default_aux_basis_for(basis, kind='jk')) and the post-HF MP2 step picking the RI aux (kind='ri') since the def2 family separates JKFIT from RIFIT.Codename honours John L. Whitten, whose 1973 paper (J. Chem. Phys. 58, 4496) is the foundational density-fitting / RI work that later RI-MP2, RIJK and SOS-MP2 build on. Bridge: the (case, code) wiring from regression spec to vibe-qc DF kernel that this patch finally closes — and the bug it surfaced by closing it.
Changed — runner DF wiring¶
runner_vibeqc.run_molecule_casenow setsopts.density_fit = True+opts.aux_basis = <jk aux>wheneverMethodSpec.dfis True, instead of returning early withstatus='unavailable'._resolve_aux_basis(method, basis_name, kind)helper picks the aux basis: preferMethodSpec.aux_basisif set, else fall back tovq.default_aux_basis_for(basis_name, kind=...). vibe-qc’s bindings raise ondensity_fit=True+ emptyaux_basis, so this must always resolve to something concrete._make_mp2_options(method, basis_name)builds the post-SCF MP2 / UMP2 options with their own DF flags. The MP2 RI aux is theRIFITfamily (def2-svp-rifit, etc.) — different from the SCF’s JKFIT — so it’s re-resolved withkind='ri'. Bothvq.run_mp2andvq.run_ump2now take this options arg (matches the v0.7.2 binding signatures).Verbose log records the active aux basis next to the
format_optionsdump so each DF case’s run is reproducible from its.logfile alone.
Found — vibe-qc DF SCF SIGSEGV (open in v0.7.x)¶
Smoke-tested on planetx 2026-05-06 with (h2o, benzene) × def2-svp × {rhf-df, rks-pbe-df}. The runner wiring is correct (DF flags reach
the C++ kernel) but vq.run_rhf segfaults inside the DF code
for both molecules with both default_aux_basis_for('def2-svp', 'jk')
== def2-svp-jk and with def2-universal-jkfit aux. The crash is
in vibe-qc’s DF SCF internals (signal 11, no Python traceback —
below the Python boundary), not in the runner or aux loading
(def2-svp-jk.g94 is present and parses correctly).
The v0.7.2 subprocess-isolation layer captures all three crashes as
clean error rows (“isolated subprocess died (signal 11)”) in the
suite’s summary.md; the dispatcher continues to the next case and
the run completes. This is the v0.7.2 wave-1.7 design payoff
working as advertised: the regression suite turned a hidden DF SCF
bug into a single-line action item without bricking the whole run.
Triage handed off to the periodic / SCF dev chat as a wave-1.8
candidate. Reproduces with examples/debug/df_smoke.py-shape
two-line scripts; smaller than h2o so a debugger run shouldn’t be
expensive.
[v0.7.2] — 2026-05-06 — Boys’ Crucible — density fitting + fault-isolated regression suite¶
🎯 Density fitting (RI) lands across the molecular SCF + post-HF stack: full 2-/3-centre integral kernels, a curated aux-basis library (def2, cc-pV*Z, Pople RIFIT, def2-universal-jkfit), and
density_fit=True+aux_basis=...options on RHF / UHF / RKS / UKS / MP2 / UMP2 energies plus J / K analytic gradients. The regression suite gains per-case subprocess isolation so a C-level crash in any one runner (PySCF FFTDF buffer, MemoryError, ORCA non-zero exit) is captured as a singleerrorrow instead of bricking the whole dispatcher.Codename honours S. F. Boys (1911-1972) whose 1950 introduction of Gaussian-type orbitals (Proc. R. Soc. A 200, 542) is the foundation that makes RI / density-fitting numerically tractable — Gaussian primitives admit closed-form 2- and 3-centre overlap and Coulomb integrals via the Gaussian-product theorem, the exact primitive operations DF rests on. Crucible: every (case, code) pair in the regression suite now runs in its own contained subprocess — fault-isolated, like a chemist’s crucible.
Added — density fitting (v0.7.2 flagship)¶
Core RI kernels — 2-centre and 3-centre Coulomb integrals via libint (ERI2 + ERI3 enabled in the build), exposed as the
DensityFittingobject that holds the ((PQ)|(rs))^{-½} fit and the 3-centre coefficient tensor.Aux-basis library —
python/vibeqc/basis_library/aux/ships the standard def2 family (def2-SVP/JKFIT, def2-TZVP/JKFIT, def2-universal-jkfit, def2-{SVP,TZVP,QZVP}-RIFIT for MP2), Dunning cc-pVxZ-RIFIT and -JKFIT, Pople RIFIT, plus -PP-, -F12-, and core-correlation companions where Basis Set Exchange has them.Energy DF —
density_fit: bool = False+aux_basis: str = ""onRHFOptions,UHFOptions,RKSOptions,UKSOptions,MP2Options,UMP2Options. When True, the SCF Coulomb (J) and exchange (K, where applicable) are built via the RI factorisation; for MP2 the (oo|vv) integrals are built from the same fit. Speedup ~5-10× at def2-SVP, ~30-100× at def2-TZVP and larger.Gradient DF —
compute_gradient/compute_gradient_rksaccept aGradientOptionsstruct that carries the same DF flags through to the gradient pass: 2c + 3c libint deriv=1 kernels + analytic DF-J + DF-K gradient assembly for HF / hybrid DFT. The DF gradient pipeline reuses the same aux fit as the energy run so closed-shell and unrestricted geometries optimise without recomputing the fit.Examples reorganised —
examples/split intomolecular/,periodic/,workflows/; six new DF inputs added covering RHF / RKS / hybrid / MP2 across def2-SVP and def2-TZVP showcasing the speedup vs the direct-ERI baseline.
Added — regression suite fault isolation¶
Per-case subprocess isolation (
examples/regression/core/_isolated_runner.py) — every(case, code)runner call now executes in its ownsubprocess.runinvocation. C-level crashes (PySCF FFTDF buffer segfault, MemoryError, ORCA exit-55, libxc abort) are caught at the parent and surfaced aserrorrows; the dispatcher continues with the next case instead of bricking. Smoke-tested on planetx (Manjaro / Linux 7.0.3) 2026-05-06 with the full 33-case suite running 73 min end-to-end through three real C-level crashes.Running long jobs over sshsection inexamples/regression/README.mddocuments theloginctl enable-linger+systemd-run --user --unit=...pattern. Without it, ssh-spawnednohup/setsidbackground processes inherit the ssh session’s cgroup (/user.slice/user-N.slice/ session-N.scope) and are killed by systemd when the session ends — a silent ~5-minute death we hit hard on planetx during wave-1.6. The fix places the suite in a transient unit under the lingering user manager (.../user@N.service/app.slice/NAME.service) which is independent of any login session.
Open in v0.7.x¶
LiH rocksalt converged absolute energy still off vs PySCF.pbc by +2.386 Ha (regression suite catches it cleanly, run 20260506- 122329-575fa3 confirms bit-exact reproducibility vs run 20260503- 125738-db49e5;
examples/debug/lih_iter_trace.pyis the per-iter dossier for triage).runner_vibeqc.run_molecule_case’s DF short-circuit (added in v0.7.1 as a wave-2 placeholder when vibe-qc had no DF API) still emitsstatus='unavailable'for DF method ids — needs a follow-up patch to actually call the newdensity_fit=True/aux_basis=...options now that they exist. v0.7.3 candidate.Wave 2 of the test suite (multi-k, dual-target dev/release).
OPTBASIS-style condition-number-penalised basis optimiser (deferred to v0.8+).
[v0.7.1] — 2026-05-04 — Pulay’s Triangle — cross-code parity layer¶
🎯 Test-only release: extends the
examples/regression/parity suite from “vibe-qc + PySCF on a handful of periodic Γ cases” to a three-wayvibe-qc + PySCF + ORCAcross-code dossier covering the molecular method matrix vibe-qc actually implements (RHF / UHF / RKS / UKS × {LDA, PBE, BLYP, B3LYP} + MP2 / UMP2), plus DF infrastructure (vibe-qc-side gap documented; PySCF-DF and ORCA-DF cross-validated). Nopython/vibeqc/API changes — anyone not running the regression suite sees no behaviour difference.Codename honours Peter Pulay: invented DIIS (1980) which makes molecular SCF converge tightly enough for µHa cross-code parity; co-developed the Pulay-Vosko RI / density-fitting framework (1990) that the DF layer rests on; and gave us analytical-gradient theory the suite will exercise next. Triangle: the three-way reference geometry vibe-qc / PySCF / ORCA — three independent witnesses triangulate bugs that two could miss.
Added — molecular regression suite expansion¶
5 new molecule specs under
examples/regression/systems/molecules/:ch4,nh3,hf,h2co,benzene(in addition to the existingh2,h2o,ne_atomfrom v0.7.0).2 open-shell specs:
o2(³Σg⁻ diradical, multiplicity=3),o3(¹A1 with biradical character).6 new methods in
examples/regression/methods/catalog.py:rks-blyp,rks-b3lyp,uks-blyp,uks-b3lyp,mp2,ump2.Density-fitting infrastructure:
MethodSpec.df: bool+aux_basis: str; new methodsrhf-df,rks-lda-df,rks-pbe-df,rks-b3lyp-df,mp2-df. PySCF runner enables DF viamf.density_fit(auxbasis=...); ORCA runner injectsRIJK def2/JKinto the simple-input. vibe-qc has no DF API — DF cases emitstatus='unavailable'from the vibe-qc side with a “wave-2 gap” note; PySCF-DF and ORCA-DF rows still cross-validate each other. When vibe-qc DF lands, only one place inrunner_vibeqc.run_molecule_caseneeds to change.Psi4 runner (
examples/regression/core/runner_psi4.py) wires the third independent reference via ASE’s Psi4 wrapper +vibeqc.benchmark.make_psi4_calculator. Built but not threaded into the dispatcher — by design, this release stays at thevibe-qc + PySCF + ORCAtriangle. Threading is a one-line wiring change inrun_one_molecule_case_devwhen needed.MP2 / UMP2 in vibe-qc runner: post-SCF correlation via
vq.run_mp2(mol, basis, rhf_result)andvq.run_ump2(mol, basis, uhf_result); row energy reportsE_totalso cross-code Δ comparisons line up with PySCF (mp2.e_tot) and ORCA (MP2/RI-MP2simple-input).
Changed — cross-code convention alignment¶
ORCA
B3LYP/Ginstead ofB3LYPfor hybrid DFT cases. ORCA’s bareB3LYPuses VWN5; PySCF’sb3lypand vibe-qc’sB3LYPuse VWN3 (the Gaussian convention). Pinning ORCA toB3LYP/Gcollapses the cross-code Δ from ~36 mHa (convention mismatch) to ~10⁻⁴ Ha (the inherent XC-grid noise floor).ORCA
NoFrozenCorefor MP2 to match PySCF + vibe-qc all-electron defaults; otherwise ORCA freezes the 1s on heavy atoms and introduces a ~10⁻⁴ Ha cross-code Δ at sto-3g.PySCF
_PYSCF_XC_MAPgains"blyp" → "b88,lyp".
Added — periodic-SCF debug¶
examples/debug/lih_iter_trace.py— companion to the regression- suite LiH/sto-3g/RKS-LDA failure (the v0.7.0 known-issue: vibe-qc converges but to E_total ~+65 Ha vs the atomic-limit reference of ~−32 Ha). Per-iter trace + energy decomposition + pob-DZVP-rev2 side-by-side, dumped toexamples/debug/lih-iter-trace/.
Smoke-tested¶
Molecular MP2 (
h2,h2o,ch4): vibe-qc / PySCF / ORCA agree at sub-µHa.Molecular B3LYP/G (
h2o,ch4): vibe-qc / PySCF / ORCA agree at ~10⁻⁴ Ha (XC-grid floor).Molecular DF (
h2o / def2-svp / rhf-df): PySCF-DF / ORCA-DF agree at 0.6 µHa; vibe-qc cleanly skipped as designed.All RHF cases (8 molecules): sub-µHa cross-code agreement.
Open in v0.7.x (unchanged from v0.7.0 release notes)¶
LiH converged absolute energy still off vs PySCF.pbc by +2.39 Ha (regression suite catches it cleanly;
lih_iter_trace.pyis the per-iter dossier for triage).Wave 2 of test suite (multi-k, dual-target dev/release).
OPTBASIS-style condition-number-penalised basis optimiser (deferred to v0.8+).
[v0.7.0] — 2026-05-02 — Löwdin’s Compass — periodic SCF correctness + linear-dependence program¶
🎯 **The big v0.7 deliverable: periodic SCF that converges to the right answer in the molecular limit, with a full linear-dependence
screening optimiser stack so the user can build a real solid- state calculation without hand-tuning cutoffs.**
Codename honours Per-Olov Löwdin (1916-2000), whose 1970 canonical-orthogonalisation paper (Adv. Quantum Chem. 5, 185) is the conceptual heart of the linear-dependence work in this release. Compass: navigates the basis-set / cutoff space to find the loosest configuration that keeps the AO overlap matrix positive-semi-definite — without the silent truncation that lets a “converged” SCF give a physically-wrong total energy on tight ionic crystals.
Added — linear-dependence + screening program (v0.7 flagship)¶
CRYSTAL-aligned strategy: refuse to silently truncate near-singular overlap matrices, push the problem to basis-set design or auto-tuned screening. Ten new public APIs across five new modules; eight periodic SCF drivers wired through end-to-end.
vq.scf_preflight_overlap_check(S, plog, ...)— runs at the start of every periodic SCF (RHF/UHF/RKS/UKS × Γ-only/multi-k). Classifies the overlap matrix intook/warn/error/criticalseverity tiers.critical(non-PSDS) raisesLinearDependenceErrorby default with a citation-rich error message recommending pob-TZVP / pob-TZVP-rev2 first,vq.make_basis(..., exp_to_discard=0.1)second, andallow_critical=Trueonly as a deliberate last resort.vq.eigs_preflight(system, basis, kmesh)— CRYSTAL’sEIGSkeyword equivalent. BuildsS(k)at every requested k-point, diagonalises, returns a structured per-k report. No SCF runs. Lets users validate basis suitability at a controlled point in the workflow before committing to expensive SCF cycles.vq.disambiguate_critical_overlap(...)— when a critical-severity preflight fires, this re-runs with tightened cutoffs to distinguish:basis_set_problem: too many diffuse primitives (fix: pob basis set orexp_to_discard);screening_undertight: under-converged exchange screening (fix: tightencutoff_bohr/schwarz_threshold; CRYSTAL’s TOLINTEG ITOL4/ITOL5 distinction, manual p. 130, 398).inconclusive: mixed evidence; recommend the basis-set fix as a strict superset of the screening fix.
Empirical finding on the LiH conventional rocksalt xfail case: the LiH “non-PSD overlap” is NOT a basis-set problem — it diagnoses cleanly as
screening_undertight(tightening cutoff ×2 takes the worst min eigenvalue from -0.16 to +0.07).vq.optimize_truncation(...)+auto_optimize_truncation=Truedefault ON in all 8 SCF drivers — bisectscutoff_bohrandschwarz_thresholdJOINTLY (per the user’s directive) to find the loosest combination that keepsSPSD at every k-point. Sanity-checked ranges (cutoff_max_bohr=80,schwarz_min=1e-18), performance budget (max 8 evaluations, typical: 1–3). Both knobs move proportionally, so neither runs far from default while the other stays put. Short-circuits at 1 evaluation when starting settings are already PSD.vq.make_basis(mol, name, exp_to_discard=0.1)— PySCF-style primitive filter (mirrorspyscf.pbc.gto.Cell.exp_to_discard). Drops every Gaussian primitive with exponent below the threshold, caches the synthesised.g94so libint can re-load it from the C++ SAD initial-guess code path. The bundled pob-TZVP / pob-TZVP-rev2 / pob-DZVP-rev2 ship in-tree and are recommended over filtering whenever possible.Per-primitive drop transparency —
vq.format_basis_filter_report(rep)lists every dropped primitive verbatim (element, shell, l-letter, exponent, libint coefficient). Per the v0.7 transparency directive: “we need to print exactly what we drop into the output file.” Drop lists end with a citation reminder: cite the basis-content change in any publication using these results.PySCF-style
sqrt(diag(S))pre-conditioning in canonical orthogonalisation (_canonical_orthogonalizer(*, normalize_diag_first =True)). The threshold operates on a unit-diagonal matrix instead of the rawS, so it has its intended meaning regardless of per-AO scale (Lykos & Schmeising 1961, Löwdin 1970 conventions). Default ON; legacy raw-Spath available via=False.Opt-in orthogonalisation methods —
vq.orthogonalise_overlap(S, method="auto")with explicitcanonical_orth,pivoted_cholesky_orth(Lehtola 2019 + 2020),symmetric_orthavailable. Auto-dispatch escalates from canonical to Cholesky when cond(S) > 1e15. NOT default in SCF drivers — these are the explicit fallbacks for users who opt past the critical-severity abort.Active-settings dump —
vq.format_options(opts)andvq.dump_active_settings(plog, groups)polymorphically print every pybind-bound options struct, dataclass, or dict as a stablekey = valuesummary. Wired into the SCF banner of one driver as proof-of-concept; remaining drivers in v0.7.x maintenance.
Fixed — periodic SCF gauge consistency (v0.7 flagship, in progress)¶
FFT-Poisson J build now samples AOs with periodic images.
python/vibeqc/ewald_j.py::evaluate_ao_periodicis the new helper;build_j_long_rangeaccepts an optionalsystemkwarg to opt into the periodic AO sum, andbuild_j_ewald_3dalways threads it through. Same plumbing inpython/vibeqc/periodic_density.py::evaluate_periodic_density_on_gridandbuild_j_long_range_periodicfor the multi-k path.Why this matters. The v0.6.x periodic-SCF total energy was not translation-invariant: shifting the molecule from the box origin to the box centre moved H₂/STO-3G/L=30 by +0.587 Ha. The v0.6.1 Madelung correction papered over the symptom in the cubic atomic limit but couldn’t (and didn’t) fix translation invariance because the underlying issue wasn’t the V_ne / J gauge — it was the FFT-Poisson grid sampling AOs aperiodically. An AO Gaussian centred at the box origin loses 7/8 of its spatial extent on a
[0, L)^3grid; periodic-image-summing the AO recovers the full charge density. After the fix:H2 / STO-3G / L=30, Ewald-3D, multi-k Γ-point dispatch: pre-fix: E(centred) - E(origin) = +0.587568 Ha ✗ broken post-fix: E(centred) - E(origin) = +0.000000 Ha ✓ exact
Both placements settle at -1.114878 Ha; PySCF gives -1.117086 Ha. The remaining ~2 mHa is FFT-grid resolution + the v0.6.1 Madelung correction now being slightly miscalibrated for the fixed gauge (retiring the explicit Madelung shift is tracked separately).
LiH dense-ionic regression test (xfail-strict in v0.6.2) is now off by ~600 Ha, down from ~1000 Ha — a major improvement but still not in the physical range. There’s a deeper multi-cell-density bug specific to dense crystals where the Ewald-3D and FFT-Poisson conventions don’t fully reconcile; needs further work before flipping the xfail.
Diagnostic kit (examples/debug/)¶
scf_translation_invariance_check.py— H₂ at origin vs centred, full SCF, total-energy diff.check_v_ne_translation.py— proves V_ne is not the bug.check_j_translation.py— decomposes J = J_SR + J_LR; isolates J_LR as the breakage and shows the periodic-AO fix recovers translation invariance to FFT precision (~5e-6 Ha).probe_v_lr.py— direct probe of ρ-on-grid, showing the 0.62 vs 2.0 integration mismatch that surfaced the bug.prototype_periodic_ao.py— single-purpose proof that ±1 image-cell summing cures the ρ-integration mismatch.PERIODIC_SCF_BUG_ANALYSIS.md— full write-up.
Known issues at time of v0.6.x (now historical — fixed in v0.7.0)¶
🚨 Periodic-SCF absolute energies were over-bound by a Madelung self-image term in v0.6.x. Reproducer:
examples/debug/scf_molecular_limit_check.py. Magnitude predictor:α_M · (Q_n² + Q_e²) / (2L)withα_M = 2.837for simple-cubic. Diagnosis tracked intests/test_periodic_atomic_limit_bug.py(xfail-strict at v0.6.x). Molecular numbers were healthy (match PySCF to machine precision); RELATIVE periodic energies (EOS-scan deltas, cohesive-energy differences at fixed lattice) were also fine — the Madelung shift is geometry-invariant. ABSOLUTE periodic energies and any molecular-limit cross-check are not. Fix landed in v0.7.0 (commit 7a88aaa) via the V_ne dispatch + Madelung-correction reconciliation + FFT-Poisson AO image-summing; this v0.6.3 entry is the historical bug-warning record.
Added — user-facing surfaces for the v0.6.x bug-warning¶
docs/index.md— red danger admonition at the top of the homepage warning that v0.6.x periodic absolute energies are unreliable. Three independent surfaces (homepage admonition, CHANGELOG Known Issues, xfail-strict regression test) all point at the same diagnosis.docs/installation.md— explicit “libomp is not optional” note for macOS. AppleClang ships without OpenMP support;brew install libompgives the headers but CMake’sFindOpenMPneedsOpenMP_ROOTpointed at$(brew --prefix libomp)for the spglib configure to succeed. Auto-detected onmainand onrelease(since cbfd22e); doc gives the manual export for users on older release branches.
Added — sponsorship + citation infrastructure¶
docs/support.md— pitch page covering author bio (PhD on pob-TZVP basis sets at the Mulliken Center, CCM-HF proof of concept), what funding pays for (Claude Max, self-hosted server, future hardware), and a prominent grid linking GitHub Sponsors + Ko-fi.docs/sponsors.md— dedicated opt-in public sponsors listing (currently a placeholder; sponsors who agree to be listed land here). Includes acknowledgements crediting the open-source dependency stack.docs/index.md— prominent green funding admonition at the top of the homepage; sponsor link in the footer.README.md— new “Support the project” section.CONTRIBUTING.md— new row in the “Where to report what” table for the funding decision path.CITATION.cffat repo root — pinned to v0.6.3, picked up by GitLab’s “Cite this repository” sidebar and external citation managers (Zotero, Mendeley). References the pob-TZVP, pob-TZVP-rev2, CCM-HF (DOI: 10.1002/jcc.23550), libint, and libxc citations.docs/citing.md— citation guide walking users through what to cite when, with APA + BibTeX entries for the software itself and per-feature references (basis sets, DFT, dispersion, symmetry).
Added — daily-workflow zsh helpers¶
docs/updating.md—vibe-up/vibe-downaliases for activating/deactivating the venv, and avibe-updatezsh function that pulls + reinstalls dev / release / experimental trees in one shot. The experimental tree is opt-in (silently skipped if not configured) — typical use is keeping a third checkout tracking an in-flight feature branch (e.g.feature/v0.7-pyscf-pbc-parity) current alongside dev/release.
Internal — CI plumbing¶
.gitlab-ci.yml— historicalbritish-english-checkjob (script-based British → US prose audit) is now formally documented as removed via an inline comment. The job had been blocking docs deploys on otherwise-fine release commits; the two scripts (scripts/british_to_us.py,scripts/british_to_us_code.py) were deleted. Run them by hand if you care..gitlab-ci.yml— addedsphinx-design>=0.6to the docs-build pip install so thegrid-item-carddirectives on the new support page render correctly. Also unlocks dropdowns / tabs / badges for future docs work.
[v0.6.3] — 2026-05-02 — Pulay’s Owl (patch) — docs hotfix: bug-warning + sponsorship + citation infra¶
Pure-docs patch in the v0.6 series. Patch releases inherit their parent minor’s codename. No code changes — only documentation, CI, and project-meta updates (the v0.6.x periodic-SCF bug warning, the GitHub Sponsors / Ko-fi pitch, the CITATION.cff + docs/citing.md) cherry-picked back onto release while engineering work on the periodic-SCF gauge fix landed on
feature/v0.7-pyscf-pbc-parity. v0.7.0 supersedes the bug warning (the gauge bug is fixed) but keeps the sponsorship and citation infrastructure.
[v0.6.2] — 2026-05-01 — Pulay’s Owl (patch) — divergence detection + v0.7-flagship promotion¶
Patch release in the v0.6 series. Patch releases inherit their parent minor’s codename. Two landings: an SCF-safety improvement that ports the v0.5.6 inline divergence-check across all 8 periodic SCF entry points, and a roadmap promotion that elevates “periodic SCF correctness + PySCF.pbc parity” to the v0.7 flagship after diagnosis showed v0.6.1’s Madelung-fix only covers the atomic limit, not dense ionic crystals.
Added — SCF divergence detection on every periodic SCF entry point¶
v0.5.6 added inline divergence-detection to
run_rks_periodic_gamma_ewald3d only. v0.6.2 lifts the heuristic
into a shared helper (vibeqc.scf_divergence.check_scf_divergence)
and applies it uniformly across all 8 periodic SCF entry points
(RHF / UHF / RKS / UKS, Γ-only + multi-k Ewald). Each driver bails
with a remediation hint when:
Eorgradbecomes NaN / Inf — arithmetic blowup.|E| > 1e6 Ha— unphysical magnitude, density is broken.iter ≥ 5 and |dE| > 1e4 Ha— DIIS / damping not recovering.
The shared REMEDIATION_HINT points at SAD initial guess +
stronger damping + tighter Ewald — the three levers that move the
needle on Hcore-bombing systems. Identical hint across all drivers
so users see consistent advice regardless of which entry point
they hit.
Added — dense-ionic-crystal absolute-energy regression test¶
tests/test_periodic_dense_ionic_bug.py — a single xfail-strict
test pinning the LiH conventional cell SCF total energy in the
physically sensible range (-50 < E < 0 Ha). Currently fails (E ≈
-1060 Ha; off by factor 30+) because the v0.6.1 Madelung-fix
formula is correct only for atomic-limit / single-charge cases,
not for dense ionic crystals where the actual periodic-image
structure is much richer than α_M·(Q_n²+Q_e²)/(2L) captures.
Goes red the moment the v0.7 fix lands.
Roadmap — v0.7.0 flagship rewritten¶
Was: “periodic stress tensor + slab builder”. Now: “periodic SCF correctness + PySCF.pbc parity”.
Three-phase v0.7 plan in docs/roadmap.md:
Phase 1 (PBC1a-d): port PySCF.pbc.df.fft.FFTDF gauge convention. Drop the G=0 pinning of J alone in favour of the combined-system neutralizing background. Replace the v0.6.1-shipped formula-based
madelung_energy_correctionwith the cell-dependent Madelung frompyscf.pbc.tools.pbc.madelung-equivalent.Phase 2 (PBC2a-d): PySCF parity test set. Easy systems first (high-symmetry gapped insulators that PySCF converges trivially with default settings — no SAD, no level shift, no aggressive damping). Test fixtures: H₂ in big box, solid Ne FCC, LiH / NaCl / MgO rocksalt, diamond C, Si diamond, BN cubic — all 8-atom conventional cells, laptop-friendly, ≤ 60 s per fixture combined PySCF + vibe-qc. Sourced from PySCF.pbc’s own tutorial examples. STO-3G first; extend to pob-TZVP after STO-3G parity holds.
Phase 3 (PBC3a-d): CRYSTAL-style speedups on top of PySCF parity. Symmetry-driven D update; shell-pair pruning by f_n equivalence-class count; TOLINTEG-style 5-vector thresholds; symmetry-equivalent k-point folding.
Stress + slab builder moves to v0.8.
CI¶
The historical British-prose audit scripts
(scripts/british_to_us.py, scripts/british_to_us_code.py)
were removed entirely. The blocking british-english-check CI
job had already been removed in v0.6.0; the audit scripts had been
retained for “run by hand” use ever since but no caller actually
did. CI yaml comment cleaned up.
Compatibility¶
All defaults unchanged. No API breaks. The new divergence-check is
additive — it bails on broken SCFs that would have run to
max_iter and crashed downstream anyway, with a useful error
instead.
Deferred to v0.7¶
Default-switch periodic
initial_guessHCORE → SAD (broke ~4 tests calibrated to Hcore iteration counts; right time to flip is alongside the v0.7 PySCF-parity test refresh).Dynamic damping (Zerner–Hehenberger), SAP guess, CRYSTAL-style manual atomic occupations, G1a-2 root-cause K-piece fix — all queued behind the v0.7 PySCF-parity flagship per the user-driven prioritisation: “fix periodic SCF correctness first; add features only when PySCF needs them too”.
[v0.6.1] — 2026-05-01 — Pulay’s Owl (patch) — periodic SCF Madelung-leak fix + SAD wiring¶
🎯 The fix that makes v0.6 periodic SCF give physically correct energies in the molecular limit. Patch releases inherit their parent minor’s codename.
Fixed — periodic SCF Madelung-leak in absolute energies¶
A critical correctness gap in the v0.6.0 Ewald-3D SCF: periodic
total energies were systematically over-bound in the molecular
limit by α_M · (Q_n² + Q_e²) / (2L) (or α_M Q_e²/(2L) for
the default DIRECT_TRUNCATED nuclear path). For He in a 30-bohr
box: pre-fix −3.183 Ha vs molecular −2.808 Ha (off by 375 mHa
in the regime where periodic should be exact match to molecular).
Root cause: the Ewald-3D Hartree J build pins the G=0 Fourier
mode of the electronic potential to zero, leaking
-α_M·Q_e²/(2L) into the SCF total via the ½ tr(D·J) term.
When nuclear repulsion uses Ewald it carries a matching
-α_M·Q_n²/(2L) Madelung self-image. Both leak terms have the
same sign for charge-balanced systems and ADD instead of
cancelling. The Madelung-cancellation infrastructure
(vibeqc.madelung) was already present as helpers but the SCF
drivers never applied it (“the caller’s responsibility”). v0.6.1
flips that to a built-in correction.
Fix: vibeqc.madelung.madelung_energy_correction(D, S, system, nuclear_uses_ewald=…) returns the correction term to add to the
SCF total energy. The convenience wrapper
madelung_energy_correction_for_lat(D, S, system, lat_opts)
infers the convention from lat_opts.coulomb_method. Wired into
all 8 Python Ewald-3D SCF drivers (RHF / UHF / RKS / UKS, Γ-only +
multi-k); each driver applies the correction at every SCF
iteration and at the converged-energy step.
Validation (atomic-limit regression tests):
System / box |
Pre-fix diff vs molecular |
Post-fix diff |
|---|---|---|
He atom, L=30 bohr |
−375 mHa |
+3 mHa |
H₂ molecule, L=30 bohr |
−376 mHa |
+2 mHa |
H₂ molecule, L=100 bohr |
−110 mHa |
+5×10⁻⁵ Ha |
The remaining ~3 mHa residual at L=30 is the finite-density-extent vs point-charge gap in the Madelung formula — real physics, not a bug. Decays as 1/L³ rather than the bug’s 1/L scaling.
Added — SAD initial guess wired into Python periodic SCF drivers¶
The molecular SCF stack already defaults to SAD (Superposition of Atomic Densities) since pre-v0.5; the periodic Python Ewald drivers hardcoded the Hcore initial guess. v0.6.0 surfaced this when a user reported NaCl/STO-3G/Γ RKS-LDA bombing with energies oscillating between +33 716 and −16 268 Ha — the Hcore guess for ionic insulators with deep core states (Na 1s ε ≈ −40 Ha, Cl 1s ε ≈ −100 Ha) is too far from physical and DIIS amplifies the swing into nonsense before recovering.
vibeqc.sad_density(molecule, basis)newly exported at the top level. Wraps the existing C++vibeqc::sad_density— isolated-atom RHF SCF with fractional Aufbau occupations per unique element, block-diagonal assembly into the molecular AO basis. Cached per (Z, basis_name) for the lifetime of the call.All 4 of
run_rhf_periodic_gamma_ewald3d,run_rks_periodic_gamma_ewald3d,run_rhf_periodic_multi_k_ewald3d,run_rks_periodic_multi_k_ewald3d,run_uks_periodic_multi_k_ewald3dhonouropts.initial_guess = InitialGuess.SAD. Multi-k drivers seed the per-k MOs from Hcore (for trace continuity) then overwriteD_realwith SAD on the unit cell at g=0 (zero elsewhere — molecular-limit convention). Each driver emits aplog.info("initial guess: SAD")line so the .out file shows which guess engaged.
Compatibility¶
Both the Madelung fix and the SAD wiring are additive and default-compatible:
The Madelung correction is always applied for the Ewald-3D drivers; pre-v0.6.1 absolute energies had the bug and should not be reproduced. The relative-energy structure (k-weighted SCF consistency, ω-invariance, multi-k = Γ at [1,1,1]) is preserved.
The default
initial_guessfor periodic options remainsHCORE—SADis opt-in viaopts.initial_guess = InitialGuess.SAD. Default-switch is a v0.6.2 follow-up so existing scripts get bit-for-bit identical iteration counts on this release.
Deferred to v0.6.2¶
The remaining items from the SCF-guess + convergence multi-release
programme (docs/roadmap.md) didn’t make this release and are
queued for the next patch:
Switch periodic default
initial_guessfromHCOREtoSAD.Saunders–Hillier level shift wired across RHF/UHF/RKS/UKS molecular + periodic.
Dynamic damping (Zerner–Hehenberger).
SCF divergence detection on every periodic SCF entry point (currently in
run_rks_periodic_gamma_ewald3donly).SAP guess (Lehtola erfc fits) as a sibling to SAD.
CRYSTAL-style manual atomic occupations API.
G1a-2 root-cause fix for the periodic K-piece gradient (currently masked by Sx2 default screening at 1e-14).
Recalibrate the LiH/MgO/Ne crystal absolute-energy benchmarks against CRYSTAL / PySCF.pbc references (skipped in v0.6.1 since the previous bounds were calibrated to pre-fix wrong values).
[v0.6.0] — 2026-04-30 — Pulay’s Owl — periodic atomic gradients¶
🎯 The big v0.6 deliverable: analytic forces on solids. vibe-qc can now do ASE-driven geometry optimisation on molecular crystals, defect cells, and surfaces; gates v0.7 stress + v0.8 phonons. Codename honours Pulay (whose 1969 correction terms are the conceptual core of this release, and whose 1980 DIIS underpins the SCF stack the gradients build on); owl for the nocturnal precision needed to extract sub-meV forces from a periodic SCF.
Added — periodic atomic gradients (G1 series)¶
G1a — Γ-only RHF gradient (
compute_gradient_periodic_rhf_gamma). Hellmann-Feynman electronic + nuclear contributions plus the full Pulay correction through the lattice-summed ∂χ/∂R one- and two-electron derivative integrals. Five new C++ primitives (the*_lattice_gradient_contributionfamily) operate on aLatticeMatrixSetdensity. Returns(n_atoms, 3)Ha/bohr.G1b — Γ-only RKS gradient (
compute_gradient_periodic_rks_gamma). Same HF skeleton plus an inline_xc_pulay_molecular_fallbackfor the XC Pulay term on the unit-cell grid. LDA exact; GGA σ-coupled term tracked as v0.6.x.G1c — multi-k drivers (
compute_gradient_periodic_rhf_multi_k,compute_gradient_periodic_rks_multi_k). Density is Bloch-folded across the IBZ-reduced k-mesh into aLatticeMatrixSetand fed to the same C++ primitives.G1d — open-shell UKS multi-k gradient (
compute_gradient_periodic_uks_multi_k). Pure-DFT only (α_HF = 0); raisesNotImplementedErrorfor hybrid UKS until the per-spin periodic K primitive lands at v0.6.x.G1e — ASE bridge (
vibeqc.ase_periodic):from vibeqc.ase_periodic import ( atoms_to_periodic_system, periodic_forces, ) forces_eV_per_A = periodic_forces(atoms, basis, kpts=[2,2,2], functional="lda")
Round-trips Å → bohr → Å + Ha/bohr → eV/Å with Newton’s-3rd-law obeyed.
Added — sibling features shipped with v0.6.0¶
Sx2 — Cauchy-Schwarz screening on the gradient ERI pass (
eri_lattice_gradient_contribution).LatticeSumOptions.schwarz_threshold_forces(default1e-14, 100× tighter thanschwarz_thresholdbecause plain Schwarz on derivatives is non-rigorous, matching CP2K’sEPS_SCHWARZ_FORCESconvention). Per-quartet + cell-level skip on the same shape as the energy-side. Side-effect: the historical G1a-2 K-piece routing bug (~5e-3 Ha/bohr disagreement vs FD on H-chain a = 2 Å periodic) is masked by default screening — analytic gradient now matches FD to ~1e-7. Settingschwarz_threshold_forces = 0.0reproduces the historical disagreement and is the regression handle for the actual root-cause fix tracked at G1a-2.Banner print at SCF entry —
run_rhf_periodic_scf,run_rks_periodic_scf,run_rks_periodic_gamma_scfemitvibe-qc <version> <codename>before the SCF header so users can verify which build is running just from the.outfile.G1d export —
compute_gradient_periodic_uks_multi_kis now reachable from the top-levelvibeqc.*namespace.
Roadmap¶
docs/roadmap.md gains a comprehensive SCF guess + convergence
program (multi-release) section laying out the v0.6.x → v0.10.x
work spanning initial guess (CORE / GWH / SAD / SAP / Hückel /
MINAO / FRAGMO / READ / broken-symmetry), SCF convergence
acceleration (damping / level-shift / DIIS family / EDIIS / ADIIS /
SOSCF / AH / TRAH / OT), and density mixing for periodic metals
(Anderson / Broyden / Kerker / Pulay-Kerker / Periodic Pulay).
All wired uniformly across RHF / UHF / RKS / UKS, molecular +
periodic. CRYSTAL-style manual atomic occupations exposed as an
opt-in override modelled on CRYSTAL23’s EIGSHIFT / ATOMSPIN
conventions. 23 citations spanning Roothaan 1951 → Helmich-Paris
2022.
Known limitations + deferred work¶
G1a-2 K-piece bug still tracked. Masked by default Sx2 screening; HF / hybrid-DFT periodic users with non-default thresholds should use the FD reference until the root-cause fix.
G1b-2 GGA σ-coupled XC Pulay: Γ-only RKS gradient currently uses the molecular-limit XC Pulay (LDA exact, GGA approximate). σ-coupled term lands at v0.6.1.
G1d hybrid-UKS: per-spin periodic K primitive is v0.6.1.
G1f CRYSTAL parity tests on LiH / MgO / diamond Si — the G1 internal consistency tests pass; CRYSTAL ground-truth tag is v0.6.x.
NaCl / MgO Hcore-guess SCF instability — the Hcore initial guess is too far from physical for ionic insulators with deep cores. v0.5.6’s divergence detection now exits cleanly with a remediation hint; the actual fix is the v0.6.x SAD / SAP guess flagship.
[v0.5.5] — 2026-04-30 — Wilson’s Otter (patch) — Cauchy-Schwarz screening¶
Performance hotfix in the v0.5 series. Patch releases inherit their
parent minor’s codename. Closes a 100–1000× wall-clock gap on real
crystals: pre-v0.5.5 the periodic two-electron Fock build had no
integral screening at all, so periodic SCFs that should finish in
seconds (LiH / STO-3G / 4×4×4 IBZ multi-k Ewald) instead took hours.
Diagnosed live via py-spy dump --pid <PID> on a hung run on a
16-core box, where every worker thread was sitting deep inside
libint2::Engine::compute2 with the master in build_fock_2e_real_space.
Added — LatticeSumOptions.schwarz_threshold¶
Standard Cauchy–Schwarz integral bound applied per shell quartet × cell triple in both periodic 2-e Fock builders:
| ⟨μ_0 ν_g | λ_λ σ_σ⟩ | ≤ Q[c_g][μ,ν] · Q[c_σ−c_λ][λ,σ]
with Q[c][s_a, s_b] = √max | (s_a_0 s_b_c | s_a_0 s_b_c) | precomputed
once at the top of every Fock build. Quartets bounded below
schwarz_threshold (default 1e-12 Ha, 0.0 to disable) skip the
libint quartet evaluation entirely. Translational invariance reduces
the Q tensor to a function of one relative lattice vector; Gaussian
shell-pair products decay exponentially with |R|, so the lattice-sum
truncation becomes rigorous instead of cutoff-driven. Wired into
both build_fock_2e_real_space (the full real-space ERI path used
by the multi-k Ewald-split SCF) and build_jk_gamma_molecular_limit
(the Γ-only periodic J/K builder); both share a
compute_schwarz_factors_per_cell helper that uses the same engine
prototype the main pass uses, so the kernel — Coulomb or
erfc-Coulomb for the Ewald short-range J — matches.
References: Häser & Ahlrichs 1989; CP2K’s EPS_SCHWARZ (1e-7
energies, tighter for forces); CRYSTAL’s TOLINTEG 5-vector.
Tests¶
tests/test_periodic_schwarz_screening.py— three pinned contracts:API surface —
LatticeSumOptions.schwarz_thresholdis a writable float, default1e-12.Performance regression — H₂ chain / STO-3G / 2×2×2 multi-k Ewald RHF SCF completes in well under 120 s (in practice ~3 s on a developer box).
Correctness — screening on (
1e-12) and off (0.0) give the same SCF energy to within the convergence floor (1e-5for this fixture), confirming the threshold is well inside production-quality SCF accuracy.
Tooling¶
pyproject.tomlgains[diagnostics]and[dev]extras, both carryingpy-spy>=0.4.pip install -e '.[dev]'brings in the full developer setup — tests + dispersion + ASE + py-spy. py-spy was the diagnostic that pinned this regression in seconds; making it install-time-available means the next user who hits a hung SCF has it ready without remembering to pip-install separately.
Roadmap¶
docs/roadmap.md gains a new “Acceleration program (multi-release)”
section laying out the path from v0.5.5 forward: gradient-pass
screening + separate forces threshold (v0.6.x), RI / RIJCOSX
(v0.7.x), fast multipole methods (v0.8.x), ADMM + OT-SCF for hybrid
DFT on big cells (v0.9.x), GAPW / PAW / ACE (v0.10.x), GPU + tensor
decomposition (v1.x). Schwarz screening is the foundation;
everything else stacks multiplicatively on top.
Compatibility¶
Existing scripts run unchanged — the new threshold defaults to
1e-12 Ha, well inside production SCF accuracy. Set
opts.lattice_opts.schwarz_threshold = 0.0 to recover legacy
unscreened behaviour.
[v0.5.4] — 2026-04-30 — Wilson’s Otter (patch) — convergence plot + run fingerprint¶
Closes the v0.5.x observability track. v0.5.1 gave us live SCF
logging; v0.5.2 the post-mortem perf log; v0.5.3 a verbosity dial
plus stdlib logging integration; v0.5.4 adds the two small
visual / identity helpers that pair with all three: a one-call
convergence-curve PNG and a deterministic identity hash that
stamps every job. Patch releases inherit their parent minor’s
codename.
Added — vibeqc.plot_convergence¶
One-call SCF convergence plot.
vq.plot_convergence(result, save="conv.png")turns any result with anscf_traceinto a publication-grade PNG:|ΔE|and‖[F,DS]‖on log-y twin axes (blue / orange), a green ✓ or red ✗ status marker over the last point, and an optional DIIS subspace-dim trace on a third offset axis. Defaultshow_diis=Truematches what most users want when scanning a stuck SCF; passshow_diis=Falsefor slide-deck cleanliness. PySCF’sscf.diis_plotequivalent for vibe-qc.save=None(the default) returns thematplotlib.figure.Figureso callers can compose the plot into a larger panel or override the title before saving.Empty-trace edge case (zero iterations — e.g. an aborted run) draws a “no data” annotation rather than crashing on
min()of an empty sequence — the kind of polish that matters for batch-script error reports.
Added — vibeqc.run_fingerprint¶
Deterministic SHA-256 identity hash.
vq.run_fingerprint(mol, basis="sto-3g", method="rhf")returns a 16-hex-character lowercase hash of the canonicalised inputs (geometry rounded to1e-10bohr, basis / method / functional lowercased, options sorted by key). Same chemistry → same hash on any machine, in any Python version. Use as a cache key, a bug-report identifier, or the answer to “is this the .out on disk the same calculation as the one I’m about to launch?” without diffing files.Wired into
run_job. The.systemmanifest now carries[run].fingerprint = "abc1234..."alongside the existingtimestamp_iso/wall_seconds/basenamefields. The field is omitted on manifests written by v0.5.1–v0.5.3 — readers should treat it as optional.
Coordination with v0.5.1 / v0.5.2 / v0.5.3¶
The two new helpers slot into the existing observability surface without changing any existing contracts:
File naming:
output.out(text log),output.system(manifest, now with[run].fingerprint),output.perf(opt-in perf log). Same basename, different extension.TOML manifest shape is back-compat: existing readers ignore unknown keys gracefully, so old parsers see
[run].fingerprintas a no-op rather than a parse error.No new env vars —
VIBEQC_LIVE_LOGGING,VIBEQC_NO_HOSTNAME,VIBEQC_PERFLOG,VIBEQC_VERBOSEcover the existing observability levers; the fingerprint is an output, not a knob.
Public API additions¶
vibeqc.plot_convergence(result, *, save=None, figsize=(8, 4.5), show_diis=True, title=None, molecule=None, basis=None) -> Figurevibeqc.run_fingerprint(molecule, *, basis, method, functional=None, charge=None, multiplicity=None, options=None) -> strvibeqc.write_system_manifest(..., fingerprint=None)— new optional kwarg.
[v0.5.3] — 2026-04-30 — Wilson’s Otter (patch) — verbosity + logging¶
Third instalment in the v0.5.x observability track. Pairs
with v0.5.1 (live SCF) and v0.5.2 (perf log) by giving callers
a single integer dial controlling how much detail the live
emit carries, plus a stdlib logging adapter so vibe-qc fits
into existing log-routing stacks (rotating files, syslog,
dictConfig). Patch releases inherit their parent minor’s
codename.
Added — verbosity levels¶
verbose: intknob onrun_job— PySCF-convention integer levels 0..9, default 4. Each level is a strict superset of the one below, so bumping it only adds output:level
what is emitted
0
silent — nothing live (.out is still written)
1
banner + warnings + final SCF status
2
add per-stage milestones +
info()lines3
add per-stage timing on stage exit
4
default — add per-iteration SCF rows
5
add inline RSS-memory snapshots
6+
phase-level wall-clock breakdown live
Threaded through every periodic SCF entry point (
run_rhf_periodic_gamma_ewald3d,run_rks_periodic_multi_k_ewald3d, the U-variants, …) so direct callers of the lower-level drivers can dial verbosity without going throughrun_job.VIBEQC_VERBOSE=Nenv var — sets the default level for batch scripts that don’t want to edit every input file. Same precedence convention asVIBEQC_LIVE_LOGGINGandVIBEQC_PERFLOG: explicitverbose=always wins, and a garbage env value silently falls back to the package default rather than raising.ProgressLogger.level— the integer level the logger was constructed with, exposed as a read-only property.verbose=True/verbose=Falseare still accepted on the constructor (back-compat with the v0.5.1 / v0.5.2 boolean API) and resolve to 4 / 0 respectively.New
ProgressLogger.memory(label, rss_mib)andProgressLogger.debug(message)helpers — gated to levels 5 and 6 respectively, parallel to the perf log’s memory snapshots and phase breakdown.
Added — stdlib logging integration¶
use_logging: boolknob onrun_jobandProgressLogger— routes every emit throughlogging.getLogger("vibeqc.run_job")instead of baresys.stdoutwrites. Mapping:banner(),info(),stage(),converged(),memory()→INFOwarn()→WARNINGiteration(),debug()→DEBUG
Composes naturally with
logging.handlers.RotatingFileHandler,SysLogHandler, andlogging.config.dictConfig— no special vibe-qc-side integration required:import logging logging.basicConfig(level=logging.INFO) vq.run_job(mol, basis="6-31g*", method="rhf", output="x", use_logging=True)
The verbose-level gate runs before the logging call, so
verbose=2+use_logging=Truedoes not emit per-iterDEBUGrecords even if the active handler is set toDEBUG.progress=Falsestill wins as a hard kill switch.The library has carried a
NullHandleron thevibeqctop-level logger since v0.5.0 (standard practice); v0.5.3 actually flows production progress through it.
Coordination with v0.5.1 / v0.5.2¶
The three observability axes share a single design family:
File naming:
output.out,output.system,output.perf— same basename, different extension, ORCA/Gaussian convention.Env-var prefix:
VIBEQC_LIVE_LOGGING=0(v0.5.1 opt-out),VIBEQC_NO_HOSTNAME=1(v0.5.1 manifest privacy),VIBEQC_PERFLOG=path(v0.5.2 perf opt-in),VIBEQC_VERBOSE=N(v0.5.3 level). One family, one spelling convention.All three can coexist in a single run::
VIBEQC_PERFLOG=run.perf VIBEQC_VERBOSE=2 \ python my-calc.py > run.log 2>&1
Limitations / followups¶
The dispatch wrappers (
run_rhf_periodic_scf,run_rhf_periodic_gamma_scf,run_rks_periodic_*_scf) do not yet accept theverbose=kwarg directly — callers who want a non-default level through the dispatch have to construct aProgressLogger(verbose=N)and pass it asprogress=. Adds a v0.5.3.x patch when the dispatch surface stabilises.
Added — basis-set name in periodic live log (follow-up)¶
Each periodic SCF entry point (the eight
python/vibeqc/periodic_*_ewald*.pydrivers) now emits abasis: <name> (<nbasis> BFs / <nshells> shells)line in the setup phase, alongside the existingomega = ..., FFT grid ...info line. Closes the parity gap with the molecular-siderun_jobbanner (which has carriedJob: RHF / basis=6-31g*since v0.5.0). Surfaces at verbose >= 2 — same gate as every otherinfo()line.At verbose >= 5 (“very verbose”) the entry point also dumps the per-shell exponents and contraction coefficients via the new public helper
vibeqc.format_basis_summary(basis)— same data PySCF prints when its own basis log is enabled. Useful for confirming which exponents got loaded when sweeping a basis-set choice on a long-running bulk run.vibeqc.format_basis_summaryis exported from the package top-level alongsideformat_scf_traceandlog_scf_trace.
[v0.5.2] — 2026-04-30 — Wilson’s Otter (patch) — perf log¶
Second instalment in the v0.5.x observability track. Pairs with v0.5.1’s live SCF logging: live shows progress during a run, the perf log shows where the time went afterwards. The two pair — “is the SCF stuck?” (live) versus “why is my LiH / pob-TZVP run taking 20 minutes?” (perf). Patch releases inherit their parent minor’s codename.
Added — performance / debug log (M3)¶
vibeqc.perf_log— new context manager that activates a fresh :class:vibeqc.PerfTrackerfor the duration of the block and writes a post-mortem text report on exit. Three ways to enable, all of which feed the same accumulator:VIBEQC_PERFLOG=output.perf python my-calc.py(env var)with vq.perf_log("output.perf"): ...(context manager)vq.run_job(..., perf_log=True)writes{output}.perfnext to.out/.molden/.system
Off by default — preserves pre-v0.5.2 behaviour for callers that don’t opt in. Explicit
perf_log=always wins over the env var.vibeqc.PerfScope— RAII timer that pushes wall + CPU deltas into the active tracker. No-op when no tracker is active, so call sites can sprinklewith PerfScope("phase"): ...unconditionally — release builds with the feature off pay oneContextVar.get()and onetime.perf_counter()call per scope. Wired intorun_job(run_job.total,geometry_optimization,basis_set_construction,scf.{rhf,uhf,rks,uks},write_molden) and into the Python-driven periodic SCF integrals (periodic.integrals_latticeper-S/T/V sub-scopes).
Report sections: phase summary sorted by wall-time descending (with parallelism = CPU / (wall × threads) per phase); auto-flag block listing phases that consumed > 5% of wall AND ran at parallelism < 0.7× available threads (the under-parallelised hot paths users care about); RSS memory snapshots at SCF transitions (
start_of_scf,end_of_scf); per-iteration SCF table (energy, ΔE, ‖[F,DS]‖, DIIS, wall since SCF start). Plain ASCII, sortable by eye, parseable byawk/grep.Public API:
vibeqc.PerfTracker,vibeqc.PerfScope,vibeqc.perf_log,vibeqc.active_tracker,vibeqc.format_perf_report— full surface indocs/user_guide/output_files.md§ “Performance debugging”.
Coordination with v0.5.1¶
The two observability axes share a single design family:
File naming:
output.out(live SCF log, always),output.system(manifest, always),output.perf(perf log, opt-in). Same basename, different extension — matches the ORCA / Gaussian convention and meansrm output.*cleans a job cleanly.Env-var prefix:
VIBEQC_LIVE_LOGGING=0(v0.5.1 opt-out),VIBEQC_NO_HOSTNAME=1(v0.5.1 manifest privacy),VIBEQC_PERFLOG=path(v0.5.2 perf opt-in). One family, one spelling convention.Both can coexist in a single run::
VIBEQC_PERFLOG=run.perf python my-calc.py > run.log 2>&1
Limitations / followups¶
C++ kernel-level scopes (
compute_eri,build_coulomb,build_exchange,xc_eval,diag_k,s_inverse_sqrt_complex,bloch_sum) are not yet instrumented — those scopes live in C++ and need a compile-time#ifdef VIBEQC_PERFLOGhook to keep release builds zero-cost. They arrive in a v0.5.2.x patch.Cell-list / grid statistics (n_cells in real-space lattice sums, Schwarz-screening efficiency, cell-pair distance histogram) are also deferred to v0.5.2.x — they require deeper inspection of internal state than the current Python-side instrumentation exposes.
[v0.5.1] — 2026-04-30 — Wilson’s Otter (patch) — observability¶
Patch release of the v0.5 series, the first instalment in the v0.5.x observability track on the roadmap. Headline feature: live SCF logging — multi-minute runs no longer go silent until the SCF returns. Also lands the polarised-GGA fxc kernel completion (17e-2 — analytic UKS Hessian for hybrid GGAs) and a runtime- provenance manifest for bundled reference outputs. Patch releases inherit their parent minor’s codename.
Added — live SCF logging (headline)¶
vibeqc.ProgressLogger— new public class wired into every periodic SCF entry point (run_rhf_periodic_scf,run_rks_periodic_scf, the EWALD_3D variants, …) and intorun_job. Defeats output buffering on long-running jobs: every write is flushed, so the canonicalnohup python my_calc.py > job.log 2>&1 &+tail -f job.logpattern shows progress unfold in real time. Per-iteration SCF progress is live for every Python-driven EWALD_3D path (the multi-k bulk runs that motivated the feature); molecular SCFs still run in C++ so they emit a banner + post-hoc summary live, with the per-iteration trace landing in the.outwhen the SCF returns.run_jobdefaults to live progress on. Solves the “is my background job stuck or actually running?” question without users having to learn a new kwarg. The.outfile is also line-buffered sotail -f output-*.outworks without any extra setup. Passprogress=Falseto silence stdout, or aProgressLoggerinstance for full routing control. The env varVIBEQC_LIVE_LOGGING=0opts out globally for batch scripts; explicitprogress=kwargs always win over the env var.
Added — runtime manifest¶
run_job(output="x")now writes a third sibling filex.systemnext tox.outandx.molden: a plain-TOML manifest pinning the runtime environment that produced the log. Captures vibe-qc version + git SHA, host OS + CPU model + physical/logical core count, OpenMP thread count actually used, total/available RAM, Python interpreter, and linked-library versions (libint / libxc / spglib / libecpint). Bundled reference outputs now carry this manifest so wall-time numbers indocs/_static/examples/<slug>/<slug>.outare interpretable (“theSCF total: 0.015 sfigure was on an Apple M2 Pro at 12 OMP threads”) and reproducible across machines.vibeqc.system_info()andvibeqc.write_system_manifest()exposed as public API so users driving the SCF drivers directly (withoutrun_job) can attach the same provenance to their own outputs. See Tutorial 29 for thegrep/diffrecipes that turn the manifest into a hardware-aware comparison against the bundled reference.Privacy: hostname opt-out.
record_hostname=Falseonrun_job(orVIBEQC_NO_HOSTNAME=1in the environment) emitshostname = "<redacted>"; every other field stays full-fidelity. Engineering’s bundled-docs regen (scripts/regenerate_doc_examples.py) sets the env var globally so machine names don’t leak intodocs/_static/examples/.
Added — Phase 17e-2 (polarised-GGA fxc kernel)¶
Analytic UKS Hessian path now covers hybrid-GGA functionals (B3LYP, PBE0, …) end-to-end — closes the v0.5.0 caveat that hybrid-GGA UKS Hessians fell back to the FD path.
Docs¶
New section in
docs/user_guide/output_files.mdcovering the.systemmanifest format, parsing recipe, and hostname opt-out.New Tutorial 29: “Reproducing a reference output” — walks through comparing your run’s
.systemagainst the bundled one for the same example.
[v0.5.0] — 2026-04-30 — Wilson’s Otter¶
Codename: Wilson’s Otter. First release under the Scientist’s-Animal codename policy. E. Bright Wilson — FG-matrix vibrations pioneer, co-author of Molecular Vibrations (1955) — anchors the milestone’s primary flagship feature: molecular Hessian → vibrational frequencies → thermochemistry.
Headlines¶
🎯 Molecular analytic Hessian + IR + thermochemistry (Phase 17). Turns vibe-qc from “computes energies” into “does chemistry”: analytic Hessians for RHF/UHF/RKS (Phase 17a–d), open-shell-DFT analytic for pure functionals via 17e, IR intensities via dipole- derivative tensors, full vibrational thermochemistry (ZPE, U, H, S, G at any T, p). Validated against PySCF analytic Hessians to <1e-7 Ha/bohr² for HF; the KS branch matches to ~1e-3 Ha/bohr² limited by XC-grid sensitivity (vibe-qc’s 75 × 17×36 Gauss-Legendre grid vs PySCF’s 75 × 302 Lebedev), with frequencies still agreeing with ORCA to ~1 cm⁻¹.
🎯 Full k-point sampling support (K-Phase). Public
vibeqc.KPoints builder with seven construction modes —
Monkhorst-Pack, Γ-centred, custom-shifted, Γ-only, IBZ-reduced via
spglib, HPKOT band paths via seekpath, explicit user lists, plus
density-based auto-mesh (KPPRA / kspacing / VASP Auto). Closes
the “manual k-list construction” gap versus CRYSTAL’s SHRINK
and VASP’s KPOINTS. Hex/trigonal cells refuse non-zero MP shifts
with an actionable error pointing at gamma_centred.
Added — Phase 17 (molecular Hessian)¶
17a-1 finite-difference Hessian + harmonic frequencies (Wilson FG analysis).
17a-2 IR intensities via dipole-derivative tensor + Wilson- Decius-Cross intensity formula.
17a-3 thermochemistry — translational + rotational + vibrational partition functions → ZPE / U / H / S / G at any T, p.
17b-1, 17b-2, 17b-3 CPHF infrastructure + skeleton 2nd-deriv integrals + analytic RHF Hessian.
17c analytic UHF Hessian (per-spin extension of 17b-3).
17d analytic RKS Hessian (KS extension with libxc fxc kernel).
17e analytic UKS Hessian — closes the open-shell-DFT branch. LDA pure functionals exact; hybrid GGA-based functionals (B3LYP) fall back to FD path until 17e-2 lands the polarised-GGA fxc.
Added — K-Phase (full k-point sampling support)¶
K1
vibeqc.KPointsPython builder (MP / Γ-centred / shifted / Γ-only). Classical-MP auto-shift convention.vibeqc.as_bloch_kmeshboundary helper for back-compat.K2 IBZ reduction via spglib (
symmetry=True, builder methodKPoints.symmetry_reduce()). Hex/trigonal Γ-centred guard.K3
KPoints.band_path(sys)autodetects Bravais via spglib spacegroup and returns the canonical Hinuma 2017 (HPKOT) k-path via seekpath. Manual segments viascheme="manual".K4
KPoints.from_list(sys, k_frac, weights=None)with auto-normalisation.K5 density-based auto-mesh:
from_kppra(AFLOW / Curtarolo 2012),from_kspacing(Materials Project / ASE),auto(VASPAutomode).metallicflag bumps density 4× and warns if smearing isn’t enabled.K7
run_rhf_periodic_scfandrun_rks_periodic_scfacceptKPointsdirectly viaas_bloch_kmeshboundary helper.examples/input-k-mesh-convergence.pydemonstrates every flavour end-to-end on cubic Mg.
Added — Visualisation interop¶
M1
vq.write_orca_hess(path, mol, hessian_result, ...)writes ORCA-format ASCII.hessfiles for moltui / chemcraft / avogadro / VMD-nmwiz visualisation. Format-faithful character-by- character match against an ORCA 6.1.1 reference.M2
vq.write_xyz_trajectoryandvq.write_opt_trajectorywrite multi-XYZ trajectory files for geometry-optimisation paths, NEB images, normal-mode movies, and general trajectory visualisation in moltui / OVITO / ASE / Avogadro / PyMOL. The.optconvenience wrapper auto-formats per-step energies + RMS gradients into the comment line.vq.normal_mode_trajectory(mol, hessian_result, mode_index)helper produces the frame list for animating one vibrational mode.
Added — Roadmap¶
The original 30–50-day v0.5 kitchen-sink milestone was split into smaller single-headline minor releases (v0.5–v0.13). v0.5 itself absorbs both the molecular-Hessian and K-Phase headlines because both shipped in the same development cycle. See docs/roadmap.md § “v0.5.0” for the full breakdown.
Added — Existing dependencies¶
spglib>=2.0andseekpath>=2.1are now hard runtime dependencies (the C++ core already vendored libspglib forattach_symmetry; the Python bindings are needed for the K-Phase user-facing helpers).
Test suite¶
1137 passed, 25 skipped, 2 xfailed (post-17e + K-Phase + M1
M2). The xfails are: (a) hybrid B3LYP smoke for 17e UKS Hessian (needs polarised GGA fxc, scheduled for Phase 17e-2), (b) one pre-existing periodic xfail.
Added¶
Phase 17c — analytic UHF Hessian. New :func:
vibeqc.compute_hessian_uhf_analyticis the per-spin extension of Phase 17b-3. Solves the UHF coupled-perturbed Hartree- Fock equations (faithful port of :func:pyscf.scf.ucphf.solve_withs1) with α and β orbital responses coupled through the J(D_α + D_β) Coulomb response.Algorithm differences vs RHF (Phase 17b-3):
Skeleton: total density
D = D_α + D_βfor the (T+V) contraction; UHF two-particle densityΓ = (1/2)(D_α+D_β)(D_α+D_β) − (α_HF/2)(D_α D_α + D_β D_β)for the ERI contraction (new C++ binding :func:compute_eri_hessian_contribution_uhf).Per-spin h1ao_σ:
∂F_σ/∂R_{A,d}withF_σ = T+V + J(D_α+D_β) − α_HF · K(D_σ). 12N Fock builds via FD per Hessian (6N for each spin).Stacked CPHF: LGMRES on
(I + L) (mo1_α, mo1_β) = (mo1base_α, mo1base_β)where the orbital-Hessian operatorLcouples α and β through the J piece. Orthonormality fix:mo1_σ[occ-occ] = -½ s1_oo,σ.Response part: per-spin contributions sum cleanly:
+2 tr(h1ao_α · dm1_α) + 2 tr(h1ao_β · dm1_β) − 2 tr(s1ao · ε_α·dm1_α) − 2 tr(s1ao · ε_β·dm1_β) − tr(s1oo_α · mo_e1_α) − tr(s1oo_β · mo_e1_β). The factor 2 (vs RHF’s 4) is the spin-restricted convention.
Solver note: switched to
scipy.sparse.linalg.lgmresafter observing that vanilla GMRES (and bicgstab) stall on OH radical / STO-3G. The UCPHF orbital-Hessian operator on small open-shell systems has a richer eigenvalue spectrum than RHF, where GMRES’ truncated Krylov subspace doesn’t span well; LGMRES’ loose-restart mechanism (carrying vectors across restarts) handles it cleanly.Validation:
Triplet O₂ / STO-3G: max element diff vs PySCF UHF analytic Hessian = 9.2 × 10⁻⁹ Ha/bohr² (FD-truncation floor).
OH radical (doublet) / STO-3G: max element diff < 1 × 10⁻⁶ Ha/bohr² — stronger test of per-spin code path (5 α-occupied + 4 β-occupied + 1 unpaired electron in SOMO, spin contamination ⟨S²⟩ ≈ 0.753 vs ideal 0.75).
Linear-molecule branch preserved: O₂ → 5 zero modes + 1 stretch.
Symmetry: machine precision.
Refusal on un-converged UHF (clean ValueError).
Public API: :func:
vibeqc.compute_hessian_uhf_analytic. The :class:HessianResultis shape-compatible with both the FD path (Phase 17a-1) and the RHF analytic path (Phase 17b-3), so the IR and thermochemistry consumers plug in unchanged.8 tests in
tests/test_hessian_analytic_uhf.py. Test suite: 1003 passed, 25 skipped, 1 xfailed (was 995 → +8). First 1000+ test milestone.Next: 17d-f — analytic RKS / UKS Hessian, which adds the libxc XC-kernel response on top of 17b-3 + 17c structure.
Phase 17b-3 — analytic RHF Hessian assembly. New :func:
vibeqc.compute_hessian_rhf_analyticcombines the skeleton second-derivative integral contractions (Phase 17b-2) with the CPHF orbital-rotation amplitudes (Phase 17b-1) to produce the exact closed-shell-RHF Hessian without any 6N-displacement finite-difference of energies or full SCFs.Algorithm (PySCF-faithful port of :func:
pyscf.scf.cphf.solve_withs1for the response part):Skeleton contributions (4 calls into 17b-2 routines): nuclear repulsion,
Σ D ∂²(T+V),Σ Γ ∂²(μν|λσ),−Σ W ∂²S.AO Fock first-derivative tensor
h1ao[A, d, μ, ν] = ∂F_μν/∂R_{A,d}and overlap derivatives1ao[A, d, μ, ν]via central differences on F and S at fixed reference density (6N Fock builds — much cheaper than the 17a-1 path’s 6N SCFs).CPHF solve: GMRES on the linear system
(I + e_ai · L) x = mo1base[vir]where L is the orbital- Hessian operator (closed-shell J − ½K) andmo1base[vir] = -e_ai · (h1mo − s1mo · ε_i)[vir]. Occ-occ block of mo1 is fixed at-½ s1_ooby orthonormality of the perturbed orbitals (Yamaguchi-Schaefer C.1.20).Orbital-energy response:
mo_e1 = hs[occ] + mo1[occ] · (ε_i − ε_j)withhs = h1mo − s1mo · ε_i + fvind(mo1).Response part of Hessian:
+4 tr(h1ao · dm1) − 4 tr(s1ao · ε·dm1) − 2 tr(s1oo · mo_e1)summed over atom pairs.
Returns a :class:
HessianResultindistinguishable from :func:compute_hessian_fd’s output, so all the 17a-2 IR and 17a-3 thermo consumers plug in unchanged.Validation:
PySCF parity on H₂O / STO-3G: maximum element diff with PySCF’s analytic Hessian is 2.5 × 10⁻⁹ Ha/bohr² (FD-truncation floor on the h1ao/s1ao FD step). Frequencies match to <0.01 cm⁻¹ (2044.090 / 4485.951 / 4787.876 cm⁻¹ for the bend / sym / antisym stretch).
Symmetry: returned Hessian symmetric to machine precision.
H₂ linear-molecule branch: 5 zero modes + 1 stretch, agrees with the FD path to <1 cm⁻¹.
Refusal on un-converged HF (clean ValueError).
GMRES details: scipy
sparse.linalg.gmreswith bothrtol=tolANDatol=tol(the absolute tolerance handles the high-symmetry edge case where||b||is near-zero — H₂ along the bond axis hasb ≈ 2 × 10⁻¹²so a relative-only criterion is unreachable). Default tolerance 1e-8, max 100 matvecs.Two debugging stories worth flagging:
Iteration sign: PySCF’s
lib.krylovsolves(I + L) x = bwith the convention that the iteration direction isx[vir] = mo1base[vir] − e_ai · fvind(x)[vir](MINUS sign). Initial port had PLUS, giving ~30% smaller responses and 4 × 10⁻² Ha/bohr² Hessian errors.GMRES tolerance: relative-only convergence criterion fails on near-zero RHS (H₂ symmetry-breaking). Setting
atol=tolfixes the edge case.
V1 implementation uses FD-on-Fock for
h1ao; a follow-up optimization will replace this with a true analytic libintderiv_order=1+ shell-slice contraction for an additional 3-5× speedup on larger molecules.8 tests in
tests/test_hessian_analytic.py: API surface, HessianResult-compatibility with the FD path, PySCF Hessian parity, frequency parity, symmetry, refusal-on-unconverged-SCF,basis_namerequirement, linear-molecule structure on H₂.Test suite: 995 passed, 25 skipped, 1 xfailed (was 987 → +8).
Phase 17b-2 — second-derivative integral contractions for the analytic RHF Hessian. Four new C++ functions wrap libint’s
deriv_order=2engines and return(3N, 3N)skeleton-Hessian contributions directly (avoiding the materialised 4D derivative tensor):compute_overlap_hessian_contribution(basis, mol, W)—−Σ_μν W ∂²S/∂R_α∂R_βfor the overlap-Lagrangian piece.compute_kinetic_nuclear_hessian_contribution(basis, mol, D)—Σ_μν D ∂²(T+V)/∂R_α∂R_β, with libint sweeping basis-centernuclear-center perturbations together.
compute_eri_hessian_contribution(basis, mol, D, alpha_hf=1.0)—Σ_μνλσ Γ ∂²(μν|λσ)/∂R_α∂R_βwith the closed-shell two- particle density.alpha_hfcovers HF (=1) / hybrid DFT / pure DFT (=0) in one path.nuclear_repulsion_hessian(mol)— closed-form∂²E_nuc/∂R_α∂R_β, no libint needed.
Implementation notes:
Buffer ordering: libint emits one 2D buffer per upper-triangle pair
(i, j)of perturbation indices (i ≤ j). For each pair we map(i, j)to(global DOF a, global DOF b)via the appropriatepert_to_dofhelper, contract the buffer with the weight matrix, and scatter into the symmetric Hessian.Off-diagonal libint pairs that map to the same global DOF (e.g. shells s1 and s2 both sit on atom 0) require a factor of 2 in the scatter — the ordered double-sum visits
(i, j)and(j, i)separately, both giving the same value. This was the initial bug when the test FD-on-gradient parity was off by O(100) on the diagonal blocks; passingi == jtoscatter_hessianfixes the accounting and the all-on-one-atom shell quartets sum to zero (translational invariance).Per-contribution gradient bindings:
nuclear_repulsion_gradient,overlap_gradient_contribution,one_electron_gradient_contribution,two_electron_gradient_contribution(and_uhf) are now exposed to Python — both for the FD cross-checks here and for the Hessian-assembly response part (Phase 17b-3).
Validation (8 tests in
tests/test_hessian_integrals.py):Symmetry of every returned matrix to
1e−12.Closed-form nuclear-repulsion Hessian matches FD on
nuclear_repulsion_gradientto1e−7Ha/bohr².FD-on-gradient parity for all three libint-driven skeleton pieces — at the H₂O / STO-3G reference geometry, contracting
∂²S, ∂²(T+V), ∂²(μν|λσ)with the same referenceW/Dgives the same(3N, 3N)matrix (to FD-truncation tolerance,1e−4for the 1-e pieces and1e−3for ERI which has the largest FD truncation error) as numerically differentiating each*_gradient_contributionat fixed weights.
Phase 17b-V — vendored libint upgraded to
deriv_order=2.scripts/build_libint.shnow builds the source tree withLIBINT2_ENABLE_ONEBODY=2,LIBINT2_ENABLE_ERI=2, andLIBINT2_*_MAX_AM="5;4;3"(max angular momentum 5, 4, 3 for derivative orders 0, 1, 2 respectively). Tightening max_am at higher derivative orders matches the convention in MPQC, NWChem, ORCA, and PySCF and keeps the build cost manageable (~30 min on an M-class MacBook vs ~10 min for the deriv_order=1-only tree).Why a custom build at all: Homebrew’s
libint 2.13.1is configured forderiv_order=1only; callinglibint2::Enginewithderiv_order=2aborts inside libint when the source code for the corresponding kernels was never generated. A vendored build with the right configure flags is the only portable path — the alternative (“pre-built tarball with deriv_order=2”) doesn’t ship from upstream. Same vendoring pattern we already use for libecpint, libxc, spglib, and FFTW.Test suite: 987 passed, 25 skipped, 1 xfailed (was 979 → +8 net from the new 17b-2 tests).
Next: 17b-3 — analytic RHF Hessian driver that combines these skeleton contributions with the CPHF orbital-response amplitudes from Phase 17b-1, plus the AO-basis perturbed-Fock first derivative (
h1ao[ia]) for each atom.Phase 17b-1 — Coupled-Perturbed Hartree-Fock kernel for RHF. New :func:
vibeqc.cphf_solve_rhfsolves the linear-response equation.. math:: \mathbf{A} \, \mathbf{U}^x = -\mathbf{B}^x
where :math:
\\mathbf{U}^xis the orbital-rotation amplitude in the occupied-virtual block (MO response to perturbation x) and :math:\\mathbf{A}is the closed-shell-RHF orbital Hessian. The matrix-vector product.. math:: [\mathbf{A}\,\mathbf{v}]{ia} = (\varepsilon_a - \varepsilon_i) v{ia} + \sum_{jb} \big[ 4(ai|jb) - (ab|ij) - (aj|ib) \big] v_{jb}
is implemented via a single AO-basis Fock build per CG iteration: :math:
G[D^v]^{ov}with :math:D^v_{\\mu\\nu} = \\sum_{ia} v_{ia} (C_{\\mu a} C_{\\nu i} + C_{\\mu i} C_{\\nu a}). Solver is preconditioned conjugate gradient with the diagonal :math:(\\varepsilon_a - \\varepsilon_i)^{-1}preconditioner — the orbital Hessian is strongly diagonal-dominant for non-pathological systems, so a few tens of CG iterations is typical.Cost per CG iteration: one :math:
O(N^4)AO Fock build, identical to one SCF iteration. For a typical small molecule (H₂O / 6-31G*), the entire 3-RHS polarizability solve takes the same wall time as a single SCF cycle.First application: :func:
vibeqc.dipole_polarizability_rhfcomputes the static dipole polarizability tensor by solving the CPHF equations with the three Cartesian dipole-integral matrices as RHS, then contracting with the dipole MO occ-vir block. Output is a symmetric(3, 3)tensor in atomic units (multiply by 0.14818 for ų).Validation:
PySCF parity on H₂O / STO-3G: CPHF polarizability tensor agrees with PySCF’s FD-on-dipole reference to ~1e-5 a.u. (FD truncation in the reference, not us).
Symmetry of α:
α_αβ = α_βαto machine precision.Positive-definiteness of α: all eigenvalues > 0 for stable closed-shell molecules.
H₂ structure: α_∥ (along bond) > α_⊥ as expected for a diatomic.
Linearity in the RHS: doubling the RHS doubles the orbital-response amplitudes.
Refusal on un-converged HF (clean ValueError) and non-convergence of CG (CPHFConvergenceError, not silent garbage).
Public API additions: :func:
vibeqc.cphf_solve_rhf, :func:vibeqc.dipole_polarizability_rhf, :class:vibeqc.CPHFOptions, :class:vibeqc.CPHFConvergenceError.11 tests in
tests/test_cphf.py. Test suite: 979 passed, 25 skipped, 1 xfailed (was 968 → +11).This is the first piece of Phase 17b — the reusable CPHF engine that powers the analytic Hessian (next), NMR shielding, hyperpolarizabilities, and TDDFT down the road. Phase 17b-2 brings second-derivative integrals (
∂²S/∂x∂y,∂²(μν|λσ)/∂x∂y) via libint; Phase 17b-3 assembles the analytic RHF Hessian using 17b-1 + 17b-2.Phase 17a-3 — molecular thermochemistry post-processor. New :func:
vibeqc.compute_thermochemistryconsumes a :class:HessianResultand returns ZPE plus harmonic-oscillator + rigid-rotor + ideal-gas thermal corrections at user-specified temperature and pressure.Output (:class:
vibeqc.ThermoResult):Per-component energies (Hartree, per molecule):
zpe= (1/2) Σ ℏω_i,e_trans= (3/2) k_B T,e_rot= (3/2) k_B T (nonlinear) or k_B T (linear),e_vib(thermal vibrational, excluding ZPE).Cumulative thermal corrections:
u_thermal= ZPE + sum of components,h_thermal=u_thermal+ k_B T (ideal gas PV = k_B T per molecule),g_thermal=h_thermal− T·S.Per-component entropies (Hartree/K, per molecule):
s_trans(Sackur-Tetrode),s_rot(rigid rotor),s_vib(harmonic oscillator),s_elec(k_B ln(g_elec) — g_elec defaults tomol.multiplicity= 2S+1).Heat capacities:
cv_trans,cv_rot,cv_vib,cv_total(per molecule, Hartree/K).Diagnostics:
rotor_type∈ {“atom”, “linear”, “nonlinear”} auto-detected from the Hessian’sis_linearflag andn_atoms;n_imaginary_modes_excludedcounts modes dropped from the partition function (>0 means the geometry isn’t a true minimum).
Conventions match PySCF’s :func:
pyscf.hessian.thermo.thermoverbatim — atomic-unit per-molecule quantities — so cross-checks go through term-by-term.ThermoOptionscoverstemperature(default 298.15 K),pressure(default 101325 Pa = 1 atm),symmetry_numberσ (default 1; user must set for any non-trivial point group), and an optionalelectronic_degeneracyoverride.Validation:
PySCF parity on H₂O / STO-3G at 298.15 K and 500 K — every per-component term (ZPE, E_trans/rot/vib, all entropies, heat capacities, U/H/G corrections) agrees within FD-frequency precision (~1e-7 Ha) and CODATA-2014 vs CODATA-2018 Boltzmann constant difference (~1e-6 relative).
Linear vs nonlinear rotor handling: H₂ →
e_rot = k_B T, H₂O →e_rot = (3/2) k_B T.Atomic gas: He → no rotation, no vibration; translational entropy at STP within 0.2% of the tabulated 126.15 J/(K·mol).
Symmetry number: doubling σ shifts S_rot by exactly −k_B ln 2 (rotational-partition-function scaling).
Electronic degeneracy: triplet O₂ default picks up S_elec = k_B ln 3.
Imaginary modes are excluded with a count diagnostic; injecting a manual imaginary frequency drops it from ZPE and the vibrational partition function exactly.
14 tests in
tests/test_thermo.py. Test suite: 968 passed, 25 skipped, 1 xfailed (was 954 → +14).This closes Phase 17a — the molecular finite-difference vibrational + thermochemistry stack is end-to-end usable. Phase 17b (analytic CPHF for RHF) replaces the 6N FD displacements with a single linear-response solve for an order-of-magnitude speed-up on larger molecules.
Phase 17a-2 — IR intensities by finite-difference dipole. New :func:
vibeqc.ir_intensities, plus aHessianFDOptions.include_dipole_derivativesflag that wires dipole-moment evaluation into the existing 6N-displacement Hessian loop. The flag isFalseby default — flipping it on adds one dipole evaluation per displaced geometry (cheap, the SCF result is already in hand) and populates a(3N, 3)dipole-derivative tensor on the result alongside the Hessian.ir_intensitiesthen transforms that Cartesian dipole-derivative tensor into the mass-weighted normal-mode basis and applies the Wilson-Decius-Cross formula.. math:: I_p = \frac{N_A \pi}{3 c^2} \Big| \frac{\partial \mu}{\partial Q_p} \Big|^2
to return per-mode IR band intensities in km/mol. Conversion factor derived from CODATA constants: 974.864 × m_u/m_e ≈ 1.78 × 10⁶ km/mol per
|∂μ/∂Q|²(atomic units, with Q in√m_e · bohr).Implementation notes:
Dipole origin = centre of mass of the reference geometry, fixed across all displacements. Stops origin-shift artifacts leaking into the dipole-derivative tensor (matters for charged species; harmless for neutral ones).
Trans/rot zero modes are masked to 0 in the IR output. The eigh decomposition of the projected Hessian returns an arbitrary orthonormal basis of the 6-dim (5-dim for linear molecules) kernel; that basis can rotate translations and rotations into each other, and rotations have non-zero
∂μ/∂Q(rotating a polar molecule changes the dipole direction). The harmonic IR formula doesn’t apply to non-vibrational modes — Gaussian / NWChem / ORCA all drop them too.SCF reuse: refactored the inner FD loop so each displaced geometry runs SCF once and the converged result feeds both
compute_gradient_*and the dipole evaluation. No redundant SCFs.
Validation:
Cartesian dipole-derivative tensor on H₂O / STO-3G agrees with PySCF’s own FD-on-dipole to ~1e-6 e (FD truncation level).
H₂ (homonuclear, no dipole derivative): all intensities < 1e-10 km/mol.
HF (heteronuclear): one IR-active mode in the 5–200 km/mol range; trans/rot all exact 0.
H₂O / STO-3G: bend / sym-stretch / antisym-stretch within the range published in Koput-style FD-of-dipole tables.
H/D isotope substitution on HF: intensity ratio I_DF / I_HF = μ_HF / μ_DF ≈ 0.526, the textbook 1/μ scaling of the harmonic IR intensity in km/mol.
Translational invariance: shifting the molecule rigidly leaves every per-mode intensity unchanged to ~1e-6 km/mol.
9 new tests added to
tests/test_hessian.py(23 total in the file). Test suite: 954 passed, 25 skipped, 1 xfailed.New :func:
HessianResultfields:dipole_derivatives((3N, 3)array, or None),dipole_origin((3,)array, or None),masses_amu((n_atoms,)array — stored soir_intensitiesand downstream thermochemistry stay self-contained).Next: 17a-3 — thermochemistry post-processor (ZPE / U / H / S / G at user-specified T, P from the harmonic-oscillator partition function).
Phase 17a-1 — molecular finite-difference Hessian + harmonic frequencies. New :func:
vibeqc.compute_hessian_fdbuilds the 3N × 3N nuclear Hessian by central differences on the analytic atomic gradient, then mass-weights and diagonalises it (with translation + rotation modes projected out) to return harmonic vibrational frequencies in cm⁻¹.Methods supported:
"RHF","UHF","RKS","UKS"via the existing analytic-gradient kernels (Phase 16). For DFT methods the functional and grid options forward to :func:compute_gradient_rks/_uks.Output (:class:
vibeqc.HessianResult): raw and mass-weighted Hessian, frequencies sorted ascending (imaginary modes returned as negative numbers per Gaussian / NWChem / ORCA convention), mass-weighted normal modes, imaginary-mode count, FD displacement count, and a flag for whether the molecule was treated as linear (auto-detected via inertia-tensor rank → 5 zero modes instead of 6).Trans/rot projection (default
project_trans_rot=True): the 6 (linear: 5) zero modes appear as exact 0.0 infrequencies_cm1so callers can slice[6:]or[5:]for the vibrational subset cleanly.Isotope substitution via
HessianFDOptions.atomic_masses_amuoverrides the standard atomic-mass table per atom — useful for D/H, ¹³C/¹²C, etc. The SCF + gradient evaluations don’t need to be repeated when only masses change.PySCF cross-check: the H2O / STO-3G FD Hessian agrees with PySCF’s analytic Hessian to ~5e-5 Ha/bohr² (FD truncation at the default step 0.005 bohr) and frequencies match to <1 cm⁻¹.
Cost: 6N gradient evaluations (one ± displacement per Cartesian DOF). Phase 17b will replace this with analytic CPHF (a single linear-response solve) for an order-of-magnitude speed-up.
14 tests in
tests/test_hessian.pypin the contract: API surface, PySCF-parity on H2O / STO-3G, linear vs nonlinear trans/rot projection (5 vs 6 zero modes), Hessian symmetry, method dispatch on RHF / UHF / RKS / UKS (LDA), no imaginary modes at a stationary point, isotope ratio D₂/H₂ → 1/√2 within the harmonic mass-scaling law, error paths (unknown method, invalid step, wrong-length isotope override), default option values.Phase C1a-2 — Saunders-Hillier level shift on the molecular SCF drivers (RHF / UHF / RKS / UKS). Mirrors the periodic-side C1a contract for closed-shell and open-shell molecules. Adds
F_shift = F + b·S − (b/2)·S·D·S (RHF / RKS) F_σ_shift = F_σ + b·S − b·(S·D_σ·S) (UHF / UKS, per spin)
to the Fock matrix before each diagonalisation, where
b = level_shift(default 0.0 = no shift). Raises virtual orbital eigenvalues bybwhile leaving the occupied ones unchanged (½ S D Sprojects onto the occupied subspace at the converged density), so the shift is inert at the SCF fixed point — only the iteration dynamics are damped. Useful when DIIS oscillates between near-degenerate occupied / virtual swaps on small-HOMO– LUMO-gap molecules. Skipped during the C1c quadratic phase (the Newton step’s(ε_a − ε_i + λ)denominator is the analogous preconditioner). The final self-consistency pass (which produces the returnedmo_energies) does not include the shift, so reported orbital eigenvalues are physical regardless of the shift value used during iteration.New options on :class:
vibeqc.RHFOptions, :class:vibeqc.UHFOptions, :class:vibeqc.RKSOptions, and :class:vibeqc.UKSOptions:level_shift(double, Hartree, default 0.0). Typical activation values: 0.1 – 0.5 Hartree.12 tests in
tests/test_molecular_level_shift.pypin the contract: field exposure on all four option structs, total-energy inertness at convergence on H2O / STO-3G (RHF, RKS) and triplet O / STO-3G (UHF, UKS), unshifted MO eigenvalues at convergence (RHF + UHF per spin), default-zero reproduces baseline byte-for- byte, and a non-zero shift actually perturbs the iteration trajectory (guards against the dead-code regression wherelevel_shiftis read but never applied).Phase C1c — second-order (“quadratic”) SCF fallback for the Γ-only periodic Ewald drivers (RHF / UHF / RKS / UKS). When standard SCF (damping + DIIS + level shift) fails to converge — typically on small-gap insulators where DIIS oscillates between near-degenerate occ/vir swaps — switch from “diagonalise F” to a Newton step in MO space:
κ_{ai} = -F_{ai}^{MO} / (ε_a − ε_i + λ) C_new = C_prev · exp(κ) D_new = 2 · C_new[:, :n_occ] · C_new[:, :n_occ]^Twith diagonal-Hessian preconditioning (λ =
quadratic_fallback_shift, default 0.1 Ha) and a trust-region cap on ‖κ‖ (quadratic_fallback_max_step, default 0.1). DIIS and level shift are skipped during the quadratic phase — the Newton step is its own update mechanism and mixing with extrapolation undoes the trust region. Activates whenquadratic_fallback_iter > 0and the SCF has run that many iterations without converging; default= 0keeps the pre-C1c behaviour bit-identical.New options on :class:
vibeqc.PeriodicRHFOptions, :class:vibeqc.PeriodicSCFOptions, and :class:vibeqc.PeriodicKSOptions:quadratic_fallback_iter,quadratic_fallback_shift,quadratic_fallback_max_step. New helper module :mod:vibeqc.quadratic_scfships :func:vibeqc.quadratic_scf.expm_skew(skew-Hermitian matrix exponential via scaling-and-squaring + Taylor — handles real and complex input, avoids a hard scipy dependency) and :func:vibeqc.quadratic_scf.quadratic_step(one Newton step taking(F, C, ε, n_occ)and returning(C_new, ε_new)).C1c-2 — multi-k Ewald drivers. The same fallback wired through :func:
vibeqc.run_rhf_periodic_multi_k_ewald3d, :func:vibeqc.run_uhf_periodic_multi_k_ewald3d, :func:vibeqc.run_rks_periodic_multi_k_ewald3d, and :func:vibeqc.run_uks_periodic_multi_k_ewald3d. Per-spin per-k Newton step on complex F(k) / C(k) —expm_skew/quadratic_stepupgraded to be dtype-agnostic (real input stays real; complex input produces a unitary rotation via the skew-Hermitian κ formulation). The same DIIS-and-level-shift bypass applies during the quadratic phase.21 tests in
tests/test_quadratic_scf.pypin kernel correctness (group law, orthogonality of exp on skew input, Brillouin-condition identity step at convergence, monotone gradient decrease per step, complex-input unitarity), bindings round-trip, and integration parity: the fallback path produces the same converged energy as the standard path on H₂ / STO-3G (RHF, UHF) and PBE (RKS, UKS), H atom UKS, and the multi-k [1,1,1] mesh on each driver.C1c-3 — molecular drivers. The same fallback wired through :func:
vibeqc.run_rhf, :func:vibeqc.run_uhf, :func:vibeqc.run_rks, and :func:vibeqc.run_uks. Native C++ kernel (cpp/include/vibeqc/quadratic_scf.hpp/cpp/src/quadratic_scf.cpp) implementsexpm_skew(real skew-symmetric only — molecular Fock is real) andquadratic_stepwith the same diagonal-Hessian preconditioning and trust-region cap as the Python kernel, mirroring the C++ signature for consistency with future Hessian / CPHF code that will share the same machinery. New options on :class:vibeqc.RHFOptions, :class:vibeqc.UHFOptions, :class:vibeqc.RKSOptions, :class:vibeqc.UKSOptions:quadratic_fallback_iter(default 0 = disabled),quadratic_fallback_shift(default 0.1),quadratic_fallback_max_step(default 0.1). UHF / UKS run the Newton step independently per spin (each with its own n_occ and ε spectrum). DIIS and damping are skipped during the quadratic phase. Six new tests intests/test_quadratic_scf.pycover the molecular path: option-default values, byte-identical default behaviour when disabled, and converged-energy parity with the standard SCF on H₂O / STO-3G (RHF, RKS) and triplet O / STO-3G (UHF, UKS).
Changed¶
New page: docs/good_practices.md — working conventions for operating a quantum-chemistry program. None are vibe-qc-specific (file layout outside the source tree,
<system>_<method>_<basis>.pynaming, version-pinning for paper calculations,OMP_NUM_THREADShygiene, basis/grid/k-mesh convergence order,tee+screenfor long runs, what to try when SCF diverges, common-gotcha table) but nobody tells beginners, and everybody learns the hard way. Lowering the barrier-to-entry for users coming from “I’ve used Gaussian / ORCA but never built one from a clone” backgrounds. Wired into the Getting-Started toctree betweenrunningandupdating, and called out fromquickstart.md“Where to go next” as a five-minute read.
[0.4.7] — 2026-04-28 — “Schrödinger’s Llama”¶
Docs-only patch — two user-visible bugs on the live v0.4.6 site.
Cherry-picked from main into hotfix-0.4.7 off the v0.4.6 tag,
tagged + fast-forwarded release (no v0.5.0-dev work pulled in).
Fixed¶
First-calculation snippet works outside the source tree. Previous wording (
save water.pythen run.venv/bin/python water.py) implicitly assumed the user stayed inside the cloned repo, because the.venv/bin/pythonshorthand is a relative path. Real userscd ~after install, hit.venv/bin/python: no such file or directory, and have to guess that the venv is back in the repo. The landing page (docs/index.md) and quickstart (docs/quickstart.md) now lead withmkdir -p ~/vibeqc-runs/<project>+ the absolute~/path/to/vibeqc/.venv/bin/python water.pyinvocation, and the activate-venv tip uses the same absolute form so it works from any directory. Also fixed a typo in docs/running.md (.venv/path/to/.venv/bin/python→~/path/to/vibeqc/.venv/bin/python). Cherry-pick of33ecfa8from main.Furo “ERROR: Adding a table of contents…” red block rendered inline on docs/quickstart.md, docs/running.md, and docs/updating.md. The Furo theme’s anti-duplication assertion fires the error visibly in the page rather than as a build-time warning — pretty user-hostile. Stripped the
{contents}blocks from all three pages; right-sidebar TOC remains unchanged. (On main, the same fix also covers docs/good_practices.md and docs/example_outputs.md, but those pages were added during v0.5 development and don’t exist on the release branch.) Subset of the fix from3778586.
[0.4.1] — UNRELEASED (planned)¶
Patch release. Fixes that need to reach published-release users
without dragging in the rest of main’s 0.5.0-dev work.
Fixed¶
scripts/build_libint.shdep-detection failed on Arch / Manjaro with Eigen 5 (1f3174f, planned cherry-pick frommainfor 0.4.1). The old check usedls a bto test for header existence;lsreturns non-zero when any listed argument is missing, so on Arch (where Eigen 5 installs at/usr/include/eigen3/only — no/usr/include/Eigen/symlink) the check falsely reported eigen3 missing even after a freshpacman -S eigen. Replaced withpkg-config --exists eigen3[ -e ... ]short-circuit + a broaderfindwalk forgmpxx.h. libint 2.13.1’s bundledFindEigen3.cmakeis already Eigen-5-aware (line 51-63 ofcmake/modules/FindEigen3.cmake), so this was purely a vibe-qc-side detection bug — once the dep-check stops lying, Arch’s system Eigen 5 builds libint cleanly.
docs/installation.mdArch / Manjaro section now includes a{note}admonition confirming Eigen 5 is fine — saving users who hit the old behaviour from chasing a phantom AUR-eigen3 workaround.scripts/build_libint.shcmake invocation gains-Wno-dev. CMake 3.30+ deprecated its bundledFindBoost.cmakemodule (policy CMP0167) and emits a dev warning whenever a project uses the oldfind_package(Boost ...)interface. libint 2.13.1’s CMakeLists.txt:360 still uses that interface (waiting on upstream to adoptfind_package(Boost CONFIG REQUIRED)+Boost::headers). Until then,-Wno-devsuppresses the cosmetic warning without changing build behaviour, keeping the install log readable on Arch / Manjaro (CMake 4.x) without hiding any user-actionable diagnostics.CMake 4.x + vendored deps (libxc, spglib, FFTW, libecpint, pugixml, libcerf): pass
CMAKE_POLICY_VERSION_MINIMUM=3.5. CMake 4.x removed compatibility withcmake_minimum_required(VERSION <3.5). libxc 7.0.0, FFTW 3.3.10, and several of the libecpint vendored deps still declare pre-3.5 minimums in their upstream CMakeLists.txt and fail to configure on Arch / Manjaro (CMake 4.2+) without this flag. Adding-DCMAKE_POLICY_VERSION_MINIMUM=3.5plus-Wno-devto every vendored-dep cmake invocation tells CMake “treat the declared minimum as 3.5 anyway” without changing actual build behaviour. Real fix is upstream those projects bumping their declared minimums; until then we override on the consume side.scripts/build_libint.shprints a “next: setup_native_deps.sh” hint on success. Users running the libint build directly (e.g. to debug the libint compile in isolation) hit a wall later atpip installbecause vibe-qc’s CMake also needs libxc / spglib / FFTW / libecpint, none of whichbuild_libint.shtouches. The orchestrator script handles all of them (and skips libint as a no-op when it’s already built); pointing users at it from insidebuild_libint.shprevents the confusion.
[0.4.3] — UNRELEASED (planned)¶
Documentation + ergonomics patch release. No code changes to runtime behaviour; adds a turnkey “update existing checkout” workflow.
Added¶
scripts/update.sh— turnkey update for existing checkouts. One command replaces the four-step manual sequence (``git fetch + checkout + pull → setup_native_deps.sh → pip installverify
). Refuses to run if the working tree has uncommitted changes, handles branch / tag / remote-only-branch refs uniformly, detects venvs at common paths (.venv//venv/`` / etc.), and prints the new banner at the end so the user can confirm the version flipped. Flags:--ref REF(defaultrelease): target git ref. Common values arerelease/main/vX.Y.Z.--rebuild-native-deps: nukethird_party/<dep>/install/before re-running the orchestrator. Use this when a vendored library version bumped between releases —setup_native_deps.shis idempotent on existence, not on version, so a stale install/ would otherwise silently win.--help: prints the inline option list. ~120 lines of bash; thin wrapper around the same git + setup_native_deps.sh + pip install steps documented indocs/updating.md.
docs/updating.md— user-facing update reference. Covers the./scripts/update.sheasy button and the manual equivalent, explains how to switch between releases (--ref vX.Y.Zto pin to a specific tag,--ref mainfor bleeding-edge dev,--ref releaseto flip back), documents common issues (banner shows old version → re-install Python pkg; vendored library version bumps →--rebuild-native-deps; missing libxc.so.7 →--rebuild-native-deps; stale CMake cache → wipebuild/), and notes the “don’t update mid-job” guidance for long-running calculations. Wired into the “Getting started” toctree betweenrunningandtour. Cross-linked fromrelease_process.md’s “Running calculations against a specific build” section, which previously sketched the same workflow but was framed for the upstream-maintainer audience.
[0.4.2] — UNRELEASED (planned)¶
Documentation-only patch release. Bundles fixes that improve the “first 10 minutes” user experience but don’t touch any code, plus the polish needed to support per-release codenames going forward.
Added¶
Landing-page version + codename banner.
docs/index.mdgains a{tip}admonition immediately under the title showing the current release version (auto-extracted fromimportlib.metadataindocs/conf.py) and its codename (looked up in aRELEASE_CODENAMEStable inconf.py). Banner reads e.g. “Current release: 0.4.2 — Schrödinger’s Llama”. The site is built from thereleasebranch, so whatever version is currently published shows here automatically; no manual edit at release time. (The codename table needs a one-line addition per minor release, but the version stamp Just Works.)docs/roadmap.md§ “Release codenames” catalogue. Source of truth for the new Scientist + Animal codename system that starts with v0.5.0. Includes:The convention (scientist tied to flagship feature, animal random + cute, patches inherit minor’s codename, names updateable up until the release ships)
Confirmed names: First Light (v0.1.0, bootstrap), Schrödinger’s Llama (v0.4.0, retroactive), Wilson’s Otter (v0.5.0 — E. B. Wilson, Molecular Vibrations 1955; v0.5 ships forces + frequencies, accepted as-proposed by the docs chat)
Backlog candidates for v0.6 → v2.0 (Whitten, Coester, Neese, Pisani, Runge, Stone, Bredow — each tied to whatever their target release flagships)
MyST
substitutionmachinery for release metadata. Newmyst_substitutionsblock indocs/conf.pyexposes{{release}},{{version}}, and{{codename}}as template variables in any markdown page. Used today on the landing page; available for tutorials / user-guide pages that want to render version-aware text.docs/running.md— single source of truth for “how to run vibe-qc scripts”. Covers the venv Python pattern (explicit.venv/bin/pythonvssource .venv/bin/activate), one-off vs interactive vs background runs, output capture for long calculations (tee+nohup+tmux), theOMP_NUM_THREADSknob, the SSH-into-bigger-box workflow, the test-suite invocation, and a common-errors table mappingModuleNotFoundError/RuntimeError: BasisSet/OSError: cannot load library 'libxc.so.7'/SCF did not convergeto specific fixes. Forward-references to the planned v0.5 personal-job-queue (Phase JQ1) and to the longer-term cluster / Slurm bridge (Phase JQ2). Linked from the “Getting started” toctree, plus inline cross-references fromindex.md,quickstart.md, andtour.md.examples/README.md“Running an example” section explains the.venv/bin/python examples/<script>.pypattern explicitly, including thecd examples/working-directory tip so output files stay together with the inputs. Cross-references the newrunning.mdfor the wider story.All
examples/*.pyandexamples/plots/*.pydocstringRun:lines updated frompython3 <script>.pyto.venv/bin/python <script>.py. ~30 example files touched. Matches what the docs now teach.
Fixed¶
docs/index.mdinstall snippet referenced the pre-orchestrator scripts. The landing page recommended./scripts/build_libint.sh && ./scripts/setup_basis_library.sh, which leaves libxc / spglib / FFTW / libecpint unbuilt and breaks the subsequentpip install(CMake fails with “Could not find Libxc”). Replaced with the orchestrator./scripts/setup_native_deps.sh(matches whatinstallation.mdandREADME.mdalready recommend), and added thegit checkout releaseline so users land on the tagged release rather thanmain‘s 0.5.0.dev0.Landing-page “Your first calculation” snippet now tells users how to actually run it. The old snippet dropped a Python script with no save-as filename, no run command, and no mention that
import vibeqcrequires the venv’s Python. New version says “save aswater.py, run with.venv/bin/python water.py” + a tip-box covering thesource .venv/bin/activatealternative and theModuleNotFoundErrorfailure mode.docs/quickstart.mdgains a “How to run the snippets below” preamble between Step 0 (Install) and Step 1 (Molecular HF) so newcomers know to save-as-file + run-with-venv-python before hitting any of the four numbered steps. Same content as the landing-page tip-box, applied once at the top of quickstart rather than repeated per step.
[0.4.0] — 2026-04-27¶
Second tagged release. Major theme: end-to-end EWALD_3D periodic
SCF — every combination of {RHF, UHF, RKS, UKS} × {Γ-only, multi-k}
now runs through the composed Ewald J + libxc V_xc + scaled-K hybrid
backend. ECP support for transition metals (libecpint vendored and
bundled). v0.4 API polish: @property accessors, unified
scf_trace items, KS / RHF coulomb-method dispatchers. Fermi-Dirac
smearing + Saunders-Hillier level shift on the periodic SCF side.
Six new tutorials (20–25), a 4-step “first 30 minutes” quickstart,
and the public docs site at https://vibe-qc.com is now wired to
release.
Added¶
Phase 15c — periodic Kohn-Sham DFT through EWALD_3D (RKS + UKS, Γ-only + multi-k). Closes the SCF×spin×k-sampling matrix. New drivers + result classes: :func:
vibeqc.run_rks_periodic_gamma_ewald3d/ :class:vibeqc.PeriodicRKSEwaldResult(15c-1), :func:vibeqc.run_rks_periodic_multi_k_ewald3d/ :class:vibeqc.PeriodicRKSMultiKEwaldResult(15c-2), :func:vibeqc.run_uks_periodic_gamma_ewald3d/ :class:vibeqc.PeriodicUKSEwaldResult(15c-3a), :func:vibeqc.run_uks_periodic_multi_k_ewald3d/ :class:vibeqc.PeriodicUKSMultiKEwaldResult(15c-3b). Hybrid functionals (B3LYP α=0.2) wired through the newexchange_scalekwarg on :func:vibeqc.build_periodic_fock_ewald3d_k/ :func:vibeqc.build_fock_2e_ewald3d_blocks; pure DFT skips the K build entirely. C++ addition: :func:vibeqc.build_xc_periodic_uks(open-shell libxc on a LatticeMatrixSet density). KS dispatcher (:func:vibeqc.run_rks_periodic_scf, :func:vibeqc.run_rks_periodic_gamma_scf) routes EWALD_3D to the Γ-only driver for [1,1,1] meshes and the multi-k driver for denser meshes. 35 new tests; all combinations match each other to ~µHa in the closed-shell limit and reproduce<S²> = 0.75on the H-atom doublet exactly.Phase 15a / 15b — periodic UHF through EWALD_3D. Open-shell HF counterparts: :func:
vibeqc.run_uhf_periodic_gamma_ewald3d(15a) and :func:vibeqc.run_uhf_periodic_multi_k_ewald3d(15b) with per-spin Pulay DIIS, Saunders-Hillier level shift,<S²>diagnostic. 15b also fixed a quiet :class:vibeqc.LatticeMatrixSetmutation bug — assigning.blocks[i] = Mwrites to a transient Python-list copy and silently no-ops at the C++ level; new :meth:set_block(i, M)mutates the underlyingstd::vector<MatrixXd>in place. Multi-k closed-shell UHF reproduces RHF Ewald to ~µHa.Phase 14 — effective core potentials (ECP) via libecpint. Five sub-phases: 14a vendored libecpint 1.0.7 + pugixml 1.15 + libcerf 3.3 build under
third_party/libecpint/install/viascripts/build_libecpint.sh. 14b shipped :func:vibeqc.compute_ecp_matrix—V_ECP_{μν} = ⟨χ_μ|V_ECP|χ_ν⟩via libecpint’s built-in XML library (ecp10mdf / ecp28mdf / ecp46mdf / ecp60mdf / ecp78mdf / lanl2dz), Cartesian-to-spherical transform per shell-pair via libint solidharmonics primitives. 14c wired ECPs through the molecular RHF / UHF / RKS / UKS drivers viaRHFOptions.ecp_centers/ecp_libraryand matching fields on the other options classes; the Hcore is augmented before diagonalisation, Z_eff replaces Z in the nuclear V and nuclear- repulsion sums. 14e validated against PySCF on Zn²⁺ / LANL2DZ to microhartree and fixed a double-counted core-nuclear contribution (the V_n must use Z_eff = Z − ncore when ECPs are present). Bundled XML library now ships inside the wheel atpython/vibeqc/ecp_library/xml/; runtime resolution falls through$VIBEQC_ECP_SHARE_DIR→ bundled path → build-time bake for editable / wheel parity.Phase D1 — DFT-D3(BJ) dispersion correction. Three-commit rollout.
D1aships the C++ framework (cpp/include/vibeqc/dispersion.hpp,cpp/src/dispersion{,_data,_params}.cpp): :class:vibeqc.D3BJParams, :class:vibeqc.DispersionResult, :func:vibeqc.compute_d3bj, :func:vibeqc.d3bj_params_for, :func:vibeqc.d3_coordination_numbers, :func:vibeqc.d3_r2r4, :func:vibeqc.d3_rcov. Coordination numbers + analytical Jacobian, BJ-damped pairwise summation, geometric gradient, starter set of 10 functionals’ damping parameters.D1badds the referencedftd3Python backend as an optional dependency (pip install 'vibe-qc[dispersion]') — Grimme’s full CN-dependent C6 grid and ~160 functionals’ parameters via the reference Fortran implementation; routing is automatic viabackend="auto". New :func:vibeqc.dftd3_availableprobe.D1cwires D3(BJ) through the user-facing drivers:run_job(..., dispersion=...)acceptsTrue(inherit the SCF functional), a functional name ("pbe","b3lyp", …), or aD3BJParamsstruct. The.outfile grows a “Dispersion correction (D3-BJ)” block; ASE calculator gets a matching kwarg.Phase 12f — periodic Becke partition. :func:
vibeqc.build_periodic_becke_gridextends the molecular Becke fuzzy-cell partition denominator over image atoms withinimage_radius_bohrof the home cell. Required for tight crystals where image-atom Voronoi cells would otherwise intrude on the reference unit cell.PeriodicKSOptions.use_periodic_beckebecke_image_radius_bohrtoggles the partition globally on the C++ side; the new Ewald RKS / UKS drivers honour it at the Python layer too.
Phase C1a — Saunders-Hillier level shift for periodic Ewald SCF.
PeriodicRHFOptions.level_shiftandPeriodicKSOptions.level_shift(default 0.0). When > 0, the per-iteration Fock is shifted byb·(S − ½SDS), raising virtual MO eigenvalues byband suppressing occupied/virtual swaps on small-gap insulators. Inert at the converged density (the SCF fixed point is unchanged). Wired through every Ewald driver — Γ-only RHF/UHF/RKS/UKS and multi-k RHF/UHF/RKS/UKS.Phase C1b — Fermi-Dirac smearing.
PeriodicRHFOptions.smearing_temperatureand matching field onPeriodicKSOptions. When > 0, occupations follown_i = 2 / (1 + exp((ε_i − μ)/T))with μ bisected against the total-electron-count constraint, and the convergence target switches fromEto the free energyA = E − T·S. Ships in the multi-k Ewald RHF / RKS drivers (UHF/UKS smearing with two chemical potentials is a follow-up). New diagnostic fields on the result types:fermi_level,entropy,free_energy,occupations.Phase V3 / V5b — visualisation: periodic cubes + PDOS. V3: :func:
vibeqc.write_cube_mo_periodic, :func:vibeqc.write_xsf_density, :func:vibeqc.write_xsf_mofor periodic Bloch orbitals. :class:vibeqc.PrimitiveCellGrid+ :func:vibeqc.make_primitive_cell_gridbuild a uniform real-space grid spanning a chosen primitive-cell region. V5b: :func:vibeqc.density_of_states_projectedcomputes the projected DOS onto user-defined AO groups (atom, atom+ℓ, or custom index lists); :class:vibeqc.ProjectedDensityOfStatescarriescontributions,shifted_energies,group_labels(all properties).Phase SYM1–SYM3a — space-group symmetry foundations. SYM1 shipped real-basis Wigner D-matrices for AO rotation (
python/vibeqc/symmetry_core.py). SYM2a shipped the AO-basis representation of a symmetry operator. SYM2b/c shipped lattice- cell orbit identification + :class:vibeqc.LatticeMatrixSetcompression for origin-fixed and atom-pair-resolved structures. SYM3a shipped orbit-reduced storage of one-electron lattice integrals: :func:vibeqc.compute_overlap_lattice_with_orbits, :func:vibeqc.compute_kinetic_lattice_with_orbits, :func:vibeqc.compute_nuclear_lattice_with_orbits, :class:vibeqc.OrbitReducedLatticeMatrix, :func:vibeqc.symmorphic_operations, :func:vibeqc.verify_lattice_matrix_set_symmetry. Order-of-|G| memory and compute reduction on real-space matrix blocks for high-symmetry cells.Natural orbitals + idempotency diagnostic. :func:
vibeqc.natural_orbitalsdecomposes a (UHF/UKS) density matrix into natural orbitals + occupation numbers; :func:vibeqc.idempotency_deviationreportsΣ_i n_i (1 − n_i)as a wave-function-quality indicator. Closed-shell occupations ≈ 2 / 0; multi-reference systems show fractional occupations.Linear-dependence diagnostic + canonical orthogonalisation. :func:
vibeqc.check_linear_dependenceinspects the overlap matrix, reports near-null eigenvalues plus the basis functions responsible (atom, shell, ℓ, exponent, weight). The RHF / UHF / RKS / UKS drivers now use canonical orthogonalisation (rectangularS^{-1/2}projecting out null space) instead of symmetricS^{-1/2}; near-singular bases (e.g. tight aug-cc-pVTZ) converge cleanly instead of crashing.Phase 12e-c — composed EWALD_3D Coulomb dispatch. Multi-commit rollout from 12e-a/b through 12e-c-4: classical Ewald summation (
ewald_point_charge_energy,ewald_nuclear_repulsion); erfc-screened nuclear-attraction lattice sum (compute_nuclear_erfc_lattice); erfc-screened ERIs for short-range Ewald J/K (omegakwarg on ERI builders); FFT Poisson (solve_poisson_erf_screened,solve_poisson_coulomb, :class:vibeqc.ScalarField3D, :func:vibeqc.build_j_long_range); composed Hartree J + ω- invariance witness (:func:vibeqc.build_j_ewald_3d, :func:vibeqc.makov_payne_coefficient_cubic); Γ-only and multi-k RHF Ewald SCF drivers; multi-k Pulay DIIS; Madelung-cancellation helpers (:func:vibeqc.cell_electron_chargeetc.).Phase P — performance. P1: OpenMP shared-memory parallelism on every compute-heavy kernel (1e/2e integrals, Fock build, periodic lattice sums, AO evaluation). P1.1: gradient parallelism + :func:
vibeqc.get_num_threads/:func:vibeqc.set_num_threadsAPIper-run timing block in the
.outfile. P1.2: closed remaining parallelism gaps (compute_dipole, MP2/UMP2 transforms,build_xc_periodicper-cell loop). P2: peak-memory estimator + pre-flight abort with override (:class:vibeqc.MemoryEstimate, :class:vibeqc.InsufficientMemoryError, :func:vibeqc.estimate_memory).
Phases 18 + 19 — atomic charges + bond orders + dipole moment. Mulliken / Löwdin charges, Mayer bond orders, dipole moment in atomic units and Debye, with center-of-mass origin. Wired into every
run_job.outfile. Public API: :func:vibeqc.mulliken_charges, :func:vibeqc.loewdin_charges, :func:vibeqc.mayer_bond_orders, :func:vibeqc.dipole_moment, :class:vibeqc.DipoleMoment.v0.4 API polish.
BasisSet.nbasisandBasisSet.nshellsare now@propertyaccessors (no parens).:class:
vibeqc.SCFIterationis the unified type forscf_traceitems across every SCF backend (was a tuple(iter, E, dE, grad)for the Ewald drivers, dataclass for the C++ direct drivers).:class:
vibeqc.BandStructure, :class:vibeqc.DensityOfStates, :class:vibeqc.ProjectedDensityOfStatesgotshifted_energiesandgroup_labelsas@propertyaccessors.KS dispatcher (:func:
vibeqc.run_rks_periodic_scf, :func:vibeqc.run_rks_periodic_gamma_scf) mirrors the RHF side; both now route onoptions.lattice_opts.coulomb_method.RHF dispatcher (:func:
vibeqc.run_rhf_periodic_scf, :func:vibeqc.run_rhf_periodic_gamma_scf) preserveslevel_shiftthrough option-class translation (the bug fix that let the dispatcher silently lose the field on the way to the Ewald backend).
Build / packaging. Every native dependency (libint, libxc, spglib, fftw, libecpint) now vendored under
third_party/<dep>/viascripts/setup_native_deps.sh. The bundled basis library (python/vibeqc/basis_library/) and ECP XML library (python/vibeqc/ecp_library/) ship inside the wheel — a stockpip install vibe-qcworks out of the box, no setup script, noLIBINT_DATA_PATHfiddling, no Homebrew dependency.BasisSetconstruction now hardened against unknown / malformed basis names (ensure_libint_initialized()on the ctor; libint2’s own throws translated with directive context; the empty-shells case keeps itsRuntimeError). 7 new error-path tests pin the contract.Banner with git provenance.
vq.print_banner()printsRelease vX.Y.Zon a tagged clean checkout anddev X.Y.Z (branch @ sha)(withdirtyflag if the working tree has uncommitted changes) otherwise. Every persisted SCF log carries the same banner, so a calculation’s exact build is unambiguous.Documentation. Six new tutorials: 20 (natural orbitals + idempotency), 21 (PDOS), 22 (periodic Bloch-orbital cubes), 23 (tight-cell DFT with periodic Becke), 24 (periodic SCF convergence — level shift + DIIS + damping), 25 (symmetry-aware storage of lattice integrals). 4-step “first 30 minutes” quickstart at
docs/quickstart.mdbacked byexamples/quickstart.py. Resource callouts (peak RAM + wall) on every tutorial.docs/release_process.mddocuments the branch model.GitLab CI/CD docs deployment. Pushes to
main(and nowrelease) trigger a build + deploy pipeline:scripts/build_site.shrunssphinx-buildin apython:3.13-slimcontainer with pinned doc deps (sphinx>=7.4,myst-parser>=4.0,linkify-it-py>=2,furo>=2024.5,sphinx-copybutton>=0.5) plus the runtime deps autosummary needs to importvibeqccleanly (numpy,ase,dftd3); producespublic/with rendered HTML +robots.txt+sitemap.xml+ a.build-inforecording commit SHA, branch, and pipeline ID. The deploy stage runs in an alpine container, decodes a base64-encoded SSH key from$DEPLOY_SSH_KEY_B64, and rsyncspublic/to the vibe-qc.com host (excluding ISPConfig-managed paths). Site updates within ~3 minutes of any push, replacing the prior 04:00 nightly cron that had silently broken in late April due to a $HOME-vs-chroot path mismatch. Build logs surface in the GitLab pipeline UI rather than/var/www/.../cron_error.log.
Changed¶
Dispatcher is the recommended entry point. Tutorials and examples migrated from the bare
run_rhf_periodic/run_rks_periodicto the dispatchers (run_rhf_periodic_scf/run_rks_periodic_scf) so user code stays the same when the Coulomb method changes fromDIRECT_TRUNCATEDtoEWALD_3D. Tutorials 5 + 23 + theuser_guide/ewald.mdreference were further updated to drop the “multi-k EWALD_3D KS is the follow-up” wording — Phase 15c ships all four (RKS+UKS) × (Γ+multi-k) drivers, and the recommendation is to flipopts.lattice_opts.coulomb_method = CoulombMethod.EWALD_3Dfor any tight-cell 3D bulk DFT.user_guide/ewald.mdgains a “Same dispatcher for KS and UKS” table covering every driver family and its dispatcher pair.docs/conf.pysuppressesautodoc.mocked_objectwarnings. Sphinx 9.x emits ~80 of these on every build (one per public symbol re-exported from the C++ extension viaautodoc_mock_imports=vibeqc._vibeqc_core). Pure noise that drowned real warnings; intentional mocks shouldn’t warn.Pre-v0.4.0 docs-site source policy clarified in
docs/release_process.md: the public site at vibe-qc.com tracksmaindirectly during pre-tag development (banner readsdev 0.4.0.dev0 (main @ <sha>)) and switches to the fast-forward-onlyreleasebranch starting with this v0.4.0 tag.
Fixed¶
BasisSet construction was a latent segfault path. Without an upstream
libint2::initialize()call (e.g. when the very first vibe-qc operation in a process is a BasisSet ctor), libint’s globals weren’t set and the file lookup was undefined behaviour. Nowensure_libint_initialized()is called from the BasisSet member-initializer list. libint2’s own throws (bogusLIBINT_DATA_PATH, malformed.g94) translated intoRuntimeErrorwith directive context (which basis name failed, whatLIBINT_DATA_PATHwas set to, where to drop a custom.g94to fix it).ECP XML share-dir resolution. The vendoring commit had left
VIBEQC_LIBECPINT_SHARE_DIRbaked at a path that wasn’t populated on every checkout, breaking ECP tests withValueError: stoi: no conversion(libecpint reading an empty XML attribute viastd::stoi). Resolution now: vendored third_party path → libecpintPACKAGE_PREFIX_DIR→find_pathfallback. Plus a runtime$VIBEQC_ECP_SHARE_DIRenv var the Python__init__sets to the wheel-bundled path so wheel installs work without rebuilding.Dispatcher silently dropped
level_shiftwhen translating betweenPeriodicSCFOptions↔PeriodicRHFOptionsinperiodic_rhf_dispatch._copy_options_to_rhf/_copy_options_to_scf. Symptom: a user settingopts.level_shift = 0.3and callingvq.run_rhf_periodic_gamma_scfsaw the same SCF trajectory asopts.level_shift = 0.0. Now copied viagetattr(opts, "level_shift", 0.0)for forward compatibility.Documentation API drift. Tutorials 04 / 05 / 12 / 14 / 17 + user-guide
k_points.md/properties.md/ase_integration.mdmigrated from the legacy direct entry points (run_rhf_periodic/run_rks_periodic) to the recommended dispatcher API.docs/user_guide/molecules.mdupdated to use property-style access formol.atoms/mol.charge/mol.multiplicitymatching the current API.LatticeMatrixSet
.blocksmutation footgun. The pybind11 binding for.blocksreturns a fresh Python list each access (default forstd::vector<MatrixXd>), so element assignment silently no-ops at the C++ level. Phase 15b added explicit :meth:set_block(i, M)to mutate in place and updated all callers; the.blocksgetter docstring now warns about the copy semantics.Functional dtor crash when the ctor throws partway through init.
Functional::Impl::funcswas resized up front and filled in a loop byxc_func_init; when a later family check rejected the functional (e.g. MGGA / HYB_MGGA), the destructor still ranxc_func_endon every slot, including the zero-filled slots that had never been initialised. Dtor now skips slots that were never inited. Was the real cause of the meta-GGA “crash” flagged in the DFT-functional-comparison tutorial.Reject empty BasisSet to avoid silent downstream segfault. libint2 silently returns a zero-shell BasisSet when it cannot find a matching
.g94file. vibe-qc now raisesRuntimeErrorwith a directive message rather than handing the empty object to integral kernels and crashing without a Python traceback.
Limitations¶
EWALD_3D KS multi-k for hybrids has not been benchmarked against CRYSTAL on real ionic crystals. The molecular-limit ω-invariance and multi-k-vs-Γ-equivalence witnesses pass; bulk validation is the next milestone-gating item.
Saunders-Dovesi multipolar splitting (Phase 12e-c-3c) is not implemented. Tight Gaussian cores (e.g. STO-3G O 1s, α ≈ 130) are not fully resolved on a 0.3-bohr FFT grid; the H₂O ω-invariance test in 12e-c-4a is xfailed pending S-D.
UKS smearing isn’t shipped. Multi-k UKS Ewald accepts the
smearing_temperaturefield onPeriodicKSOptionsbut currently ignores it.No molecular level-shift yet. The
level_shiftfield exists onPeriodicRHFOptions/PeriodicKSOptionsbut not on the molecularRHFOptions/UHFOptions/RKSOptions/UKSOptions(Phase C1a-2 follow-up).CRYSTAL parser reads basis blocks but not the ECP block (Phase 14d follow-up).
[Unreleased — pre-v0.4.0 history]¶
The CHANGELOG entries below document work that landed on main
during the v0.4.0 development window. They are kept for historical
context; their content is rolled up in the v0.4.0 entry above.
Added (legacy)¶
Quickstart page split + tour rename.
docs/quickstart.mdis now a focused 4-step “first 30 minutes” walkthrough (molecular HF → open-shell UHF → 3D periodic SCF via the EWALD_3D dispatcher → orbital cube file) backed byexamples/quickstart.py— total wall under 1 minute on a single core, demonstrates that the out-of-the-boxpip install vibe-qcuser experience needs zero setup. The previous “tour”-style content moved todocs/tour.mdwith a top-of-file pointer for newcomers. The toctree under “Getting started” now listsinstallation→quickstart→tour→tutorial/index.6 new tutorials (20–25) plus the NEB animation for tutorial 19, all wired into
docs/tutorial/index.md:20 — natural orbitals + idempotency diagnostic.
21 — projected density of states (PDOS).
22 — periodic Bloch-orbital cubes via Phase V3 writers.
23 — tight-cell DFT with the periodic Becke partition (uses Phase 12f’s
vq.build_periodic_becke_grid/PeriodicKSOptions.use_periodic_becke).24 — periodic SCF convergence (level shift, DIIS, damping; uses Phase C1a’s
opts.level_shiftand the new C1bopts.smearing_temperature).25 — symmetry-aware storage of lattice integrals (Phase SYM3a’s
compute_overlap_lattice_with_orbitsfamily).
docs/user_guide/ewald.mdrewrite against the shipped EWALD_3D dispatcher API (run_rhf_periodic_scf/run_rhf_periodic_gamma_scfinstead of the barerun_rhf_periodic_gamma_ewald3dbackend the prior version documented).Resource callouts on every tutorial. Tutorials 01–19 now carry standardised peak-RAM + wall-time figures (Apple M2 baseline, calibrated via the new
scripts/measure_tutorial_resources.pyprobe). Tutorials 20–25 already carried Resources sections from when they shipped.examples/README.mdgains a top-of-file resource-expectations table covering the example classes by ballpark.bug-7 doc note in
docs/user_guide/molecules.md— “Configuring open-shell systems” section explains that vibe-qc’s spin information lives onMolecule.multiplicity, not on the SCF-driver options classes (which deliberately do not expose aspinfield). Closes the doc-note follow-up the engineering chat agreed to defer in the v0.4 bug sweep.docs/release_process.mdregistered in the toctree under the “Project” caption alongside changelog / contributing / license. Sphinx-Wbuild now clean of orphan-document warnings.
Fixed¶
Dispatcher silently dropped
level_shiftwhen translating betweenPeriodicSCFOptions↔PeriodicRHFOptionsinperiodic_rhf_dispatch._copy_options_to_rhf/_copy_options_to_scf. Both helpers copied 9 fields by hand and forgot the C1a-introducedlevel_shift. Symptom: a user settingopts.level_shift = 0.3and callingvq.run_rhf_periodic_gamma_scfsaw the same SCF trajectory asopts.level_shift = 0.0because the freshPeriodicRHFOptionsconstructed for the Ewald backend lost the field. Now copied viagetattr(opts, "level_shift", 0.0)for forward compatibility. Test gap closed by adding dispatcher-level coverage (the prior tests exercised only the bare backends).Documentation API drift. Tutorials 04 / 05 / 12 / 14 / 17 + user-guide
k_points.md/properties.md/ase_integration.mdmigrated from the legacy direct entry points (run_rhf_periodic/run_rks_periodic) to the recommended dispatcher API (run_rhf_periodic_scf/run_rks_periodic_scf) shipped in Phase 12e-c-4 / Phase 15.docs/user_guide/molecules.mdupdated to use property-style access formol.atoms/mol.charge/mol.multiplicitymatching the current API.examples/plots/level-shift-scf-trace.pydrops the deadisinstance(it, tuple)defensive branch now thatscf_traceitems are uniformlySCFIterationacross every backend.Phase 12e-c-4c — multi-k Ewald-3D periodic RHF (
-ithrough-iv). Closes out the bulk-crystal pieces ofEWALD_3Din four sub-commits.-ilifts the long-range Hartree builder from molecular-limit to proper bulk: :func:vibeqc.evaluate_periodic_density_on_gridand :func:vibeqc.build_j_long_range_periodicevaluate the full lattice sumρ(r) = Σ_g Σ_{μν} D(g)_{μν} χ_μ(r − R_g) χ_ν(r)on a uniform FFT grid and return either a Γ-only J matrix or per-cell J(g) blocks.-iilands Pulay DIIS inrun_rhf_periodic_gamma_ewald3d(commutator-error rolling history, numerical-singular fallback, default subspace 8).-iii-aships :func:vibeqc.build_periodic_fock_ewald3d_k, the multi-k Fock builder that returns one F(k) per k-point with proper Bloch-summed J_LR.-iii-bwraps it in :func:vibeqc.run_rhf_periodic_multi_k_ewald3d, a closed-shell multi-k SCF driver returning :class:vibeqc.PeriodicRHFMultiKEwaldResult; validated at[1,1,1]against the Γ driver to ~1e-13 Ha and at[2,2,2]for non-trivial k-space density.-ivshipspython/vibeqc/madelung.pywith eight helpers (:func:vibeqc.cell_electron_charge, :func:vibeqc.cell_nuclear_charge, :func:vibeqc.cell_net_charge, :func:vibeqc.cubic_cell_edge, :func:vibeqc.madelung_alpha, :func:vibeqc.madelung_correction_scalar, :func:vibeqc.apply_madelung_correction, :func:vibeqc.apply_madelung_correction_per_k) that expose the G=0 gauge cancellation between the electronic Poisson solver and the nuclear point-charge sum. For neutral crystals the net α vanishes by construction; charged cells get anα·Sshift restoring the isolated-system limit. 28 new tests across the four sub-commits.Phase 12e-c-4b — Γ-point periodic RHF SCF using EWALD_3D. :func:
vibeqc.run_rhf_periodic_gamma_ewald3dreturns :class:vibeqc.PeriodicRHFEwaldResult: the molecular-limit closed-shell Γ-only driver wired through the composed Ewald J of Phase 12e-c-4a. Hcore from Bloch-summed T + V lattice integrals; canonical orthogonalisation (1e-7 threshold); Hcore initial guess; Fock buildF = Hcore + J_ewald(ω, D) − ½ Kwith K from the ω=0 real-space exchange (full-range, standard periodic-HF practice). ω-invariance over ω ∈ {0.3, 0.5, 1.0, 1.5} is < 0.5 % of |E|;E_ewald − E_direct = ½·α·n_electronswith α the cubic Makov-Payne coefficient, so the gauge shift propagates cleanly through SCF. 7 new tests.Phase 12e-c-4a — composed Ewald-3D Hartree J + ω-invariance validation. :func:
vibeqc.build_j_ewald_3dstitches the erfc short-range J (12e-c-2) and the FFT long-range J (12e-c-3b) into a single periodic Hartree builder;J_SR(ω) + J_LR(ω)is ω-invariant up to numerical precision. Helper :func:vibeqc.makov_payne_coefficient_cubicreturns the scalar-times-overlap shift between the composed periodic J and the isolated-molecule J for cubic cells. ω-invariance on H₂ at two box sizes < 0.5 %; H₂O xfailed (STO-3G O 1s core not resolved on a 0.3-bohr grid — Phase 12e-c-3c Saunders–Dovesi multipolar splitting will fix this). 8 new tests.Phase 12e-c-3 — long-range Hartree J via FFT Poisson convolution. Two sub-commits.
-3aadds FFTW3 as a build dependency (find_package + Homebrew/apt/conda install instructions in the installation guide) and ships :func:vibeqc.solve_poisson_erf_screened, :func:vibeqc.solve_poisson_coulomb, :class:vibeqc.ScalarField3D, and supporting helpers — the reciprocal-space Poisson solver (orthorhombic for now) for the erf-screened Coulomb kernelṼ(G) = (4π / G²) · exp(−G²/(4ω²)) · ρ̃(G).-3bships :func:vibeqc.build_j_long_rangeand :func:vibeqc.auto_grid— the long-range J_LR builder via density-on-grid sampling, FFT convolution, and AO-pair re-integration. The G=0 gauge (V(G=0) ≡ 0 in the Poisson solver) leaves J_LR alone differing from the isolated J_full by a Makov-Paynec·Sshift; the shift cancels in the composed Ewald-3D J of 12e-c-4a. 11 + 9 new tests.Phase SYM2c — atom-pair-resolved orbits for non-origin-fixed structures. Generalises SYM2b’s lattice-block compression to ionic crystals like NaCl / MgO / CsCl / ZnS where some atom sits at a non-origin Wyckoff position and picks up lattice shifts under most operators. New module
python/vibeqc/symmetry_lattice_c.py: :class:vibeqc.AtomPairOrbit, :class:vibeqc.AtomPairOrbits, :func:vibeqc.identify_atom_pair_orbits, :func:vibeqc.compress_lattice_matrix_set_c, :func:vibeqc.reconstruct_lattice_matrix_set_c. Orbits are now triples(source_atom, dest_atom, cell_index)under the action(a, b, h) ↦ (π(a), π(b), R·h + s_a − s_b); reconstruction formulaF^{(π(a),π(b))}(R·h + s_a − s_b) = D_a(R) · F^{(a,b)}(h) · D_b(R)^T. Validated on NaCl (see the new symmetry example).Phase SYM2b — lattice-cell orbit identification + LatticeMatrixSet compression. :class:
vibeqc.LatticeOrbit, :class:vibeqc.LatticeOrbits, :func:vibeqc.identify_lattice_orbits, :func:vibeqc.compress_lattice_matrix_set, :func:vibeqc.reconstruct_lattice_matrix_set, :func:vibeqc.lattice_to_cartesian_rotation— partition the real-space lattice cell list into orbits underh ↦ R·h, store one representative per orbit, and reconstruct other members on demand via SYM2a AO-permutation matrices. Origin-fixed-atom case only; SYM2c handles the general one. Order-of-|G| memory and compute reduction on real-space matrix blocks for high-symmetry cells.Phase SYM2a — AO-basis representation of a symmetry operator. New module
python/vibeqc/symmetry_ao.py: :func:vibeqc.atom_permutation_under_op, :class:vibeqc.AtomPermutation, :func:vibeqc.build_ao_permutation_matrix. Composes the SYM1 Wigner D-matrices with the per-atom permutation induced by the operator into the full(n_bf, n_bf)AO permutation matrixP(R)such that an AO coefficient vector rotates asv' = P · v. Caches D^l per l so a basis with many shells of the same l incurs the Wigner work only once.Phase SYM1 — real-basis Wigner D-matrices for AO rotation. Foundational v0.2.5 (symmetry) machinery. Pure Python / numpy module
python/vibeqc/symmetry_core.pywith :func:vibeqc.wigner_d_real, :func:vibeqc.wigner_d_complex, :func:vibeqc.wigner_small_d, :func:vibeqc.euler_angles_from_rotation, :func:vibeqc.real_spherical_to_complex_unitary. Pipeline:R → ZYZ Euler angles → complex Wigner D → real-basis transform via a Condon-Shortley unitary. Initial release covered proper rotations only; a follow-up commit adds improper rotations (reflections, inversion, rotoreflections; det R = −1) via the factorisationD^l(R_improper) = (−1)^l · D^l(−R_improper), unlocking the full O(3) coverage that real point groups need. 98 new tests cover Wigner identitiesD(R₁R₂) = D(R₁)D(R₂), orthogonality, gimbal-lock cases, and small known operators.Phase D1 — DFT-D3(BJ) dispersion correction. Three-commit rollout.
D1aships the C++ framework (cpp/include/vibeqc/dispersion.hpp,cpp/src/dispersion{,_data,_params}.cpp): :class:vibeqc.D3BJParams, :class:vibeqc.DispersionResult, :func:vibeqc.compute_d3bj, :func:vibeqc.d3bj_params_for, :func:vibeqc.d3_coordination_numbers, :func:vibeqc.d3_r2r4, :func:vibeqc.d3_rcov. Coordination numbers + analytical Jacobian, BJ-damped pairwise summation, geometric gradient, starter set of 10 functionals’ damping parameters.D1badds the referencedftd3Python backend as an optional dependency (pip install 'vibe-qc[dispersion]') — Grimme’s full CN-dependent C6 grid and ~160 functionals’ parameters via the reference Fortran implementation; routing is automatic viabackend="auto", with the C++ backend as a complete-without-the- optional-dep fallback. New :func:vibeqc.dftd3_availableprobe.D1cwires D3(BJ) through the user-facing drivers:run_job(..., dispersion=...)acceptsTrue(inherit the SCF functional), a functional name ("pbe","b3lyp", …), or aD3BJParamsstruct. The.outfile grows a “Dispersion correction (D3-BJ)” block (s6/s8/a1/a2, E_disp in Ha and kcal/mol, E_SCF, E_total). ASE calculator gets a matching kwarg; geometry optimisation walks the dispersion-corrected PES. 16 + 9 + 12 new tests.Phase P1.2 — close remaining OpenMP parallelism gaps. Audit of every C++ kernel against the engine-per-thread + thread-local- accumulator pattern from P1 / P1.1; five gaps closed:
compute_dipole(no longer “overkill” once larger basis sets are in play), the AO→MO transform and energy accumulation inrun_mp2andrun_ump2(parallel-for on the GEMM outer loopscollapse(2) reduction on the MP2 energy sum), and the three per-cell loops in
build_xc_periodic(per-cell-independent GEMMsper-thread Eigen buffer reduction for the rho/grad-rho accumulator). With this, every compute-heavy kernel vibe-qc ships is OpenMP-parallel.
Canonical orthogonalisation for linearly-dependent AO bases. Replaces the plain
S^{−1/2}symmetric orthogonaliser in the RHF / UHF / RKS / UKS drivers with a rectangular canonical orthogonaliser that projects out near-null overlap eigenvectors before Fock diagonalisation. Well-conditioned bases unchanged (1e-10 Ha agreement); near-linearly-dependent bases (e.g. H₂ at 0.5 bohr with aug-cc-pVTZ, min S eig ~ 1.9e-7) now converge cleanly instead of throwingRuntimeError: AO basis is linearly dependent. New shared helper :class:vibeqc.CanonicalOrthogonalizerand :func:vibeqc.canonical_orthogonalizer. MO coefficient matrix shape becomes(n_basis, n_kept)withn_kept ≤ n_basis; downstream consumers (gradient, MP2, properties) absorb the reduction transparently. New exampleexamples/input-h2-tight-canonical-orth.py.Pre-flight linear-dependence diagnostic for the AO basis. New :func:
vibeqc.check_linear_dependence, :class:vibeqc.LinearDependenceReport, :class:vibeqc.LinearDependenceOffender, :func:vibeqc.format_linear_dependence_report, :func:vibeqc.raise_if_severe, :class:vibeqc.LinearDependenceError. Inspects the overlap matrix, reports near-null eigenvalues plus the basis functions responsible (atom, shell, ℓ, exponent, weight), and renders a human-readable report in the same style as the memory pre-flight. Optional hard fail above a user-chosen severity threshold. Diagnostic only — the remediation path is the canonical-orthogonalisation pass above.Phases 18 + 19 — atomic charges, bond orders, dipole moment. Every
run_job.outfile now appends a properties block with Mulliken and Löwdin atomic charges, Mayer bond orders (filtered to pairs withB_AB ≥ 0.10for compactness), and the molecular dipole moment in both atomic units (e·bohr) and Debye, with the centre of mass as the default origin. Public Python API: :func:vibeqc.mulliken_charges, :func:vibeqc.loewdin_charges, :func:vibeqc.mayer_bond_orders, :func:vibeqc.dipole_moment(returning :class:vibeqc.DipoleMoment), :func:vibeqc.center_of_mass, and the lower-level :func:vibeqc.compute_dipole/ :class:vibeqc.DipoleIntegrals.format_scf_tracegrowsbasis=andinclude_properties=keywords. Verified against PySCF: Mulliken charges and dipole-moment components on H₂O / 6-31G* agree to better than 1e-6. 17 new tests; full suite 381 passing.Phase P1.1 — gradient parallelism + thread-count control + timing reports. All five shell-quartet loops in
cpp/src/gradient.cpp(overlap, kinetic, nuclear, two-electron RHF, two-electron UHF) now run under OpenMP via the thread-local-gradient-with-post-reduce pattern — closing the remaining P1 gap so analytic gradients scale on multi-core hardware. New public API: :func:vibeqc.get_num_threads, :func:vibeqc.set_num_threads(withn <= 0meaning “restore default” — eitherOMP_NUM_THREADSor the hardware core count).run_jobgrows anum_threads=keyword; every.outfile records the actual thread count used plus a wall-clock timing block summarising SCF total, per-iteration average, geometry-optimisation time (if any), and job total. 7 new tests cover the thread API and timing.Phase 12e-c-2 — erfc-screened ERIs for the short-range Ewald J/K.
build_jk_gamma_molecular_limitandbuild_fock_2e_real_spacegrow an optionalomegaparameter (default0.0). When positive, the builders swap libint’sOperator::coulombforOperator::erfc_coulombwith screening parameterω, producing the short-range piece of the Ewald split for 3D bulk. Matching long-range piece (reciprocal-space Hartree, FFTW-accelerated) lands in 12e-c-3. 10 new tests cover the ω → 0 / ω → ∞ limits, monotone decrease in ω, and the J(0) = J_short(ω) + J_long(ω) decomposition consistency.Phase 12e-c-1 — Gaussian-charge Ewald for V(g) via grid integration. First sub-phase of the full 3D Ewald stack. New public API: :func:
vibeqc.ewald_point_charge_potential(Ewald-summed Coulomb potential of a periodic lattice of point charges, with optional short/long-range decomposition), :func:vibeqc.ewald_nuclear_potential(convenience wrapper for electronic attraction), and :func:vibeqc.compute_nuclear_lattice_ewald(fullV_μν(g)via analytical erfc short-range (libint’s erfc_nuclear, Phase 12e-b) plus numerical grid integration of the smooth erf long-range part). α-invariant to ~1e-4 Ha across the working range of α. Not yet dispatched bycompute_nuclear_lattice; callers that want the Ewald V must invoke it directly. FullEWALD_3Ddispatch with the ERI Coulomb (J-term) Ewald, Saunders–Dovesi multipolar splitting, and FFTW3 acceleration follow in 12e-c-2 and 12e-c-3.Phase P2 — memory estimator + pre-flight abort. Every compute-heavy driver now surfaces an upper-bound peak-memory estimate before the SCF starts, printed to the run log as
vibeqc estimates this calculation will require ~12.4 GB of memory: ERI tensor 11.6 GB ... Available on this machine: 119.8 GB. Proceeding.
When the estimate exceeds available RAM,
run_jobaborts with :class:vibeqc.InsufficientMemoryError; users can setmemory_override=Trueon therun_jobcall to proceed anyway (the block then showsProceeding (override)). New public API: :class:vibeqc.MemoryEstimate, :class:vibeqc.InsufficientMemoryError, :func:vibeqc.estimate_memory, :func:vibeqc.check_memory, :func:vibeqc.available_memory_bytes, :func:vibeqc.format_memory_report. Estimators cover RHF / UHF / RKS / UKS / MP2 / UMP2; theavailable_memory_bytesprobe tries psutil first (optional dep), then falls back to/proc/meminfo(Linux) andos.sysconf(macOS). 22 new tests.Phase P1 — OpenMP shared-memory parallelism. Single-node multithreading lands on every compute-heavy kernel: molecular 1e and 2e integrals (
compute_overlap,compute_kinetic,compute_nuclear,compute_eri), the molecular Fock builder (build_fock_g,build_coulomb,build_exchange), periodic one-electron lattice sums (compute_overlap_lattice,compute_kinetic_lattice,compute_nuclear_lattice,compute_nuclear_erfc_lattice), the Γ-only and multi-k periodic Fock builds (build_jk_gamma_molecular_limit,build_fock_2e_real_space), the Ewald real-space and reciprocal-space loops, and AO evaluation on DFT grids (evaluate_ao,evaluate_ao_with_gradient,evaluate_ao_with_hessian). Thread-safe via a one-engine-per-thread pool incpp/include/vibeqc/thread_pool.hpp. Controlled byOMP_NUM_THREADS; no API change, all 320 regression tests pass unchanged. Benchmark harness inscripts/bench.pyreports scaling across a sweep of thread counts. Analytic gradients and the SAD initial guess remain single-threaded for now — a follow-on P1.1 pass.Phase 12e-a — classical Ewald for point-charge Madelung energies. New public API:
vibeqc.EwaldOptions,vibeqc.ewald_point_charge_energy,vibeqc.ewald_nuclear_repulsion.CoulombMethod.EWALD_3Ddispatchesnuclear_repulsion_per_cellthrough the Ewald engine. Validated against literature Madelung constants for NaCl (1e-8), CsCl (1e-6), ZnS (1e-4), and simple-cubic jellium (1e-6); α-invariance holds to 1e-9 across α ∈ {0.2, 0.3, 0.5, 1.0}. Seedocs/user_guide/ewald.md.Phase 12e-b — erfc-screened nuclear-attraction lattice sum as an Ewald building block. New public API:
vibeqc.compute_nuclear_erfc_lattice(basis, system, omega, options). Wraps libint’sOperator::erfc_nuclear; exponentially convergent real-space sum for any ω > 0. Used by Phase 12e-c to assemble the fullEWALD_3Ddispatch ofcompute_nuclear_lattice.Documentation — new
docs/user_guide/ewald.mdwith math, references, and usage examples; API index updated with the new symbols and the Version & banner section added; roadmap and feature matrix reflect 12e-a/b shipped and the 12e-c / 12f / v0.3.0+ work ahead.
Fixed¶
Reject empty BasisSet to avoid silent downstream segfault.
libint2silently returns a zero-shellBasisSetwhen it cannot find a matching.g94file underLIBINT_DATA_PATHfor the requested name (a real example: the diffuse-augmented Pople6-311++g**, missing from the shipped data directory). vibe-qc then handed that empty object to the integral kernels and crashed with no Python traceback. Now raisesRuntimeErrorwith a directive message naming the basis and pointing the user at fixing the spelling, using the canonical libint/BSE form, or dropping a file intobasis_library/custom/.Functional dtor crash when the ctor throws partway through init.
Functional::Impl::funcswas resized up front and filled in a loop byxc_func_init; when a later family check rejected the functional (e.g. MGGA / HYB_MGGA), the destructor still ranxc_func_endon every slot, including the zero-filled slots that had never been initialised — null-pointer deref inside libxc, hard segfault, no traceback. This was the real cause of the meta-GGA “crash” flagged in the DFT-functional-comparison tutorial. Dtor now skips slots that were never inited.
Limitations¶
EWALD_3Dend-to-end ships in this[Unreleased]block but is not yet validated against CRYSTAL on real ionic crystals (LiH, NaCl, MgO, Si). The molecular-limit and ω-invariance witnesses pass; the CRYSTAL benchmark pass is the gating item for the v0.2.0 tag.The Saunders–Dovesi multipolar splitting (Phase 12e-c-3c) is not yet implemented. Tight Gaussian cores (e.g. STO-3G O 1s, α ≈ 130) are not resolved on a 0.3-bohr FFT grid; the H₂O ω-invariance test in 12e-c-4a is xfailed pending S-D.
The 12e-c-4c-iv Madelung-cancellation helpers ship as a toolkit but are not auto-applied by the multi-k Ewald driver; charged-cell callers must call them explicitly.
Phase 12f periodic Becke partition is not yet shipped — periodic DFT integration in tight crystals can still see image-atom Voronoi cells intrude on the reference unit cell.
[0.1.0] — 2026-04-18¶
First tagged release. All subsequent releases will follow semantic
versioning (pre-1.0: any minor bump may break API; 0.2.0, 0.3.0,
… mark milestone achievements).
Added¶
Runtime banner printed at the top of SCF-trace output — shows vibe-qc version, MPL 2.0 attribution, and linked library versions (libint, libxc, spglib). Exposed as
vibeqc.banner(),vibeqc.print_banner(),vibeqc.library_versions().Documentation site (Sphinx + Furo + MyST) with installation guide, tutorial, feature matrix, roadmap, and autogenerated API reference.
MPL 2.0 license at the repository root.
CRYSTAL-format basis-set parser and NWChem/.g94 emitter (
python/vibeqc/basis_crystal.py) for per-element CRYSTAL basis files.Bredow-group pob- basis sets* fetched into
basis_library/custom/: pob-TZVP (H–Br), pob-TZVP-rev2 (H–Br), pob-DZVP-rev2 (subset).Phase 12d — multi-k periodic Kohn-Sham DFT (
run_rks_periodic), LDA and pure GGA. Validated against molecular RKS in the molecular limit across dim ∈ {1, 2, 3} × {LDA, PBE, BLYP}.Phase 12c — multi-k periodic RHF (
run_rhf_periodic) with real-space density matrix and the general three-lattice-index direct-SCF Fock build.Phase 12b — Γ-only periodic RHF (
run_rhf_periodic_gamma) in the molecular-limit regime.Phase 12a — periodic one-electron infrastructure:
PeriodicSystem, lattice-summed S/T/V integrals, Bloch sums, Monkhorst-Pack k-mesh, IBZ reduction via spglib.spglib integration (
vibeqc.Crystal,analyze_symmetry,to_primitive) + minimal VASP 5 POSCAR I/O.UMP2 — open-shell MP2 on a UHF reference, three spin channels.
RMP2 — closed-shell MP2 on an RHF reference.
Known limitations (being addressed)¶
3D bulk Coulomb lattice sum is currently direct-truncated (conditional convergence). Proper Ewald splitting lands in Phase 12e.
Periodic DFT uses the molecular Becke weight partition; image-atom Voronoi cells may intrude on the reference unit cell for tight crystals. Proper periodic Becke partition lands in Phase 12f.
ECP basis sets (pob-* for Rb–I, Cs–Po, La–Lu) parse but cannot be used: applying them needs libecpint integration.
Hybrid periodic DFT implemented in the code path (via
exchange_scale) but not yet validated end-to-end.Periodic UKS not yet available.