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 (vqfetch CLI; pulls crystal & molecular structures from OPTIMADE / Materials Project / NOMAD / COD into ready-to-run vibe-qc inputs with full citation provenance — design contract on claude/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 returned status='unavailable' for every DF case; v0.7.2 landed the density_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_case now sets opts.density_fit = True + opts.aux_basis = <jk aux> whenever MethodSpec.df is True, instead of returning early with status='unavailable'.

  • _resolve_aux_basis(method, basis_name, kind) helper picks the aux basis: prefer MethodSpec.aux_basis if set, else fall back to vq.default_aux_basis_for(basis_name, kind=...). vibe-qc’s bindings raise on density_fit=True + empty aux_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 the RIFIT family (def2-svp-rifit, etc.) — different from the SCF’s JKFIT — so it’s re-resolved with kind='ri'. Both vq.run_mp2 and vq.run_ump2 now take this options arg (matches the v0.7.2 binding signatures).

  • Verbose log records the active aux basis next to the format_options dump so each DF case’s run is reproducible from its .log file 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 single error row 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 DensityFitting object that holds the ((PQ)|(rs))^{-½} fit and the 3-centre coefficient tensor.

  • Aux-basis librarypython/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 DFdensity_fit: bool = False + aux_basis: str = "" on RHFOptions, 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 DFcompute_gradient / compute_gradient_rks accept a GradientOptions struct 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 reorganisedexamples/ split into molecular/, 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 own subprocess.run invocation. C-level crashes (PySCF FFTDF buffer segfault, MemoryError, ORCA exit-55, libxc abort) are caught at the parent and surfaced as error rows; 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 ssh section in examples/regression/README.md documents the loginctl enable-linger + systemd-run --user --unit=... pattern. Without it, ssh-spawned nohup / setsid background 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.py is 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 emits status='unavailable' for DF method ids — needs a follow-up patch to actually call the new density_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-way vibe-qc + PySCF + ORCA cross-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). No python/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 existing h2, h2o, ne_atom from 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 methods rhf-df, rks-lda-df, rks-pbe-df, rks-b3lyp-df, mp2-df. PySCF runner enables DF via mf.density_fit(auxbasis=...); ORCA runner injects RIJK def2/JK into the simple-input. vibe-qc has no DF API — DF cases emit status='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 in runner_vibeqc.run_molecule_case needs 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 the vibe-qc + PySCF + ORCA triangle. Threading is a one-line wiring change in run_one_molecule_case_dev when needed.

  • MP2 / UMP2 in vibe-qc runner: post-SCF correlation via vq.run_mp2(mol, basis, rhf_result) and vq.run_ump2(mol, basis, uhf_result); row energy reports E_total so cross-code Δ comparisons line up with PySCF (mp2.e_tot) and ORCA (MP2 / RI-MP2 simple-input).

Changed — cross-code convention alignment

  • ORCA B3LYP/G instead of B3LYP for hybrid DFT cases. ORCA’s bare B3LYP uses VWN5; PySCF’s b3lyp and vibe-qc’s B3LYP use VWN3 (the Gaussian convention). Pinning ORCA to B3LYP/G collapses the cross-code Δ from ~36 mHa (convention mismatch) to ~10⁻⁴ Ha (the inherent XC-grid noise floor).

  • ORCA NoFrozenCore for 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_MAP gains "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 to examples/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.py is 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 into ok/warn/error/critical severity tiers. critical (non-PSD S) raises LinearDependenceError by default with a citation-rich error message recommending pob-TZVP / pob-TZVP-rev2 first, vq.make_basis(..., exp_to_discard=0.1) second, and allow_critical=True only as a deliberate last resort.

  • vq.eigs_preflight(system, basis, kmesh) — CRYSTAL’s EIGS keyword equivalent. Builds S(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 or exp_to_discard);

    • screening_undertight: under-converged exchange screening (fix: tighten cutoff_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=True default ON in all 8 SCF drivers — bisects cutoff_bohr and schwarz_threshold JOINTLY (per the user’s directive) to find the loosest combination that keeps S PSD 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 (mirrors pyscf.pbc.gto.Cell.exp_to_discard). Drops every Gaussian primitive with exponent below the threshold, caches the synthesised .g94 so 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 transparencyvq.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 raw S, so it has its intended meaning regardless of per-AO scale (Lykos & Schmeising 1961, Löwdin 1970 conventions). Default ON; legacy raw-S path available via =False.

  • Opt-in orthogonalisation methodsvq.orthogonalise_overlap(S, method="auto") with explicit canonical_orth, pivoted_cholesky_orth (Lehtola 2019 + 2020), symmetric_orth available. 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 dumpvq.format_options(opts) and vq.dump_active_settings(plog, groups) polymorphically print every pybind-bound options struct, dataclass, or dict as a stable key = value summary. 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_periodic is the new helper; build_j_long_range accepts an optional system kwarg to opt into the periodic AO sum, and build_j_ewald_3d always threads it through. Same plumbing in python/vibeqc/periodic_density.py::evaluate_periodic_density_on_grid and build_j_long_range_periodic for 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)^3 grid; 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.837 for simple-cubic. Diagnosis tracked in tests/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 libomp gives the headers but CMake’s FindOpenMP needs OpenMP_ROOT pointed at $(brew --prefix libomp) for the spglib configure to succeed. Auto-detected on main and on release (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.cff at 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.mdvibe-up / vibe-down aliases for activating/deactivating the venv, and a vibe-update zsh 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 — historical british-english-check job (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 — added sphinx-design>=0.6 to the docs-build pip install so the grid-item-card directives 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:

  • E or grad becomes 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_correction with the cell-dependent Madelung from pyscf.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_guess HCORE → 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_ewald3d honour opts.initial_guess = InitialGuess.SAD. Multi-k drivers seed the per-k MOs from Hcore (for trace continuity) then overwrite D_real with SAD on the unit cell at g=0 (zero elsewhere — molecular-limit convention). Each driver emits a plog.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_guess for periodic options remains HCORESAD is opt-in via opts.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_guess from HCORE to SAD.

  • 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_ewald3d only).

  • 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_contribution family) operate on a LatticeMatrixSet density. Returns (n_atoms, 3) Ha/bohr.

  • G1b — Γ-only RKS gradient (compute_gradient_periodic_rks_gamma). Same HF skeleton plus an inline _xc_pulay_molecular_fallback for 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 a LatticeMatrixSet and fed to the same C++ primitives.

  • G1d — open-shell UKS multi-k gradient (compute_gradient_periodic_uks_multi_k). Pure-DFT only (α_HF = 0); raises NotImplementedError for 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 (default 1e-14, 100× tighter than schwarz_threshold because plain Schwarz on derivatives is non-rigorous, matching CP2K’s EPS_SCHWARZ_FORCES convention). 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. Setting schwarz_threshold_forces = 0.0 reproduces the historical disagreement and is the regression handle for the actual root-cause fix tracked at G1a-2.

  • Banner print at SCF entryrun_rhf_periodic_scf, run_rks_periodic_scf, run_rks_periodic_gamma_scf emit vibe-qc <version> <codename> before the SCF header so users can verify which build is running just from the .out file.

  • G1d exportcompute_gradient_periodic_uks_multi_k is now reachable from the top-level vibeqc.* 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.6] — 2026-04-30 — Wilson’s Otter (patch) — cell-level Schwarz + SCF divergence exit + banner

Performance + ergonomics follow-up to v0.5.5. Patch releases inherit their parent minor’s codename. Three landings, all behind existing defaults so the upgrade is drop-in.

Performance — cell-level Cauchy-Schwarz screening

v0.5.5 introduced per-quartet Schwarz on the periodic 2-e Fock build: each shell quartet × cell triple is bounded by

| ⟨μ_0 ν_g | λ_λ σ_σ⟩ | ≤ Q[c_g][μ,ν] · Q[c_σ−c_λ][λ,σ]

and skipped if the bound × D_max falls below the threshold. That cut two-three orders of magnitude on real crystals. v0.5.6 lifts the check outside the inner shell-quartet loop:

Q_max[c] = max over (s_a, s_b) of Q[c][s_a, s_b]

and for each (c_g, c_λ, c_σ) cell triple checks Q_max[c_g] · Q_max[c_h] · D_max < threshold before iterating any of the n_shells⁴ quartets. On systems like the 8-atom NaCl conventional cell with 72 BFs / 40 shells, this cuts another 10-100× on top of v0.5.5 by short-circuiting whole cell triples that have negligible shell-pair overlap. Wired into both build_fock_2e_real_space (multi-k Ewald short-range J + full-range K) and build_jk_gamma_molecular_limit (Γ-only molecular-limit J/K).

Shared Schwarz utilities are now in cpp/include/vibeqc/schwarz.hpp

  • cpp/src/schwarz.cpp so future SCF-builder additions can opt in with a single include.

Ergonomics — SCF divergence detection

Per a user report on NaCl/STO-3G/Γ-only RKS-LDA where the SCF was oscillating with +33716 / -16268 / +41422 / -16350 Ha and running to max_iter before hitting an index error in a downstream property calculation. run_rks_periodic_gamma_ewald3d now exits with a RuntimeError carrying a remediation hint when:

  • E or ||[F, DS]|| becomes NaN / Inf,

  • |E| > 1e6 Ha (unphysical magnitude — Hcore guess broken),

  • iter >= 5 and |dE| > 1e4 Ha (oscillating wildly).

Each error suggests a fix: stronger damping, tighter Ewald, or the v0.6.x SAD initial guess (filed as a flagship v0.6.x feature; see docs/roadmap.md).

Ergonomics — banner print at SCF entry

run_rhf_periodic_scf, run_rks_periodic_scf, and run_rks_periodic_gamma_scf now emit vibe-qc <version> <codename> as the first line of progress output, so users (and tail -f on a running .out) can verify which build is actually running without running a separate vibeqc.print_banner() call. Resolved via the existing codename_for_version lookup so patches inherit their parent minor’s codename.

Compatibility

All defaults unchanged. Existing scripts run faster on real crystals without code changes. The divergence detection only fires on genuinely-broken SCFs that previously ran to max_iter without producing useful output anyway.

[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:

    1. API surfaceLatticeSumOptions.schwarz_threshold is a writable float, default 1e-12.

    2. 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).

    3. Correctness — screening on (1e-12) and off (0.0) give the same SCF energy to within the convergence floor (1e-5 for this fixture), confirming the threshold is well inside production-quality SCF accuracy.

Tooling

  • pyproject.toml gains [diagnostics] and [dev] extras, both carrying py-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 an scf_trace into 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. Default show_diis=True matches what most users want when scanning a stuck SCF; pass show_diis=False for slide-deck cleanliness. PySCF’s scf.diis_plot equivalent for vibe-qc.

  • save=None (the default) returns the matplotlib.figure.Figure so 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 to 1e-10 bohr, 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 .system manifest now carries [run].fingerprint = "abc1234..." alongside the existing timestamp_iso / wall_seconds / basename fields. 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].fingerprint as a no-op rather than a parse error.

  • No new env vars — VIBEQC_LIVE_LOGGING, VIBEQC_NO_HOSTNAME, VIBEQC_PERFLOG, VIBEQC_VERBOSE cover 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) -> Figure

  • vibeqc.run_fingerprint(molecule, *, basis, method, functional=None, charge=None, multiplicity=None, options=None) -> str

  • vibeqc.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: int knob on run_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() lines

    3

    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 through run_job.

  • VIBEQC_VERBOSE=N env var — sets the default level for batch scripts that don’t want to edit every input file. Same precedence convention as VIBEQC_LIVE_LOGGING and VIBEQC_PERFLOG: explicit verbose= 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=False are 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) and ProgressLogger.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: bool knob on run_job and ProgressLogger — routes every emit through logging.getLogger("vibeqc.run_job") instead of bare sys.stdout writes. Mapping:

    • banner(), info(), stage(), converged(), memory()INFO

    • warn()WARNING

    • iteration(), debug()DEBUG

    Composes naturally with logging.handlers.RotatingFileHandler, SysLogHandler, and logging.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=True does not emit per-iter DEBUG records even if the active handler is set to DEBUG. progress=False still wins as a hard kill switch.

  • The library has carried a NullHandler on the vibeqc top-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 the verbose= kwarg directly — callers who want a non-default level through the dispatch have to construct a ProgressLogger(verbose=N) and pass it as progress=. 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*.py drivers) now emits a basis: <name>  (<nbasis> BFs / <nshells> shells) line in the setup phase, alongside the existing omega = ..., FFT grid ... info line. Closes the parity gap with the molecular-side run_job banner (which has carried Job: RHF / basis=6-31g* since v0.5.0). Surfaces at verbose >= 2 — same gate as every other info() 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_summary is exported from the package top-level alongside format_scf_trace and log_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.PerfTracker for 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:

    1. VIBEQC_PERFLOG=output.perf python my-calc.py (env var)

    2. with vq.perf_log("output.perf"): ... (context manager)

    3. vq.run_job(..., perf_log=True) writes {output}.perf next 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 sprinkle with PerfScope("phase"): ... unconditionally — release builds with the feature off pay one ContextVar.get() and one time.perf_counter() call per scope. Wired into run_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_lattice

    • per-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 by awk / grep.

  • Public API: vibeqc.PerfTracker, vibeqc.PerfScope, vibeqc.perf_log, vibeqc.active_tracker, vibeqc.format_perf_report — full surface in docs/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 means rm 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_PERFLOG hook 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 into run_job. Defeats output buffering on long-running jobs: every write is flushed, so the canonical nohup python my_calc.py > job.log 2>&1 & + tail -f job.log pattern 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 .out when the SCF returns.

  • run_job defaults to live progress on. Solves the “is my background job stuck or actually running?” question without users having to learn a new kwarg. The .out file is also line-buffered so tail -f output-*.out works without any extra setup. Pass progress=False to silence stdout, or a ProgressLogger instance for full routing control. The env var VIBEQC_LIVE_LOGGING=0 opts out globally for batch scripts; explicit progress= kwargs always win over the env var.

Added — runtime manifest

  • run_job(output="x") now writes a third sibling file x.system next to x.out and x.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 in docs/_static/examples/<slug>/<slug>.out are interpretable (“the SCF total: 0.015 s figure was on an Apple M2 Pro at 12 OMP threads”) and reproducible across machines.

  • vibeqc.system_info() and vibeqc.write_system_manifest() exposed as public API so users driving the SCF drivers directly (without run_job) can attach the same provenance to their own outputs. See Tutorial 29 for the grep / diff recipes that turn the manifest into a hardware-aware comparison against the bundled reference.

  • Privacy: hostname opt-out. record_hostname=False on run_job (or VIBEQC_NO_HOSTNAME=1 in the environment) emits hostname = "<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 into docs/_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.md covering the .system manifest format, parsing recipe, and hostname opt-out.

  • New Tutorial 29: “Reproducing a reference output” — walks through comparing your run’s .system against 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.KPoints Python builder (MP / Γ-centred / shifted / Γ-only). Classical-MP auto-shift convention. vibeqc.as_bloch_kmesh boundary helper for back-compat.

  • K2 IBZ reduction via spglib (symmetry=True, builder method KPoints.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 via scheme="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 (VASP Auto mode). metallic flag bumps density 4× and warns if smearing isn’t enabled.

  • K7 run_rhf_periodic_scf and run_rks_periodic_scf accept KPoints directly via as_bloch_kmesh boundary helper. examples/input-k-mesh-convergence.py demonstrates every flavour end-to-end on cubic Mg.

Added — Visualisation interop

  • M1 vq.write_orca_hess(path, mol, hessian_result, ...) writes ORCA-format ASCII .hess files for moltui / chemcraft / avogadro / VMD-nmwiz visualisation. Format-faithful character-by- character match against an ORCA 6.1.1 reference.

  • M2 vq.write_xyz_trajectory and vq.write_opt_trajectory write multi-XYZ trajectory files for geometry-optimisation paths, NEB images, normal-mode movies, and general trajectory visualisation in moltui / OVITO / ASE / Avogadro / PyMOL. The .opt convenience 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.0 and seekpath>=2.1 are now hard runtime dependencies (the C++ core already vendored libspglib for attach_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_analytic is 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} with F_σ = 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 operator L couples α 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.lgmres after 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:HessianResult is 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_analytic combines 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_withs1 for the response part):

    1. Skeleton contributions (4 calls into 17b-2 routines): nuclear repulsion, Σ D ∂²(T+V), Σ Γ ∂²(μν|λσ), −Σ W ∂²S.

    2. AO Fock first-derivative tensor h1ao[A, d, μ, ν] = ∂F_μν/∂R_{A,d} and overlap derivative s1ao[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).

    3. 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) and mo1base[vir] = -e_ai · (h1mo s1mo · ε_i)[vir]. Occ-occ block of mo1 is fixed at s1_oo by orthonormality of the perturbed orbitals (Yamaguchi-Schaefer C.1.20).

    4. Orbital-energy response: mo_e1 = hs[occ] + mo1[occ] · (ε_i ε_j) with hs = h1mo s1mo · ε_i + fvind(mo1).

    5. Response part of Hessian: +4 tr(h1ao · dm1) 4 tr(s1ao · ε·dm1) 2 tr(s1oo · mo_e1) summed over atom pairs.

    Returns a :class:HessianResult indistinguishable 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.gmres with both rtol=tol AND atol=tol (the absolute tolerance handles the high-symmetry edge case where ||b|| is near-zero — H₂ along the bond axis has b 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.krylov solves (I + L) x = b with the convention that the iteration direction is x[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=tol fixes the edge case.

    V1 implementation uses FD-on-Fock for h1ao; a follow-up optimization will replace this with a true analytic libint deriv_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_name requirement, 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=2 engines 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-center

      • nuclear-center perturbations together.

    • compute_eri_hessian_contribution(basis, mol, D, alpha_hf=1.0)Σ_μνλσ Γ ∂²(μν|λσ)/∂R_α∂R_β with the closed-shell two- particle density. alpha_hf covers 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 appropriate pert_to_dof helper, 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; passing i == j to scatter_hessian fixes 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_gradient to 1e−7 Ha/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 reference W/D gives the same (3N, 3N) matrix (to FD-truncation tolerance, 1e−4 for the 1-e pieces and 1e−3 for ERI which has the largest FD truncation error) as numerically differentiating each *_gradient_contribution at fixed weights.

  • Phase 17b-V — vendored libint upgraded to deriv_order=2. scripts/build_libint.sh now builds the source tree with LIBINT2_ENABLE_ONEBODY=2, LIBINT2_ENABLE_ERI=2, and LIBINT2_*_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.1 is configured for deriv_order=1 only; calling libint2::Engine with deriv_order=2 aborts 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_rhf solves the linear-response equation

    .. math:: \mathbf{A} \, \mathbf{U}^x = -\mathbf{B}^x

    where :math:\\mathbf{U}^x is 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_rhf computes 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_thermochemistry consumes a :class:HessianResult and 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 to mol.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’s is_linear flag and n_atoms; n_imaginary_modes_excluded counts modes dropped from the partition function (>0 means the geometry isn’t a true minimum).

    Conventions match PySCF’s :func:pyscf.hessian.thermo.thermo verbatim — atomic-unit per-molecule quantities — so cross-checks go through term-by-term. ThermoOptions covers temperature (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 optional electronic_degeneracy override.

    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 a HessianFDOptions.include_dipole_derivatives flag that wires dipole-moment evaluation into the existing 6N-displacement Hessian loop. The flag is False by 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_intensities then 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:HessianResult fields: dipole_derivatives ((3N, 3) array, or None), dipole_origin ((3,) array, or None), masses_amu ((n_atoms,) array — stored so ir_intensities and 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_fd builds 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 in frequencies_cm1 so callers can slice [6:] or [5:] for the vibrational subset cleanly.

    • Isotope substitution via HessianFDOptions.atomic_masses_amu overrides 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.py pin 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 by b while leaving the occupied ones unchanged (½ S D S projects 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 returned mo_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.py pin 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 where level_shift is 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]^T
    

    with 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 when quadratic_fallback_iter > 0 and the SCF has run that many iterations without converging; default = 0 keeps 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_scf ships :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_step upgraded 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.py pin 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) implements expm_skew (real skew-symmetric only — molecular Fock is real) and quadratic_step with 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 in tests/test_quadratic_scf.py cover 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>.py naming, version-pinning for paper calculations, OMP_NUM_THREADS hygiene, basis/grid/k-mesh convergence order, tee + screen for 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 between running and updating, and called out from quickstart.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.py then run .venv/bin/python water.py) implicitly assumed the user stayed inside the cloned repo, because the .venv/bin/python shorthand is a relative path. Real users cd ~ 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 with mkdir -p ~/vibeqc-runs/<project> + the absolute ~/path/to/vibeqc/.venv/bin/python water.py invocation, 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 of 33ecfa8 from 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 from 3778586.

[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.sh dep-detection failed on Arch / Manjaro with Eigen 5 (1f3174f, planned cherry-pick from main for 0.4.1). The old check used ls a b to test for header existence; ls returns 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 fresh pacman -S eigen. Replaced with pkg-config --exists eigen3

    • [ -e ... ] short-circuit + a broader find walk for gmpxx.h. libint 2.13.1’s bundled FindEigen3.cmake is already Eigen-5-aware (line 51-63 of cmake/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.md Arch / 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.sh cmake invocation gains -Wno-dev. CMake 3.30+ deprecated its bundled FindBoost.cmake module (policy CMP0167) and emits a dev warning whenever a project uses the old find_package(Boost ...) interface. libint 2.13.1’s CMakeLists.txt:360 still uses that interface (waiting on upstream to adopt find_package(Boost CONFIG REQUIRED) + Boost::headers). Until then, -Wno-dev suppresses 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 with cmake_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.5 plus -Wno-dev to 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.sh prints 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 at pip install because vibe-qc’s CMake also needs libxc / spglib / FFTW / libecpint, none of which build_libint.sh touches. The orchestrator script handles all of them (and skips libint as a no-op when it’s already built); pointing users at it from inside build_libint.sh prevents 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 install

    • verify). 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 (default release): target git ref. Common values are release / main / vX.Y.Z.

      • --rebuild-native-deps: nuke third_party/<dep>/install/ before re-running the orchestrator. Use this when a vendored library version bumped between releases — setup_native_deps.sh is 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 in docs/updating.md.

  • docs/updating.md — user-facing update reference. Covers the ./scripts/update.sh easy button and the manual equivalent, explains how to switch between releases (--ref vX.Y.Z to pin to a specific tag, --ref main for bleeding-edge dev, --ref release to 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 → wipe build/), and notes the “don’t update mid-job” guidance for long-running calculations. Wired into the “Getting started” toctree between running and tour. Cross-linked from release_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.md gains a {tip} admonition immediately under the title showing the current release version (auto-extracted from importlib.metadata in docs/conf.py) and its codename (looked up in a RELEASE_CODENAMES table in conf.py). Banner reads e.g. “Current release: 0.4.2 — Schrödinger’s Llama”. The site is built from the release branch, 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 substitution machinery for release metadata. New myst_substitutions block in docs/conf.py exposes {{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/python vs source .venv/bin/activate), one-off vs interactive vs background runs, output capture for long calculations (tee + nohup + tmux), the OMP_NUM_THREADS knob, the SSH-into-bigger-box workflow, the test-suite invocation, and a common-errors table mapping ModuleNotFoundError / RuntimeError: BasisSet / OSError: cannot load library 'libxc.so.7' / SCF did not converge to 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 from index.md, quickstart.md, and tour.md.

  • examples/README.md “Running an example” section explains the .venv/bin/python examples/<script>.py pattern explicitly, including the cd examples/ working-directory tip so output files stay together with the inputs. Cross-references the new running.md for the wider story.

  • All examples/*.py and examples/plots/*.py docstring Run: lines updated from python3 <script>.py to .venv/bin/python <script>.py. ~30 example files touched. Matches what the docs now teach.

Fixed

  • docs/index.md install 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 subsequent pip install (CMake fails with “Could not find Libxc”). Replaced with the orchestrator ./scripts/setup_native_deps.sh (matches what installation.md and README.md already recommend), and added the git checkout release line so users land on the tagged release rather than main ‘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 vibeqc requires the venv’s Python. New version says “save as water.py, run with .venv/bin/python water.py” + a tip-box covering the source .venv/bin/activate alternative and the ModuleNotFoundError failure mode.

  • docs/quickstart.md gains 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 new exchange_scale kwarg 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.75 on 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.LatticeMatrixSet mutation bug — assigning .blocks[i] = M writes to a transient Python-list copy and silently no-ops at the C++ level; new :meth:set_block(i, M) mutates the underlying std::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/ via scripts/build_libecpint.sh. 14b shipped :func:vibeqc.compute_ecp_matrixV_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 via RHFOptions.ecp_centers / ecp_library and 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 at python/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. D1a ships 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. D1b adds the reference dftd3 Python 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 via backend="auto". New :func:vibeqc.dftd3_available probe. D1c wires D3(BJ) through the user-facing drivers: run_job(..., dispersion=...) accepts True (inherit the SCF functional), a functional name ("pbe", "b3lyp", …), or a D3BJParams struct. The .out file grows a “Dispersion correction (D3-BJ)” block; ASE calculator gets a matching kwarg.

  • Phase 12f — periodic Becke partition. :func:vibeqc.build_periodic_becke_grid extends the molecular Becke fuzzy-cell partition denominator over image atoms within image_radius_bohr of the home cell. Required for tight crystals where image-atom Voronoi cells would otherwise intrude on the reference unit cell. PeriodicKSOptions.use_periodic_becke

    • becke_image_radius_bohr toggles 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_shift and PeriodicKSOptions.level_shift (default 0.0). When > 0, the per-iteration Fock is shifted by b·(S ½SDS), raising virtual MO eigenvalues by b and 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_temperature and matching field on PeriodicKSOptions. When > 0, occupations follow n_i = 2 / (1 + exp((ε_i μ)/T)) with μ bisected against the total-electron-count constraint, and the convergence target switches from E to the free energy A = 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_mo for periodic Bloch orbitals. :class:vibeqc.PrimitiveCellGrid + :func:vibeqc.make_primitive_cell_grid build a uniform real-space grid spanning a chosen primitive-cell region. V5b: :func:vibeqc.density_of_states_projected computes the projected DOS onto user-defined AO groups (atom, atom+ℓ, or custom index lists); :class:vibeqc.ProjectedDensityOfStates carries contributions, 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.LatticeMatrixSet compression 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_orbitals decomposes a (UHF/UKS) density matrix into natural orbitals + occupation numbers; :func:vibeqc.idempotency_deviation reports Σ_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_dependence inspects 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 (rectangular S^{-1/2} projecting out null space) instead of symmetric S^{-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 (omega kwarg 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_charge etc.).

  • 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_threads API

    • per-run timing block in the .out file. P1.2: closed remaining parallelism gaps (compute_dipole, MP2/UMP2 transforms, build_xc_periodic per-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 .out file. 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.nbasis and BasisSet.nshells are now @property accessors (no parens).

    • :class:vibeqc.SCFIteration is the unified type for scf_trace items 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.ProjectedDensityOfStates got shifted_energies and group_labels as @property accessors.

    • KS dispatcher (:func:vibeqc.run_rks_periodic_scf, :func:vibeqc.run_rks_periodic_gamma_scf) mirrors the RHF side; both now route on options.lattice_opts.coulomb_method.

    • RHF dispatcher (:func:vibeqc.run_rhf_periodic_scf, :func:vibeqc.run_rhf_periodic_gamma_scf) preserves level_shift through 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>/ via scripts/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 stock pip install vibe-qc works out of the box, no setup script, no LIBINT_DATA_PATH fiddling, no Homebrew dependency. BasisSet construction 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 its RuntimeError). 7 new error-path tests pin the contract.

  • Banner with git provenance. vq.print_banner() prints Release vX.Y.Z on a tagged clean checkout and dev X.Y.Z (branch @ sha) (with dirty flag 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.md backed by examples/quickstart.py. Resource callouts (peak RAM + wall) on every tutorial. docs/release_process.md documents the branch model.

  • GitLab CI/CD docs deployment. Pushes to main (and now release) trigger a build + deploy pipeline: scripts/build_site.sh runs sphinx-build in a python:3.13-slim container 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 import vibeqc cleanly (numpy, ase, dftd3); produces public/ with rendered HTML + robots.txt + sitemap.xml + a .build-info recording 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 rsyncs public/ 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_periodic to the dispatchers (run_rhf_periodic_scf / run_rks_periodic_scf) so user code stays the same when the Coulomb method changes from DIRECT_TRUNCATED to EWALD_3D. Tutorials 5 + 23 + the user_guide/ewald.md reference 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 flip opts.lattice_opts.coulomb_method = CoulombMethod.EWALD_3D for any tight-cell 3D bulk DFT. user_guide/ewald.md gains a “Same dispatcher for KS and UKS” table covering every driver family and its dispatcher pair.

  • docs/conf.py suppresses autodoc.mocked_object warnings. Sphinx 9.x emits ~80 of these on every build (one per public symbol re-exported from the C++ extension via autodoc_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 tracks main directly during pre-tag development (banner reads dev 0.4.0.dev0 (main @ <sha>)) and switches to the fast-forward-only release branch 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. Now ensure_libint_initialized() is called from the BasisSet member-initializer list. libint2’s own throws (bogus LIBINT_DATA_PATH, malformed .g94) translated into RuntimeError with directive context (which basis name failed, what LIBINT_DATA_PATH was set to, where to drop a custom .g94 to fix it).

  • ECP XML share-dir resolution. The vendoring commit had left VIBEQC_LIBECPINT_SHARE_DIR baked at a path that wasn’t populated on every checkout, breaking ECP tests with ValueError: stoi: no conversion (libecpint reading an empty XML attribute via std::stoi). Resolution now: vendored third_party path → libecpint PACKAGE_PREFIX_DIRfind_path fallback. Plus a runtime $VIBEQC_ECP_SHARE_DIR env var the Python __init__ sets to the wheel-bundled path so wheel installs work without rebuilding.

  • Dispatcher silently dropped level_shift when translating between PeriodicSCFOptionsPeriodicRHFOptions in periodic_rhf_dispatch._copy_options_to_rhf / _copy_options_to_scf. Symptom: a user setting opts.level_shift = 0.3 and calling vq.run_rhf_periodic_gamma_scf saw the same SCF trajectory as opts.level_shift = 0.0. Now copied via getattr(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.md migrated from the legacy direct entry points (run_rhf_periodic / run_rks_periodic) to the recommended dispatcher API. docs/user_guide/molecules.md updated to use property-style access for mol.atoms / mol.charge / mol.multiplicity matching the current API.

  • LatticeMatrixSet .blocks mutation footgun. The pybind11 binding for .blocks returns a fresh Python list each access (default for std::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 .blocks getter docstring now warns about the copy semantics.

  • Functional dtor crash when the ctor throws partway through init. Functional::Impl::funcs was resized up front and filled in a loop by xc_func_init; when a later family check rejected the functional (e.g. MGGA / HYB_MGGA), the destructor still ran xc_func_end on 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 .g94 file. vibe-qc now raises RuntimeError with 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_temperature field on PeriodicKSOptions but currently ignores it.

  • No molecular level-shift yet. The level_shift field exists on PeriodicRHFOptions / PeriodicKSOptions but not on the molecular RHFOptions / 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.md is 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 by examples/quickstart.py — total wall under 1 minute on a single core, demonstrates that the out-of-the-box pip install vibe-qc user experience needs zero setup. The previous “tour”-style content moved to docs/tour.md with a top-of-file pointer for newcomers. The toctree under “Getting started” now lists installationquickstarttourtutorial/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_shift and the new C1b opts.smearing_temperature).

    • 25 — symmetry-aware storage of lattice integrals (Phase SYM3a’s compute_overlap_lattice_with_orbits family).

  • docs/user_guide/ewald.md rewrite against the shipped EWALD_3D dispatcher API (run_rhf_periodic_scf / run_rhf_periodic_gamma_scf instead of the bare run_rhf_periodic_gamma_ewald3d backend 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.py probe). Tutorials 20–25 already carried Resources sections from when they shipped. examples/README.md gains 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 on Molecule.multiplicity, not on the SCF-driver options classes (which deliberately do not expose a spin field). Closes the doc-note follow-up the engineering chat agreed to defer in the v0.4 bug sweep.

  • docs/release_process.md registered in the toctree under the “Project” caption alongside changelog / contributing / license. Sphinx -W build now clean of orphan-document warnings.

Fixed

  • Dispatcher silently dropped level_shift when translating between PeriodicSCFOptionsPeriodicRHFOptions in periodic_rhf_dispatch._copy_options_to_rhf / _copy_options_to_scf. Both helpers copied 9 fields by hand and forgot the C1a-introduced level_shift. Symptom: a user setting opts.level_shift = 0.3 and calling vq.run_rhf_periodic_gamma_scf saw the same SCF trajectory as opts.level_shift = 0.0 because the fresh PeriodicRHFOptions constructed for the Ewald backend lost the field. Now copied via getattr(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.md migrated 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.md updated to use property-style access for mol.atoms / mol.charge / mol.multiplicity matching the current API. examples/plots/level-shift-scf-trace.py drops the dead isinstance(it, tuple) defensive branch now that scf_trace items are uniformly SCFIteration across every backend.

  • Phase 12e-c-4c — multi-k Ewald-3D periodic RHF (-i through -iv). Closes out the bulk-crystal pieces of EWALD_3D in four sub-commits. -i lifts the long-range Hartree builder from molecular-limit to proper bulk: :func:vibeqc.evaluate_periodic_density_on_grid and :func:vibeqc.build_j_long_range_periodic evaluate 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. -ii lands Pulay DIIS in run_rhf_periodic_gamma_ewald3d (commutator-error rolling history, numerical-singular fallback, default subspace 8). -iii-a ships :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-b wraps 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. -iv ships python/vibeqc/madelung.py with 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 α·S shift 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_ewald3d returns :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 build F = Hcore + J_ewald(ω, D) ½ K with 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_electrons with α 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_3d stitches 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_cubic returns 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. -3a adds 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). -3b ships :func:vibeqc.build_j_long_range and :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-Payne c·S shift; 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 formula F^{(π(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 under h 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 matrix P(R) such that an AO coefficient vector rotates as v' = 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.py with :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 factorisation D^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 identities D(R₁R₂) = D(R₁)D(R₂), orthogonality, gimbal-lock cases, and small known operators.

  • Phase D1 — DFT-D3(BJ) dispersion correction. Three-commit rollout. D1a ships 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. D1b adds the reference dftd3 Python 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 via backend="auto", with the C++ backend as a complete-without-the- optional-dep fallback. New :func:vibeqc.dftd3_available probe. D1c wires D3(BJ) through the user-facing drivers: run_job(..., dispersion=...) accepts True (inherit the SCF functional), a functional name ("pbe", "b3lyp", …), or a D3BJParams struct. The .out file 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 in run_mp2 and run_ump2 (parallel-for on the GEMM outer loops

    • collapse(2) reduction on the MP2 energy sum), and the three per-cell loops in build_xc_periodic (per-cell-independent GEMMs

    • per-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 throwing RuntimeError: AO basis is linearly dependent. New shared helper :class:vibeqc.CanonicalOrthogonalizer and :func:vibeqc.canonical_orthogonalizer. MO coefficient matrix shape becomes (n_basis, n_kept) with n_kept n_basis; downstream consumers (gradient, MP2, properties) absorb the reduction transparently. New example examples/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 .out file now appends a properties block with Mulliken and Löwdin atomic charges, Mayer bond orders (filtered to pairs with B_AB 0.10 for 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_trace grows basis= and include_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 (with n <= 0 meaning “restore default” — either OMP_NUM_THREADS or the hardware core count). run_job grows a num_threads= keyword; every .out file 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_limit and build_fock_2e_real_space grow an optional omega parameter (default 0.0). When positive, the builders swap libint’s Operator::coulomb for Operator::erfc_coulomb with 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 (full V_μν(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 by compute_nuclear_lattice; callers that want the Ewald V must invoke it directly. Full EWALD_3D dispatch 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_job aborts with :class:vibeqc.InsufficientMemoryError; users can set memory_override=True on the run_job call to proceed anyway (the block then shows Proceeding (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; the available_memory_bytes probe tries psutil first (optional dep), then falls back to /proc/meminfo (Linux) and os.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 in cpp/include/vibeqc/thread_pool.hpp. Controlled by OMP_NUM_THREADS; no API change, all 320 regression tests pass unchanged. Benchmark harness in scripts/bench.py reports 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_3D dispatches nuclear_repulsion_per_cell through 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}. See docs/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’s Operator::erfc_nuclear; exponentially convergent real-space sum for any ω > 0. Used by Phase 12e-c to assemble the full EWALD_3D dispatch of compute_nuclear_lattice.

  • Documentation — new docs/user_guide/ewald.md with 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. libint2 silently returns a zero-shell BasisSet when it cannot find a matching .g94 file under LIBINT_DATA_PATH for the requested name (a real example: the diffuse-augmented Pople 6-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 raises RuntimeError with a directive message naming the basis and pointing the user at fixing the spelling, using the canonical libint/BSE form, or dropping a file into basis_library/custom/.

  • Functional dtor crash when the ctor throws partway through init. Functional::Impl::funcs was resized up front and filled in a loop by xc_func_init; when a later family check rejected the functional (e.g. MGGA / HYB_MGGA), the destructor still ran xc_func_end on 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_3D end-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.