vibe-qc v0.4 “Schrödinger’s Llama”: from Day 3 to first public release

vibe-qc is an open-source quantum chemistry and solid-state code — C++17 backend, Python frontend via pybind11, ASE Calculator interface — being written in the open. Day 1 shipped the molecular stack and Ewald infrastructure. Day 2 closed v0.2.0. Day 3 set the public-release bar at v0.4.0 and shipped the first convergence aid. This post covers everything from that decision to v0.4.6 “Schrödinger’s Llama” — 150 commits on main, six patch tags on release, 131 tests growing to 909, and the gap between “compiles on the developer’s machine” and “actually installs on Manjaro” navigated in full.

Three-panel summary: periodic SCF method matrix all shipped, six-patch release cascade, test count 131 to 909
Left: every cell of the periodic SCF matrix — RHF, UHF, RKS, UKS × Γ-only and multi-k — shipped by v0.4. Centre: six hotfixes in 48 hours, four driven by Linux installer bugs. Right: test count from Day 1 through v0.4.6, zero regressions across all six patches.

Closing the method matrix

The v0.4 engineering goal was to close every combination of method, spin, and k-sampling for periodic Ewald SCF. Eight cells: RHF and UHF, RKS and UKS, each at Γ-only and multi-k. Day 2 had RHF at both k-samplings. The remaining six shipped across the v0.4 arc.

Periodic UHF (Phases 15a and 15b) opened open-shell Hartree-Fock on solids — magnetic systems, spin-polarised defect cells, antiferromagnetic unit cells. The unrestricted SCF minimises the energy separately in the $\alpha$ and $\beta$ spin channels, with the constraint that both share the same Coulomb field. Periodic RKS and UKS (Phases 15c-1 through 15c-3) required a new C++ kernel, build_xc_periodic_uks, to handle open-shell libxc evaluation on a LatticeMatrixSet density. The exchange-correlation energy for the open-shell case is

$$E_{\mathrm{xc}}[\rho^\alpha, \rho^\beta] = \int \varepsilon_{\mathrm{xc}}(\rho^\alpha(\mathbf{r}), \rho^\beta(\mathbf{r}),|\nabla\rho^\alpha|, |\nabla\rho^\beta|)\,d\mathbf{r}$$

evaluated on the periodic grid with Becke partitioning extended to image atoms. With that kernel in place, the SCF matrix was closed: any combination of method and spin runs at either Γ-only or multi-k with the same dispatcher entry point. The periodic DFT tutorial, the tight-cell Becke tutorial, and the periodic SCF convergence tutorial all exercise different corners of this matrix.

ECPs, dispersion, and convergence aids

Three capability tracks landed on top of the SCF matrix.

Phase 14 added effective-core potentials via libecpint 1.0.7, vendored with pugixml and libcerf. ECP support is wired through all four molecular drivers and validated against PySCF on Zn²⁺/LANL2DZ to micro-hartree accuracy. This opens the pob-* ECP basis sets for Rb through Lu — the heavy-element range that covers metal oxides, perovskites, and lanthanoid chemistry. The ECP integrals add a non-local potential term to the core Hamiltonian:

$$\hat{V}_{\mathrm{ECP}} = \hat{U}_{\mathrm{local}} + \sum_{\ell=0}^{L} \hat{U}_\ell \hat{P}_\ell$$

where $\hat{P}_\ell$ projects onto angular momentum $\ell$ and $\hat{U}_\ell$ is the semi-local correction. The integration hit one genuinely tricky bug: the libecpint API header documentation was actively misleading about whether the function appended /xml/ to the share-dir argument. It does. The source at api.cpp:73 was the authority.

Phase D1 added DFT-D3(BJ) dispersion correction, wired through run_job and the ASE Calculator. The dispersion tutorial shows the binding-curve comparison that makes the underbinding failure of uncorrected GGAs visible in a single plot.

The convergence track (C1a through C1c) shipped three tools. C1a is Saunders-Hillier level shifting (covered in the Day 3 post), which modifies the Fock matrix before diagonalisation as $\tilde{\mathbf{F}} = \mathbf{F} + b(\mathbf{S} – \frac{1}{2}\mathbf{S}\mathbf{D}\mathbf{S})$, raising virtual eigenvalues by $b$ without changing the converged fixed point. C1b added Fermi-Dirac smearing with fractional occupations. C1c shipped a quadratic SCF fallback — a Newton step in MO space,

$$\Delta\mathbf{\kappa} = -\mathbf{H}^{-1}_{\mathrm{diag}}\,\mathbf{g}$$

where $\mathbf{g}$ is the orbital-rotation gradient and $\mathbf{H}_{\mathrm{diag}}$ is the diagonal orbital Hessian, with a trust-region cap to prevent overlarge steps. Default is off; opts.quadratic_fallback_iter = N activates after iteration $N$. The SCF convergence user guide covers all three tools in order of when to reach for them.

The bug that tutorials found

The most instructive quality event of the v0.4 arc was a 12-item bug report that came from the process of writing tutorials. The pattern: reading the API carefully enough to explain it surfaces issues that running the API does not.

The sharpest example was level-shift silently dropped in the dispatcher. Engineering had shipped C1a, added level_shift to SCFOptions, and the tests passed. But _copy_options_to_rhf and _copy_options_to_scf copied nine fields by hand and had not been updated. Setting level_shift=0.4 in a tutorial script produced no effect on the SCF trace. The fix was one line in two functions. The regression test now asserts that a dispatcher round-trip preserves all SCFOptions fields — the same class of bug cannot recur silently.

The other significant find: the basis library was not inside the wheel. basis_library/ lived at repo root with basis_library/basis/ gitignored, populated by a shell script at build time. A pip wheel does not run shell scripts. The fix was moving the whole tree to python/vibeqc/basis_library/. Ninety standard .g94 basis sets plus three solid-state pob-* sets now ship inside the wheel. No environment variable required.

Schrödinger’s Llama: the patch-release cascade

Schrödinger's Llama comic: a worried llama in a wooden box labelled macOS OK, Linux question mark, Arch triple question mark, with a radioactive pip install vibe-qc flask and a thought bubble asking whether v0.4.5 is fixed on Manjaro
The release is simultaneously “working” and “broken on Linux” — until a user opens the box. Six patch releases in 48 hours, four of them Linux installer bugs that development on macOS had never revealed.

Tagging v0.4.0 followed docs/release_process.md: bump pyproject.toml, write the changelog, tag from main, fast-forward release. The banner started reading Release v0.4.0. Then the first user on a different machine tried to install it.

v0.4.1 hit three independent Arch/CMake failures. libint’s Eigen detection used ls a b, which returns nonzero if any argument is missing — on Arch, Eigen 5 installs only at /usr/include/eigen3/Eigen/Core, not the top-level path that the detection script expected. libint’s FindBoost invocation triggered fatal CMake 4.x deprecation warnings. And all three vendored dependencies declared cmake_minimum_required(VERSION <3.5), which CMake 4.x rejects outright. None of these were vibe-qc bugs. They were upstream code aging into newer toolchains, absorbed because the install path is part of the public surface.

v0.4.4 and v0.4.5 were the genuinely interesting failures. After vendoring five native libraries, the rpath only mentioned two of them. macOS found the others via DYLD‘s fallback search. Linux does not.

Side-by-side dependency tree showing DT_RUNPATH non-transitivity causing libxc not found, versus DT_RPATH transitivity fixing it
Left: under DT_RUNPATH (the modern GNU ld default), the search path in vibeqc_core.so does not propagate to libecpint’s own dependency search — libcerfcpp.so.3 is not found. Right: -Wl,--disable-new-dtags forces DT_RPATH, which is transitive. macOS’s @rpath is transitive by default, which is why this was Linux-only.

v0.4.4 added a foreach loop over all five vendored deps — problem solved for direct dependencies. Then v0.4.5 hit libcerfcpp.so.3 not found, even though the file existed on disk. The root cause was DT_RPATH vs DT_RUNPATH in ELF binaries. Modern GNU ld defaults to emitting DT_RUNPATH, which is non-transitive: the rpath in vibeqc_core.so applies only to its direct dependencies. When libecpint loads its own deps, the loader does not inherit that rpath. Two-prong fix: -Wl,--disable-new-dtags on the Linux link of vibeqc_core.so (forcing DT_RPATH), plus -DCMAKE_INSTALL_RPATH='$ORIGIN' baked into libecpint at build time. The ELF loader’s transitivity semantics are not something most scientific software developers need to know about — until they do.

v0.4.6 closed the codename system. RELEASE_CODENAMES lives in python/vibeqc/banner.py. The docs CI container does not install vibe-qc (no C++ stack in python:3.13-slim), so importlib.metadata.version("vibe-qc") fell through to a placeholder. The fix was loading banner.py as a standalone module via importlib.util, bypassing vibeqc/__init__.py entirely. Docs and runtime now read the same dictionary without either depending on the other’s import chain. Every SCF log from v0.4.6 onwards carries Release v0.4.6 "Schrödinger's Llama" — unambiguous build provenance in every pasted traceback.

Documentation: 19 tutorials to 25

Six new tutorials shipped: natural orbitals and the idempotency diagnostic, projected density of states, periodic Bloch orbital cubes, tight-cell DFT with the periodic Becke partition, periodic SCF convergence, and symmetry-aware lattice integral storage. All 25 now carry resource callouts — peak RSS and wall-clock on a reference machine, generated by a dev script and pasted in. The quickstart was restructured from a 2000-line everything-page into four focused documents.

The CI/CD side replaced a silent nightly cron with a GitLab pipeline: sphinx-build -W --keep-going (warnings are errors), rsync to the production webroot, triggered only on the release branch. One gotcha worth documenting: GitLab File-type variables silently corrupt multi-line OpenSSH private keys. Store the key base64-encoded in a regular Variable and decode it in the YAML with base64 -d.

Going public

GitLab project visibility flipped to Public at v0.4.0. Anonymous clone lands on the release branch by default — the latest tagged release, not the dev main. The project has a communication address (mpei@vibe-qc.com), a PGP key for security reports (fingerprint CC6D 30BB DF96 F694 C615 FBDE 4CD5 65CF 26B1 E7E5, public key at vibe-qc.com/_static/pgp/mpei.asc), a SECURITY.md, and a CONTRIBUTING.md decision tree.

The number that matters

909 tests pass on a fresh install. The verification command is in installation.md. Zero regressions across six patch releases. The feature work — closing the periodic SCF matrix, ECPs, D3(BJ), three convergence tools — was the straightforward part. The work that made it real for someone on a different machine was four patches driven by Linux loader semantics, one by CMake version policy, and one by a codename dictionary that two separate CI environments needed to read from the same source. All six are in the changelog. All six are fixed permanently.

The code is at vibe-qc.com. MPL 2.0. Clone it, run the tests, read the 25 tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.