"""Runtime banner and linked-library version reporter.
Printed at the top of any serious vibe-qc text output so a user can always
identify exactly which build of the program produced a given log.
Programmatic API
----------------
``banner()``
Return the full multi-line banner as a string.
``print_banner()``
Convenience: ``print(banner(), **kwargs)``.
``library_versions()``
Dict of the versions of linked native dependencies (libint, libxc,
spglib) plus the package version. Useful in tests and bug reports.
``build_info()``
Dict describing the current source build: git branch, short commit
SHA, working-tree dirty flag, and exact-tag (if any). Empty when
running from an installed wheel without a ``.git/`` available.
``enforce_runtime_pin_from_env()``
Optional release-validation guard. When ``VIBEQC_EXPECT_*``
environment variables are set, fail early if the running version,
git SHA, or branch does not match the requested pin.
The ``format_scf_trace()`` helper in :mod:`vibeqc.scf_log` prepends the
banner automatically so a persisted SCF run log carries its provenance --
critical for matching a calculation result back to the exact build that
produced it (see ``docs/release_process.md``).
"""
from __future__ import annotations
import os
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
from typing import Mapping, TextIO
# Package version -- reads the installed metadata so it tracks pyproject.toml
# automatically at build/install time. Falls back to an explicit label if the
# package isn't found (e.g. running from a source tree without installation).
try:
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _pkg_version
except ImportError: # pragma: no cover -- Python 3.11+ always ships this
_pkg_version = None
PackageNotFoundError = Exception # type: ignore[misc, assignment]
__all__ = [
"RELEASE_CODENAMES",
"VIBEQC_VERSION",
"banner",
"build_info",
"codename_for_version",
"enforce_runtime_pin_from_env",
"library_versions",
"print_banner",
]
# --- Release codename catalog --------------------------------------------
#
# Every major + minor release carries a "Scientist's Animal" codename --
# see docs/roadmap.md Sec. "Release codenames" for the full policy. The
# catalog is keyed by the X.Y.Z release version; minor releases (X.Y.0)
# are always present, and patch releases (X.Y.Z, Z > 0) MAY add their
# own override here if they have a distinct theme worth surfacing
# separately (e.g. v0.7.1 "Pulay's Triangle", v0.7.2 "Boys' Crucible",
# v0.7.3 "Whitten's Bridge" all override v0.7.0 "Löwdin's Compass").
# Patch versions WITHOUT an entry inherit the parent minor's codename
# via the fallback in :func:`codename_for_version`. Dev builds
# (.devN / aN / bN / rcN) inherit the codename of the upcoming release
# after PEP-440 suffix stripping.
#
# When cutting a new MINOR release (X.Y.0), add the entry here in the
# same commit that bumps ``pyproject.toml``. Patches need an entry
# here only if they get a distinct codename; otherwise they inherit.
#
# This catalog is also imported by ``docs/conf.py`` to render the
# landing-page "Current release" tip and the codename in MyST
# substitutions ({{codename}}). One source of truth, two consumers.
RELEASE_CODENAMES: dict[str, str] = {
"0.1.0": "Roothaan's Raven", # Clemens Roothaan (1918-2019),
# Roothaan-Hall equations (1951):
# the matrix-eigenvalue formulation of
# HF that every molecular code rests on.
# Raven: the first bird, intelligent,
# lays the foundation everything else
# builds on.
"0.2.0": "Ewald's Eagle", # Paul Peter Ewald (1888-1985),
# Ewald summation (1921) for
# conditionally convergent lattice
# sums. Eagle: soars above the
# periodic landscape with a Fourier
# perspective.
"0.2.5": "Wigner's Wolf", # Eugene Wigner (1902-1995),
# Nobel 1963, group theory in quantum
# mechanics. The Wigner D-matrices
# are the mathematical core of the
# symmetry exploitation phase. Wolf:
# hunts in packs (group theory).
"0.3.0": "Mulliken's Magpie", # Robert S. Mulliken (1896-1986),
# Nobel 1966, molecular orbital
# theory and population analysis.
# Magpie: collects shiny things
# (properties, visualizations).
"0.4.0": "Hellmann's Hedgehog", # Hans Hellmann (1903-1938),
# Hellmann-Feynman theorem (1937)
# makes analytic gradients tractable.
# Hedgehog: well-protected by spines
# (ECPs replace core electrons).
"0.5.0": "Wilson's Otter", # vibrations / Hessian flagship
# (E. B. Wilson 1955)
"0.6.0": "Pulay's Owl", # periodic atomic gradients --
# Pulay correction terms (1969)
# are the conceptual core of
# this release; Pulay also gave
# us DIIS (1980) which underpins
# the SCF stack the gradients
# build on. Owl: nocturnal,
# sees what others miss
# (sub-meV displacement-derived
# forces).
"0.7.0": "Löwdin's Compass", # periodic SCF correctness
# flagship: linear-dependence
# diagnostic + screening
# optimiser + Lehtola pivoted
# Cholesky + transparent
# primitive filtering. Per-Olov
# Löwdin gave us canonical
# orthogonalisation (Adv. Quantum
# Chem. 5, 185, 1970) and the
# symmetric S^{-1/2} that anchors
# this release's overlap-matrix
# work. Compass: navigates the
# basis-set / cutoff space and
# finds the loosest PSD-keeping
# configuration without silent
# truncation. Aligns with
# CRYSTAL's "refuse to silently
# truncate; push the problem to
# basis-set design" philosophy.
"0.7.1": "Pulay's Triangle", # cross-code parity layer:
# automated regression suite
# (examples/regression/) extended
# to molecules + DF + open-shell
# + MP2 + B3LYP/G; vibe-qc, PySCF
# and ORCA driven from a single
# spec and Δ-compared per case.
# Peter Pulay invented DIIS (1980)
# which makes molecular SCF
# converge tightly enough for
# µHa cross-code parity, the
# Pulay-Vosko RI / density-fitting
# framework (1990) that anchors
# the DF layer, and the analytical-
# gradient theory the suite will
# exercise next. Triangle: the
# three-way reference geometry
# vibe-qc / PySCF / ORCA.
"0.7.3": "Whitten's Bridge", # bridge the v0.7.2 DF feature
# into the molecular regression
# runner: `density_fit=True` +
# `aux_basis=...` (with autodetect
# via `default_aux_basis_for`)
# threaded through the SCF and
# post-SCF MP2 paths. John L.
# Whitten introduced
# density-fitting / RI in 1973
# (J. Chem. Phys. 58, 4496),
# giving the field the trick that
# makes def2-style RI auxiliary
# bases tractable. Bridge: the
# (case, code) wiring from spec to
# vibe-qc DF kernel that this
# patch closes -- and the bug
# discovered by closing it
# (def2-svp DF SCF SIGSEGVs in
# `run_rhf`, surfaced cleanly via
# the v0.7.2 subprocess-isolation
# layer rather than bricking the
# whole suite).
"0.8.0": "Grimme's Gecko", # molecular methods polish:
# B2PLYP + DSD-PBEP86-D4 double
# hybrids, D4 dispersion via dftd4,
# EEQ atomic-charge model
# (Caldeweyher/Grimme D4a-i,
# Apache-2.0 port), direct SCF for
# all four molecular drivers,
# RIJCOSX performance closure
# vs ORCA, Lebedev-Laikov angular
# grids + Stratmann-Scuseria-Frisch
# atomic partition, B3LYP-VWN5
# convention alignment, libecpint
# banner integration, vqfetch
# external-data Phase 1. Stefan
# Grimme's group's lineage runs
# through every major v0.8.0
# methods deliverable: D3(BJ) -> D4
# -> EEQ; the B2PLYP family (2006);
# DSD-PBEP86 (Kozuch/Martin/Grimme);
# composite "3c" methods (queued
# for v0.9.0). Gecko: small, fast,
# surfaces-stick -- the mol-methods
# arc that ships an entire class
# of well-validated post-HF methods
# built on a clean dispersion +
# double-hybrid foundation.
"0.7.2": "Boys' Crucible", # density-fitting flagship:
# 2c + 3c integral kernels +
# DensityFitting object + def2 /
# cc-pV*Z / Pople-RIFIT aux
# libraries + DF wired into RHF /
# UHF / RKS / UKS / MP2 / UMP2
# energy and J / K analytic
# gradients. S. F. Boys (1911-1972)
# introduced Gaussian-type orbitals
# in 1950 (Proc. R. Soc. A 200, 542),
# the foundation that makes RI /
# density-fitting numerically
# tractable: the exp(-a r^2)
# primitives admit Gaussian-product
# 2- and 3-centre overlap and
# Coulomb integrals in closed form.
# Crucible: per-case subprocess
# isolation in the regression
# suite -- every (case, code) pair
# runs in its own contained
# subprocess so a C-level crash
# (PySCF segfault, ORCA non-zero
# exit, libxc abort) is captured
# as a single error row instead of
# bricking the whole dispatcher.
"0.9.0": "Knowles's Kingfisher", # wavefunction-methods cycle:
# MP2 -> MP3 -> MP4 -> CCD -> CCSD
# canonical-correlation
# scaffolding + the full
# semiempirical-DFTB stack
# (GFN2-xTB, PM6, OM1/OM2/OM3) +
# basissetdev integration. Peter
# J. Knowles is a coupled-cluster
# / configuration-interaction
# methods author (the MOLPRO
# MRCI / direct-CI machinery,
# Knowles & Handy 1984 FCI
# algorithm, the Werner-Knowles
# CASSCF). Kingfisher: precise,
# patient, strikes once -- the
# post-HF correlation ladder
# that v0.14.0+ will build on.
# Retroactively added to the
# catalog post-v0.10.0; v0.9.0
# cut went to a side branch and
# the entry never landed on main.
"0.10.0": "Pisani's Penguin", # periodic-SCF maturity cycle:
# multi-k EDIIS + Python
# accelerator family, SYM6
# crystal symmetry, BIPOLE
# L_max>=2, GPW M2-full iterative
# SCF, CRYSTAL-a / Ewald-w gauge
# unification (cluster-A1 fix at
# 859efe0a closes +19 Ha
# translation-invariance break
# on LiH rocksalt), full DFTB
# stack + DFT+U Increments 1/2/
# 4a/4b/4d-light, NEB, PWPB95,
# periodic D3-BJ. Cesare Pisani
# founded the CRYSTAL code at
# Turin (1976-) -- the periodic-
# SCF lineage vibe-qc inherits.
# Penguin: lives at extremes
# (solid-state, structured,
# low-T). Periodic GDF deferred
# to v0.11.0 (fourth cycle:
# v0.8.0 -> v0.9.0 -> v0.10.0 ->
# v0.11.0).
"0.11.0": "Sun's Stingray", # periodic-GDF chain closure:
# compcell + multi-k + AFT --
# fourth-cycle defer finally
# closes (v0.8.0 -> v0.9.0 ->
# v0.10.0 -> v0.11.0). The
# Sun-Berkelbach RSGDF
# (range-separated Gaussian
# density fitting, Sun et al.
# JCP 147, 164119, 2017) is the
# algorithm vibe-qc actually
# implements: short-range Lpq
# via Coulomb-metric GDF +
# long-range via analytic-FT
# auxiliary plane-wave terms.
# Qiming Sun also authored
# PySCF (the µHa-parity
# reference). Stingray: glides
# over the periodic landscape
# via a Fourier transform --
# k-space agile.
"0.12.0": "Knuth's Beaver", # algorithmic density release:
# DLPNO-CCSD, TDDFT, CASCI/
# CASSCF, IC-CASPT2, CIS/TDA
# gradients, MSINDO periodic
# CCM, metadynamics, MD, MACE
# MLIP, GPW production surface,
# BIPOLE analytic gradient
# gate, MPI parallelisation.
# Donald Knuth authored The
# Art of Computer Programming
# -- algorithmic rigour at the
# foundation of DLPNO pair-
# classification, integral
# transforms, and per-pair
# solvers. Beaver: industrious
# builder (nine headline
# features in a single minor
# cycle).
"0.13.0": "Wisesa's Fox", # generalised regular (GR)
# k-grids headline; ROHF/ROKS;
# periodic local correlation
# (megacell MP2 + DLPNO-CCSD(T));
# Gilat-Raubenheimer metallic DOS;
# MSINDO to Xe; BIPOLE<->CRYSTAL
# parity. Pandu Wisesa: GR-grid
# first author (2016). Fox:
# nimble + efficient (~2x MP).
"0.14.0": "Bartlett's Goose", # canonical CCSD(T)
# headline (R/U/ROHF + (T), FNO)
# + DLPNO-MP2/CCSD(T) (R+U) +
# VV10/double-hybrids + ROHF/ROKS
# + periodic guesses/mixers.
# Rodney Bartlett: CCSD for
# quantum chemistry (Purvis-
# Bartlett 1982). PRE-STAGED
# (decided 2026-06-20); renders
# only on a 0.14.x build, locked
# for real at the v0.14.0 cut.
"0.15.0": "Neese's Cheetah", # DLPNO
# local correlation headline
# (molecular DLPNO-MP2/CCSD(T)) +
# eigensolver framework + CASSCF
# analytic gradients/geom-opt +
# COOP/COHP + QTAIM + dense-crystal
# periodic-XC fix. Frank Neese:
# DLPNO-CCSD(T) (Riplinger-Neese
# 2013). Cheetah: the speed local
# correlation buys. PRE-STAGED
# (decided 2026-06-25); renders
# only on a 0.15.x build, locked
# at the v0.15.0 cut.
}
def _strip_dev_suffix(version: str) -> str:
"""Return the release version a dev build is heading toward, by
stripping any ``.devN``, ``aN``, ``bN``, ``rcN`` (PEP 440)
pre-release suffix. ``"0.5.0.dev0"`` -> ``"0.5.0"``."""
for sep in (".dev", "a", "b", "rc"):
idx = version.find(sep)
if idx > 0 and version[idx - 1].isdigit():
return version[:idx]
return version
def codename_for_version(version: str) -> str | None:
"""Return the codename for a given vibe-qc release version, or
``None`` if no codename is assigned.
Resolution order:
1. Strip ``.devN`` / ``aN`` / ``bN`` / ``rcN`` pre-release
suffix (PEP 440 dev builds inherit the upcoming release's
codename).
2. Direct lookup of the resulting ``X.Y.Z`` in
:data:`RELEASE_CODENAMES`.
3. Fall back to the parent minor's codename: look up
``f"{X}.{Y}.0"``. So v0.4.1 / v0.4.2 / ... automatically
inherit v0.4.0's codename (matches the policy in
``docs/roadmap.md Sec. Release codenames``: "Patch releases
inherit their parent minor's codename").
4. ``None`` if no codename is registered for the minor.
Examples
--------
>>> codename_for_version("0.5.0")
"Wilson's Otter"
>>> codename_for_version("0.5.0.dev0")
"Wilson's Otter"
>>> codename_for_version("0.4.1") # patch inherits v0.4.0
"Hellmann's Hedgehog"
>>> codename_for_version("99.99.99") is None
True
"""
stripped = _strip_dev_suffix(version)
if stripped in RELEASE_CODENAMES:
return RELEASE_CODENAMES[stripped]
parts = stripped.split(".")
if len(parts) >= 2:
anchor = f"{parts[0]}.{parts[1]}.0"
if anchor in RELEASE_CODENAMES:
return RELEASE_CODENAMES[anchor]
return None
def _compute_version() -> str:
"""Read the installed distribution metadata. The distribution is
named ``vibe-qc`` in ``pyproject.toml`` since the rename; older
local installs may still be listed as the legacy ``vibeqc``, so we
try both."""
if _pkg_version is None:
return "0.0.0+unknown"
for dist_name in ("vibe-qc", "vibeqc"):
try:
return _pkg_version(dist_name)
except PackageNotFoundError:
continue
return "0.0.0+unknown"
VIBEQC_VERSION: str = _compute_version()
def _blas_backend_label(libraries: str) -> str:
"""Map the raw BLAS_LIBRARIES string from CMake's FindBLAS to a
short human-readable backend label for the banner.
Examples:
"-framework Accelerate" -> "Accelerate"
"/usr/lib/libopenblas.so" -> "OpenBLAS"
"/usr/lib/libblas.so.3.12.0" -> "netlib BLAS"
"/opt/intel/.../libmkl_rt.so" -> "MKL"
"" -> "none"
The label is informational -- it falls back to the raw library
name when no pattern matches, so a build against an unexpected
BLAS still gets a meaningful banner line.
"""
if not libraries:
return "none"
haystack = libraries.lower()
# Order matters: more specific patterns first.
if "accelerate" in haystack:
return "Accelerate"
if "mkl" in haystack:
return "MKL"
if "openblas" in haystack:
return "OpenBLAS"
if "blis" in haystack:
return "BLIS"
if "atlas" in haystack:
return "ATLAS"
if "libblas" in haystack or "/blas" in haystack:
return "netlib BLAS"
# Unknown backend -- return the basename so the user sees *something*
# concrete in the banner instead of "unknown".
first = libraries.split(";")[0].strip()
if first.startswith("-framework "):
return first[len("-framework ") :]
return os.path.basename(first) or first
[docs]
def library_versions() -> Mapping[str, str]:
"""Return the version strings of vibe-qc itself and its native
dependencies. Keys are always present; values are ``"unknown"``
if a probe fails, ``"none"`` when the dependency was not linked
at build / install time.
Core (always linked at build time): ``libint``, ``libxc``,
``spglib``, ``libecpint``, ``fftw3``, ``blas``.
Optional (linked at runtime only when the ``[dispersion]``
extra is installed): ``dftd3``, ``dftd4``. Each is set to
``"none"`` when the corresponding PyPI package isn't installed
in the user's environment -- distinguishes "not installed" from
"probe failed" (the latter would be ``"unknown"``).
The ``"vibe-qc"`` key carries the project version. The legacy
``"vibeqc"`` key is kept as an alias so code that predates the
rename keeps working without change.
The ``"blas"`` key carries a short backend label (Accelerate /
OpenBLAS / MKL / netlib BLAS / ...) decorated with ``" +LAPACKE"``
when EIGEN_USE_LAPACKE is enabled, so the banner conveys both
the linkage and whether dense solvers delegate to LAPACK.
"""
versions: dict[str, str] = {
"vibe-qc": VIBEQC_VERSION,
"vibeqc": VIBEQC_VERSION, # alias, retained for back-compat
}
# Probe the compiled extension. Each of the libraries exposes a
# version accessor; every version is known at link time.
try:
from . import _vibeqc_core as _core
except ImportError:
versions.update(
{
"libint": "unknown",
"libxc": "unknown",
"spglib": "unknown",
"fftw3": "unknown",
"blas": "unknown",
}
)
return versions
try:
versions["libint"] = _core.libint_version()
except Exception:
versions["libint"] = "unknown"
try:
versions["libxc"] = getattr(_core, "libxc_version", lambda: "unknown")()
except Exception:
versions["libxc"] = "unknown"
try:
versions["spglib"] = _core.spglib_version()
except Exception:
versions["spglib"] = "unknown"
# libecpint version probe -- added with the v0.8.0 banner / libecpint
# coupled fix (prep doc Sec. 488-501). libecpint has been bundled since
# v0.4.0 (commit add8163) but the banner didn't surface its version
# until v0.8.0. Defensive: an older _core lacking libecpint_version
# falls back to "unknown" rather than failing the whole library
# probe.
try:
versions["libecpint"] = getattr(_core, "libecpint_version", lambda: "unknown")()
except Exception:
versions["libecpint"] = "unknown"
# FFTW3 version probe -- FFTW3 has been a required build dep since
# the FFT-Poisson Ewald long-range Hartree solver landed; the GAPW
# route (v0.10.x) is the second consumer. Banner coverage added per
# CLAUDE.md Sec. 6 alongside the M1 PW-basis infrastructure. Defensive
# getattr: an older _core lacking fftw3_version falls back to
# "unknown".
try:
versions["fftw3"] = getattr(_core, "fftw3_version", lambda: "unknown")()
except Exception:
versions["fftw3"] = "unknown"
# BLAS info was added with the BLAS+LAPACK linkage commit; an
# older C extension predating that change won't have blas_info.
# Treat that as "unknown" so the banner remains parseable across
# builds.
try:
_info = _core.blas_info()
if not _info.get("blas_enabled"):
versions["blas"] = "none"
else:
label = _blas_backend_label(_info.get("libraries", ""))
if _info.get("lapacke_enabled"):
label = f"{label} +LAPACKE"
versions["blas"] = label
except Exception:
versions["blas"] = "unknown"
# Optional dispersion backends (dftd3, dftd4). Both ship as PyPI
# wheels with bundled binary libraries (libdftd3 / libdftd4) that
# vibe-qc links to at runtime via ctypes/cffi when the user calls
# vibeqc.compute_d3 / vibeqc.compute_d4. CLAUDE.md Sec. 6 mandates
# banner coverage for every linked native dep; these are linked
# *optionally* (extra `[dispersion]`) so the value is "none" when
# the package is absent rather than "unknown" -- distinguishes
# "not installed" from "failed to probe".
for pkg in ("dftd3", "dftd4"):
try:
module = __import__(pkg)
versions[pkg] = str(getattr(module, "__version__", "unknown"))
except ImportError:
versions[pkg] = "none"
except Exception:
versions[pkg] = "unknown"
# Optional MLIP stack (the `[mace]` extra): PyTorch + ACEsuit MACE +
# e3nn. Not vendored native libs, but heavy runtime deps that produce
# user-facing energies via method="mace", so the banner records their
# versions for provenance (CLAUDE.md Sec. 6). "none" when the extra is
# absent (e.g. the Python 3.14 dev env, where MACE can't be installed).
for pkg in ("torch", "mace", "e3nn"):
try:
module = __import__(pkg)
versions[pkg] = str(getattr(module, "__version__", "unknown"))
except ImportError:
versions[pkg] = "none"
except Exception:
versions[pkg] = "unknown"
return versions
# --- Build-provenance probe -----------------------------------------------
#
# When vibe-qc runs from an editable install or directly from a source
# checkout, we want the banner to record exactly which git revision is
# in play -- branch name, short SHA, dirty-flag, and exact-tag (for
# tagged releases). When running from a wheel that has no ``.git/``
# alongside it, all of these probes return None and the banner falls
# back to the package version alone.
#
# This matters because users will run calculations against specific
# branches for testing (``release``, ``main``, or topic branches), and
# the only way to match a misbehaving SCF log to the build that
# produced it is to record the SHA in the log itself.
#
# Probes are cached at module-import time to avoid repeated subprocess
# calls; ``build_info.cache_clear()`` is available for tests.
def _find_git_root(start: Path) -> Path | None:
"""Walk upward from ``start`` looking for a ``.git`` directory or
``.git`` worktree-link file. Returns the parent directory of the
first match, or None if we hit the filesystem root."""
p = start.resolve()
while p != p.parent:
if (p / ".git").exists():
return p
p = p.parent
return None
def _git(repo: Path, *args: str) -> str | None:
"""Run a git command in ``repo`` and return stripped stdout, or
None on any failure (no git installed, broken repo, command
rejected). Never raises."""
try:
out = subprocess.check_output(
["git", *args],
cwd=str(repo),
stderr=subprocess.DEVNULL,
text=True,
timeout=2.0,
)
return out.strip() or None
except (
subprocess.CalledProcessError,
FileNotFoundError,
subprocess.TimeoutExpired,
OSError,
):
return None
@lru_cache(maxsize=1)
def build_info() -> Mapping[str, str | bool | None]:
"""Return git provenance for the current build, or empty mapping if
no ``.git/`` is available (e.g. installed wheel).
Keys (always present, may be None / False):
- ``branch`` : current branch name, or ``"HEAD"`` for detached.
- ``sha`` : 7-character abbreviated commit SHA.
- ``sha_full``: full 40-character SHA.
- ``dirty`` : True if the working tree has uncommitted changes.
- ``tag`` : exact tag at HEAD, e.g. ``"v0.4.1"``; None if not
on a tag.
- ``is_release``: True if the current commit is exactly tagged AND
the working tree is clean. This is the only state
where the banner displays as "Release X.Y.Z".
Empty dict (``{}``) when no git context exists at all.
"""
repo = _find_git_root(Path(__file__).parent)
if repo is None:
return {}
# Allow CI / packaging to override these values without spawning git.
# Useful when building wheels from a tarball where .git is absent.
env_branch = os.environ.get("VIBEQC_BUILD_BRANCH")
env_sha = os.environ.get("VIBEQC_BUILD_SHA")
env_tag = os.environ.get("VIBEQC_BUILD_TAG")
sha_full = env_sha or _git(repo, "rev-parse", "HEAD")
sha = sha_full[:7] if sha_full else None
branch = env_branch or _git(repo, "rev-parse", "--abbrev-ref", "HEAD")
if branch == "HEAD":
# Detached HEAD: try to identify by tag or short ref name.
branch = _git(repo, "describe", "--all", "--exact-match") or "HEAD"
tag = env_tag
if tag is None:
# ``--exact-match`` returns non-zero when the current commit isn't
# tagged; _git swallows that and returns None, which is the
# correct "not a release" signal.
tag = _git(repo, "describe", "--tags", "--exact-match")
dirty_status = _git(repo, "status", "--porcelain")
dirty = bool(dirty_status)
is_release = bool(tag) and not dirty
return {
"branch": branch,
"sha": sha,
"sha_full": sha_full,
"dirty": dirty,
"tag": tag,
"is_release": is_release,
}
_EXPECT_VERSION_ENV = "VIBEQC_EXPECT_VERSION"
_EXPECT_SHA_ENV = "VIBEQC_EXPECT_GIT_SHA"
_EXPECT_BRANCH_ENV = "VIBEQC_EXPECT_GIT_BRANCH"
def _pin_env(name: str) -> str | None:
value = os.environ.get(name, "").strip()
return value or None
def _sha_pin_matches(expected: str, actual_full: object, actual_short: object) -> bool:
exp = expected.strip().lower()
if len(exp) < 7:
return False
for candidate in (actual_full, actual_short):
if not candidate:
continue
cand = str(candidate).strip().lower()
if not cand or cand == "unknown":
continue
if len(exp) <= len(cand) and cand.startswith(exp):
return True
return False
def enforce_runtime_pin_from_env() -> None:
"""Fail early when the current build does not match an opt-in pin.
The guard is intentionally environment-driven so release-paper and
validation queues can pin an exact runtime without changing the public
``run_job`` / ``run_periodic_job`` signatures. If none of the pin
variables are set, this function is a no-op.
Supported variables:
- ``VIBEQC_EXPECT_VERSION``: exact package version, e.g. ``0.15.1``.
- ``VIBEQC_EXPECT_GIT_SHA``: commit prefix of at least seven characters.
- ``VIBEQC_EXPECT_GIT_BRANCH``: exact branch name, e.g. ``release``.
"""
expected_version = _pin_env(_EXPECT_VERSION_ENV)
expected_sha = _pin_env(_EXPECT_SHA_ENV)
expected_branch = _pin_env(_EXPECT_BRANCH_ENV)
if not (expected_version or expected_sha or expected_branch):
return
info = build_info()
actual_branch = info.get("branch") or "unknown" if info else "unknown"
actual_sha = info.get("sha") or "unknown" if info else "unknown"
actual_sha_full = info.get("sha_full") or "unknown" if info else "unknown"
mismatches: list[str] = []
if expected_version is not None and expected_version != VIBEQC_VERSION:
mismatches.append(
f"{_EXPECT_VERSION_ENV}={expected_version!r}, "
f"runtime version is {VIBEQC_VERSION!r}"
)
if expected_branch is not None and expected_branch != actual_branch:
mismatches.append(
f"{_EXPECT_BRANCH_ENV}={expected_branch!r}, "
f"runtime branch is {actual_branch!r}"
)
if expected_sha is not None:
if len(expected_sha.strip()) < 7:
mismatches.append(
f"{_EXPECT_SHA_ENV} must contain at least 7 characters "
f"(got {expected_sha!r})"
)
elif not _sha_pin_matches(expected_sha, actual_sha_full, actual_sha):
mismatches.append(
f"{_EXPECT_SHA_ENV}={expected_sha!r}, "
f"runtime git SHA is {actual_sha!r}"
)
if info and bool(info.get("dirty")):
mismatches.append(
f"{_EXPECT_SHA_ENV} is set but the runtime checkout is dirty"
)
if mismatches:
detail = "; ".join(mismatches)
raise RuntimeError(
"vibe-qc runtime pin mismatch: "
f"{detail}. Refusing to start the calculation; unset or correct "
"the VIBEQC_EXPECT_* environment variables for this job."
)
_COPYRIGHT_LINE = "© Michael F. Peintinger . MPL 2.0 . https://vibe-qc.com"
def _version_descriptor() -> str:
"""Compose the line that identifies the current build:
* Tagged release, clean tree -> "Release v0.5.0 \"Wilson's Otter\""
* Dev build, branch + sha -> "dev 0.5.0.dev0 \"Wilson's Otter\" (main @ abc1234)"
* Dev build, dirty tree -> "dev 0.5.0.dev0 \"Wilson's Otter\" (main @ abc1234, dirty)"
* Wheel install (no git info) -> "vibe-qc 0.5.0 \"Wilson's Otter\""
* Pre-codename releases -> no quoted codename suffix
(e.g. "Release v0.4.1", "dev 0.4.0.dev0 (main @ abc1234)")
The codename is bound to the release version; dev builds against
an upcoming release inherit its codename so the banner is fun
even on a working tree. See ``_RELEASE_CODENAMES``.
Generated example artifacts are no longer committed, so release
labeling relies only on the exact tag / clean tree state reported
by :func:`build_info`.
"""
info = build_info()
codename = codename_for_version(VIBEQC_VERSION)
cn_suffix = f' "{codename}"' if codename else ""
if not info:
return f"vibe-qc {VIBEQC_VERSION}{cn_suffix}"
if info.get("is_release"):
return f"Release {info['tag']}{cn_suffix}"
branch = info.get("branch") or "HEAD"
sha = info.get("sha") or "unknown"
suffix = ", dirty" if info.get("dirty") else ""
return f"dev {VIBEQC_VERSION}{cn_suffix} ({branch} @ {sha}{suffix})"
[docs]
def banner(*, width: int = 72) -> str:
"""Return the full banner as a single string, no trailing newline.
``width`` controls the horizontal extent; defaults to a terminal-
friendly 72 characters. The banner is pure ASCII + box-drawing
characters so it renders cleanly in any modern UTF-8 terminal.
"""
versions = library_versions()
libs = (
f"libint {versions['libint']} . "
f"libxc {versions['libxc']} . "
f"spglib {versions['spglib']} . "
f"libecpint {versions['libecpint']} . "
f"fftw3 {versions['fftw3']} . "
f"blas {versions['blas']}"
)
# Optional dispersion backends. Hidden when both are absent --
# users without the `[dispersion]` extra installed see the
# unchanged classic banner. When at least one is installed the
# banner gains a second linkage line.
disp_parts = []
if versions.get("dftd3", "none") != "none":
disp_parts.append(f"dftd3 {versions['dftd3']}")
if versions.get("dftd4", "none") != "none":
disp_parts.append(f"dftd4 {versions['dftd4']}")
disp_line = " . ".join(disp_parts) if disp_parts else None
# Optional MLIP stack (the `[mace]` extra). Hidden when absent -- users
# without `[mace]` see the unchanged banner.
mlip_parts = []
if versions.get("mace", "none") != "none":
mlip_parts.append(f"mace {versions['mace']}")
if versions.get("torch", "none") != "none":
mlip_parts.append(f"torch {versions['torch']}")
if versions.get("e3nn", "none") != "none":
mlip_parts.append(f"e3nn {versions['e3nn']}")
mlip_line = " . ".join(mlip_parts) if mlip_parts else None
descriptor = _version_descriptor()
header = f"{descriptor} -- Quantum chemistry for molecules and solids"
libs_line = f"linked: {libs}"
# Adjust width if any line is wider than requested -- banner should
# never truncate content. Compose each row's full text BEFORE the
# width calc so prefixes ("linked: ", "dispersion: ") are
# accounted for. Before v0.8.1: width used `len(libs) + 4` but the
# actual row is f"linked: {libs}" (8 chars longer). When libs was
# the longest source, width was undersized by 8 -> _row pad went
# negative -> closing ║ jammed up against the last char on the libs
# row. Surfaced once libecpint joined the linked: line and OpenBLAS
# +LAPACKE pushed libs past header in length on typical builds.
row_widths = [
len(header) + 4,
len(_COPYRIGHT_LINE) + 4,
len(libs_line) + 4,
]
if disp_line is not None:
row_widths.append(len(f"dispersion: {disp_line}") + 4)
if mlip_line is not None:
row_widths.append(len(f"mlip: {mlip_line}") + 4)
width = max(width, *row_widths)
bar = "═" * (width - 2)
top = f"╔{bar}╗"
bot = f"╚{bar}╝"
def _row(text: str) -> str:
pad = (width - 2) - len(text)
return f"║ {text}{' ' * (pad - 1)}║"
rows = [
top,
_row(header),
_row(_COPYRIGHT_LINE),
_row(libs_line),
]
if disp_line is not None:
rows.append(_row(f"dispersion: {disp_line}"))
if mlip_line is not None:
rows.append(_row(f"mlip: {mlip_line}"))
rows.append(bot)
return "\n".join(rows)
[docs]
def print_banner(*, file: TextIO = sys.stdout, width: int = 72) -> None:
"""Print the banner to ``file`` (default: stdout)."""
print(banner(width=width), file=file)