Input scripts and output files¶
A classic quantum-chemistry workflow means running a job and getting back
a text log, plus files a viewer can open. vibeqc.run_job bundles that
up so you don’t have to wire it together by hand: one call writes the
formatted output, the molden orbital file, and, for optimization
runs, a trajectory animation.
Writing an input script¶
An input “script” in vibe-qc is just a Python file. The conventional shape:
# input-h2o.py
from pathlib import Path
from vibeqc import Molecule, run_job
HERE = Path(__file__).parent
mol = Molecule.from_xyz(HERE / "h2o.xyz")
run_job(
mol,
basis="6-31g*",
method="rhf",
output=HERE / "output-h2o",
)
Run it like any Python file:
python3 input-h2o.py
A spread of ready-to-run example inputs lives under
examples/molecular/
and examples/periodic/
single-point RHF / DFT, open-shell UHF, BFGS geometry optimization, MP2 / double hybrids, dispersion-corrected optimization, cube output, the SCF Fock-build modes, and more. Copy any of them as a template.
Output files¶
Given output="output-h2o", run_job writes up to a dozen file
families named by the same stem. Always-on for every molecular
run: .out (text log), .system (TOML manifest), .molden
(orbitals), .xyz (final geometry), .bibtex + .references
(citations, v0.8.x+, see citations), and
.population.{txt,json} (Mulliken / Löwdin / Mayer / dipole, also
v0.8.x+). Conditional / opt-in: .qvf, vibe-qc’s native
single-file visualization archive bundling structure, density, and
orbitals for vibe-view
(opt-in via output_qvf=); .traj (geometry optimisation
only); .perf (opt-in via perf_log=); .scf.jsonl (opt-in via
structured_log=); .density.cube and .{homo,lumo,…}.cube
(opt-in via write_cube=); .dump (only on SCF failure).
Periodic runs (run_periodic_job) emit the same family plus
.POSCAR and .xsf for the crystal structure.
output-h2o.out, the text log¶
Plain ASCII, readable in any editor. Sections, in order:
Banner, vibe-qc + libint + libxc + spglib versions, for provenance. Identical to
vibeqc.print_banner().Job header, method + basis.
Initial atom table, Z + Cartesian bohr + charge + multiplicity
electron count.
Optimization block (only when
optimize=True), target convergence, final optimized geometry.SCF trace, iteration-by-iteration energy, ΔE, commutator norm, DIIS history length. Flags
converged/NOT converged.Energy components (DFT only), nuclear repulsion, electronic, Coulomb J, HF-exchange K, XC, total.
Orbital-energy table, all occupied MOs and up to
n_virtual=5virtual MOs (override vian_virtual=...), with HOMO/LUMO markers and the HOMO-LUMO gap in Ha and eV. For UHF/UKS, separate Alpha and Beta blocks with per-spin HOSMO / LUSMO markers and per-spin gaps.References block (v0.8.x+),
## Referencessection listing every paper that should be cited for the calculation just run. Auto-assembled from the bundled citation database; the matching BibTeX entries live in the.bibtexsibling.
The same content is available programmatically as
vibeqc.format_scf_trace(result, molecule=...), pass it a file handle
and a molecule to get a string you can log, print, or splice into your
own output layout.
output-h2o.qvf: native visualization archive (opt-in)¶
vibe-qc’s native visualization format, QVF (.qvf), is a single
ZIP archive that bundles a calculation’s structure, electron density,
orbitals, and basis + MO coefficients into one file, opened in
vibe-view, vibe-qc’s GPU-accelerated viewer. It is the recommended
way to get visualization output: rather than wiring a .molden and a
handful of .cube files together for a third-party viewer, one
output_qvf=True writes everything in one place.
run_job(mol, basis="6-31g*", method="rhf",
write_cube=["density", "homo", "lumo"], # which fields to evaluate
output_qvf=True, output="output-h2o")
# → output-h2o.qvf
The archive carries typed sections: structure, volume.density, one
volume.orbital per requested MO, wavefunction.gto (basis +
coefficients, so the viewer resamples any orbital on demand), and the
citations. Open it with:
vibe-view open output-h2o.qvf
run_periodic_job(..., output_qvf=True) writes a periodic .qvf the
same way (structure, density, and bands / DOS where computed). The
format is open and versioned: see the
QVF tech spec, The QVF file format, end to end
(format) and vibe-view: an end-to-end walkthrough
(viewer), and the motivation post Quantum chemistry needs a modern
file format.
For concrete periodic archives, the static
example-output catalog includes the curated
chi-ccm-b-qvf bundle: two finite-BvK chi-CCM-B QVF files, full .out
logs, sanitized .system manifests, and headless vibe-view captures for
structure, density, orbitals, replication, and Wannier-centre overlays.
It also includes mgo-route-gdf-bipole, a current MgO primitive
RHF/STO-3G 2 x 2 x 2 GDF/BIPOLE route bundle with full logs, sanitized
manifests, QVFs, summaries, and vibe-view captures. The BIPOLE QVF is
structure/properties/SCF-history only because its optional density/DOS
artifact generation warned after the SCF had converged.
output-h2o.bibtex / output-h2o.references, auto-citations¶
Two siblings that pair with the in-.out references block (see
the citations user guide for the full schema):
.bibtex, one@article/@softwareentry per cited work, in citation order. Drop into\bibliography{output-h2o.bibtex}and\cite{...}each entry by itsbibtex_key..references, Chicago-style numbered list, human-readable. Open in a plain-text editor when you want to glance at the bibliography without firing up LaTeX.
Both are regenerated on every run, so a re-run with a different
functional / basis / dispersion automatically produces the updated
bibliography. Routing gaps (unknown basis, custom libxc id without a
route) surface as # no citation route for … warning lines at the
bottom of the .references file; the job itself never fails on a
gap.
output-h2o.molden, molecular orbitals¶
Molden-format file carrying:
The geometry in atomic units.
The full basis set (per-atom, per-shell exponents and contraction coefficients, raw, primitive normalisation is reapplied by the reader).
Every molecular orbital: symmetry label (
Afor now, vibe-qc is not yet symmetry-adapted), orbital energy in Hartree, spin, occupancy (2 for occupied restricted, 1 for each alpha/beta spin-occupied, 0 otherwise), then the AO coefficients reordered from libint’sm = -L..+Lconvention to Molden’s(m=0, +1, -1, +2, -2, ...)ordering so the file is drop-in for any molden-aware viewer (Jmol, Avogadro, Molden itself, IQmol, MolView).
For unrestricted (UHF/UKS) results the file contains two MO blocks, first the Alpha spin, then the Beta spin, as the Molden format requires.
Open any .molden file via:
# Jmol (cross-platform, Java):
jmol output-h2o.molden
# Avogadro 2 (cross-platform):
avogadro output-h2o.molden
# MolTUI — terminal-only, no GUI required (great over SSH).
# Install via `pip install -e '.[viewer]'` from the vibe-qc checkout
# or via `./scripts/install_optional_tools.sh`. See the
# [installation page](../installation.md#optional-terminal-viewer-moltui).
moltui output-h2o.molden
output-h2o.system, runtime manifest¶
Plain TOML pinning the runtime environment that produced the
.out. The .out file carries the chemistry; the .system
sibling carries the hardware, linked-library, and timestamp
context needed to interpret a wall-time figure or reproduce a
calculation on a different box. It also records the validation
boundary: external QC programs are references only, run
out-of-process, and are not imported as vibe-qc backends. Without it,
an .out says
“SCF total: 0.015 s” with no indication whether that’s a fast
machine, a slow one, or a single-threaded build.
Sample manifest:
# vibe-qc system manifest — written alongside output-<job>.out by run_job(...).
# Captures the runtime environment so generated calculation outputs are
# reproducible and wall-time numbers are interpretable. The [plan]
# section is the declared output contract (written once, at job start);
# the [outputs] section is the running status (rewritten as each file
# lands). External QC programs are validation references only.
[vibeqc]
version = "0.12.0"
codename = "Knuth's Beaver"
git_sha = "0123456789abcdef0123456789abcdef01234567"
git_branch = "main"
is_release = true
[host]
hostname = "peintinger-m2.local"
os = "Darwin"
os_release = "23.4.0"
os_pretty = "macOS 14.4"
arch = "arm64"
[cpu]
model = "Apple M2 Pro"
physical_cores = 10
logical_cores = 16
omp_threads_used = 12
[memory]
total_gb = 32.0
available_gb = 24.0
[python]
version = "3.14.0"
implementation = "CPython"
executable = "~/.venv/bin/python"
[libraries]
libint = "2.13.1"
libxc = "7.0.0"
spglib = "2.7.0"
libecpint = "1.0.7"
fftw3 = "3.3.10"
[validation]
external_programs_policy = "External QC programs are validation references only."
execution_boundary = "Run external programs out-of-process and parse their outputs; do not import them as vibe-qc backends."
native_backend_policy = "vibe-qc runtime methods execute vibe-qc-owned native or Python code."
[run]
timestamp_iso = "2026-04-29T21:42:14-04:00"
wall_seconds = 0.084
basename = "input-h2o-rhf"
pid = 51284
# v0.8.x+ Phase-O1 additions: declarative pre-flight plan + running
# outputs status. vq's `--vibeqc-preflight` reads [plan] to know what
# files to expect; vq's status polling reads [outputs] for liveness.
[plan]
stem = "output-h2o"
job_kind = "molecular_scf"
method = "RHF"
basis = "6-31g*"
functional = ""
options_digest = "f3a2c1..."
[[plan.files]]
role = "log"
path = "output-h2o.out"
format = "text"
always = true
description = "Human-readable SCF log."
[[plan.files]]
role = "manifest"
path = "output-h2o.system"
format = "toml"
always = true
description = "Runtime manifest (this file)."
# … one row per declared artefact (.molden, .xyz, .bibtex,
# .references, .population.{txt,json}, optionally .traj / .perf /
# .scf.jsonl / .dump / .density.cube / .homo.cube / .lumo.cube / …)
[outputs]
status = "complete" # running | complete | crashed | dry_run
finished_at_iso = "2026-04-29T21:42:14-04:00"
[[outputs.files]]
path = "output-h2o.out"
written = true
bytes = 4231
sha256 = "ab12cd34..."
checksum_status = "sha256"
wall_time_s = 0.082
[[outputs.files]]
path = "output-h2o.system"
written = true
bytes = 0
sha256 = ""
checksum_status = "self-excluded"
wall_time_s = 0.084
# ... one row per declared artefact, with its write/checksum state and
# wall-time-since-job-start.
The git_sha is the full immutable 40-character commit ID when Git provenance
is available (builds without Git metadata use the unknown sentinel).
checksum_status is pending, sha256, unavailable, failed, or
self-excluded. The .system row is always self-excluded: a file cannot
truthfully contain the digest or byte count of its own final bytes, because
inserting either value changes those bytes. Its empty SHA and zero byte count
are explicit sentinels, not a failed validation. Older manifests that contain
a self-digest captured the pre-finalization file and that digest must not be
used to validate the fetched .system file.
The shape is fixed, every section + key listed above is always
present, even when a probe falls back to "unknown". Downstream
parsers don’t need to handle missing keys, only the unknown
sentinel value. The [plan] + [outputs] sections were added in
the v0.8.x output-module work (Phase O1 of
docs/design_output_module.md) and
are strictly additive, pre-v0.8.0 parsers that only read the older
sections still work without changes.
Privacy: hostname opt-out. For runs you plan to share
publicly, pass record_hostname=False to run_job, or set the
VIBEQC_NO_HOSTNAME=1 environment variable to opt out globally.
Either lever emits hostname = "<redacted>" (the field stays
present so the TOML shape is stable for parsers, only the value
is masked). Use the env var for public examples, paper artifacts,
and shared issue reproductions so local machine names do not leak.
Other manifest fields (CPU model, OS, memory, library versions) are
not redacted; the redaction is scoped to the hostname only.
Read it from Python with stdlib tomllib:
import tomllib
with open("output-h2o.system", "rb") as f:
manifest = tomllib.load(f)
print(manifest["cpu"]["model"], manifest["run"]["wall_seconds"])
Reproducing an example output walks through using the manifest to compare two local runs of the same input.
output-h2o.xyz, final geometry (v0.8.x+)¶
Always-on alongside .molden: an ASE-style extended XYZ with
the final geometry in Ångström, the SCF total energy on the
comment line as energy=<Ha>, and lattice metadata for periodic
jobs (Phase O5). Useful as input for downstream tools (a Gaussian
input generator, an ORCA parity run, a viewer that prefers
plain-text geometry over .molden):
3
energy=-76.04939147 prop=energy
O 0.0000000000 0.0000000000 0.0000000000
H 0.0000000000 0.7569997744 -0.5184799434
H 0.0000000000 -0.7569997744 -0.5184799434
For periodic runs the comment line carries
Lattice="ax ay az bx by bz cx cy cz" pbc="T T T" so any
ASE-aware tool reads the cell correctly. Opt out with
write_xyz_file=False on run_job / run_periodic_job.
output-crystal.POSCAR / output-crystal.xsf / output-crystal.cif, periodic structure¶
run_periodic_job emits three always-on crystal-structure
siblings (Phase O5 + D2, v0.8.x+):
{stem}.POSCAR, VASP-5 POSCAR with selective-dynamics off; drop straight into VASP, pymatgen, or any tool that reads the POSCAR format.{stem}.xsf, XCrySDen XSF structure block (lattice in Ångström, atoms in fractional coordinates); read by VESTA and XCrySDen for crystal-structure visualisation.{stem}.cif, IUCr-standard Crystallographic Information File. Singledata_vibeqcblock with cell parameters in Å / degrees,_symmetry_space_group_name_H-M = 'P 1'and the identity symmetry op, and per-atom site labels indexed by element (Mg1,O1,Mg2,O2, …). Read by pymatgen, ASE, VESTA, Materials Project, and the Crystallography Open Database.
In addition to the four format-specific siblings,
run_periodic_job also emits a {stem}.xyz in Extended-XYZ form
(the ASE-convention Lattice="..." / Properties=... / pbc=...
keys carried in the comment line) so vanilla XYZ readers see the
geometry and ASE-aware readers recover the periodic cell.
All four files declare the conventional cell from the
PeriodicSystem input, vibe-qc does not currently relax cell
parameters, so the cell on the output matches the cell on input.
Opt out of any one with run_periodic_job(..., write_poscar_file=False) / write_xsf_structure_file=False /
write_cif_file=False.
{stem}.density.xsf, periodic volumetric density (opt-in)¶
When write_density=True is passed to run_periodic_job,
vibe-qc evaluates the SCF electron density on a primitive-cell
real-space grid and writes it as a DATAGRID_3D_density XSF
block:
run_periodic_job(
system, basis,
method="RHF",
output="nacl",
write_density=True,
density_spacing_bohr=0.2,
)
# → nacl.density.xsf
Open in VESTA, XCrySDen, or moltui nacl.density.xsf to see
the isosurface inside the unit cell. Control the voxel spacing
with density_spacing_bohr= (default 0.2 bohr ≈ 0.11 Å).
{stem}.bxsf, band energies on a k-mesh (manual)¶
BXSF is not auto-emitted by run_periodic_job, you call
write_bxsf yourself after a multi-k SCF run to produce
Fermi-surface data. See
Volumetric data: BXSF
for the full recipe.
output-h2o.population.{txt,json}, properties dump (v0.8.x+)¶
Two always-on siblings carrying the population-analysis + dipole
data in machine-readable form. The matching block in .out is for
human reading; these files are the parseable form for
dashboards, regression scripts, and downstream analysis.
.population.txt, tab-separated, six#-commented sections: Mulliken charges, Löwdin charges, Mayer bond orders (top-N, default threshold 0.1), Wiberg bond indices (top-N), NPA atomic charges, and the dipole moment. Loadable into pandas / awk / spreadsheet importers with#as the comment marker..population.json, one JSON object with top-level keysmulliken/loewdin/mayer/wiberg/npa/dipole/errors, each shaped as a list of records (or a single object fordipole/errors). Drop-in forjson.load(...).
The Wiberg and NPA sections are computed by the bond-analysis modules – see the bond analysis guide for the theory, the direct APIs (NBO search, EDA, orbital entanglement), and the citation surface.
A property-computation failure (e.g. Mayer bond orders on a near-singular overlap) on one section does NOT suppress the others
partial success is preserved and the missing section is reported in the
errorsdict / via a# section: N/A - <error>line.
Opt out with run_job(..., write_population_file=False) for batch
runs that won’t need the population data.
output-h2o.density.cube / output-h2o.{homo,lumo}.cube, volumetric data (opt-in, v0.8.x+)¶
Gaussian-cube volumetric files for VMD / Avogadro / Jmol /
ChimeraX visualisation. Opt-in via run_job(..., write_cube=...):
|
Files written |
|---|---|
|
|
|
The corresponding MO cube |
|
Offset MO labels |
|
That MO index (0-based) |
|
Any mix of the above |
Grid spacing + padding default to 0.2 / 4.0 bohr; pass
cube_spacing= / cube_padding= to tune. UHF/UKS density cubes
are the total density D_α + D_β. Each cube file is wrapped in
its own try/except so a single grid-evaluation failure on one MO
doesn’t block the others.
vq.run_job(mol, basis="6-31g*", method="rhf", output="h2o",
write_cube=["density", "homo", "lumo"])
# → h2o.density.cube, h2o.homo.cube, h2o.lumo.cube
output-h2o.traj, optimization trajectory¶
Emitted only when optimize=True. It’s an ASE binary trajectory,
one frame per optimizer step, containing atomic positions + energy +
forces. View it as an animation with:
ase gui output-h2o.traj
Convert to XYZ for tools that prefer that format:
ase convert output-h2o.traj output-h2o-frames.xyz
Or iterate frames programmatically:
from ase.io import read
frames = read("output-h2o.traj", index=":")
for step, atoms in enumerate(frames):
print(step, atoms.get_potential_energy())
Progress logging¶
Long calculations, multi-minute molecular SCFs on a big basis, periodic bulk runs with EWALD_3D and a multi-k mesh, used to be silent until the SCF returned. The headline question this answers: is the calculation stuck or actually running?
run_job defaults to live progress on. The job emits a banner,
per-stage milestones, and a final summary to stdout (line-flushed),
and the .out file is line-buffered, so the canonical remote-job
workflow shows progress in real time without any extra setup:
nohup python LiH.py > LiH.log 2>&1 &
tail -f LiH.log # mirrors progress to the captured log
tail -f output-LiH.out # same picture from the .out file directly
This emits, in order:
a banner naming the method, basis, functional, and thread count;
one line per setup stage (
geometry_optimization,write_molden, …) with elapsed wall-time on completion;the SCF banner (“Starting molecular SCF (RHF) …”);
the full SCF trace and orbital tables when the SCF returns;
a
Job total X.XXs - output written to …summary line.
Disabling¶
Two equivalent levers, both restore the historical silent behavior:
# Per-call:
run_job(mol, basis="6-31g*", method="rhf", output="x",
progress=False)
# Globally for a shell / batch job (only takes effect when
# `progress` is left at its default; explicit `progress=` kwargs win):
export VIBEQC_LIVE_LOGGING=0
A VIBEQC_LIVE_LOGGING=0 env var is the right answer for batch
scripts that don’t want to edit every input file. Explicit
progress=True / progress=False / a ProgressLogger instance
always wins, so a debugging session can re-enable progress for one
shell.
Verbosity¶
The verbose= kwarg on run_job (added in v0.5.3) tunes how
much detail the live progress + .out carry. Levels follow the
PySCF convention, each level is a strict superset of the one
below, so bumping verbose only adds output:
level |
what is emitted |
|---|---|
0 |
silent, nothing live (the |
1 |
banner + warnings + final SCF status only |
2 |
add per-stage milestones + |
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 (overlaps the post-mortem |
Two equivalent ways to set the level:
# Per-call:
run_job(mol, basis="6-31g*", method="rhf", output="x",
verbose=2)
# Globally for a shell / batch job (only takes effect when
# `verbose` is left at its default of None; explicit `verbose=`
# kwargs win):
export VIBEQC_VERBOSE=2
A junk env value (typo, leftover) silently falls back to the
package default, an overnight batch shouldn’t die because of
VIBEQC_VERBOSE=verbose. Levels 1 and 2 are the right knob for
batch sweeps that want one summary per job without per-iter
spam; level 5+ is for debugging a specific run that’s behaving
oddly. The level only gates the live emit, the
format_scf_trace block in {output}.out is unaffected and
always carries the full per-iteration history.
Stdlib logging integration¶
When a project already pipes everything through logging
(rotating files, syslog, JSON-to-Loki, dictConfig), the
use_logging=True kwarg routes vibe-qc’s progress through the
same stack instead of bare stdout writes:
import logging
import vibeqc as vq
logging.basicConfig(level=logging.INFO)
vq.run_job(mol, basis="6-31g*", method="rhf", output="x",
use_logging=True)
Banners, milestones, and the final SCF summary land at INFO;
per-iteration SCF rows at DEBUG; warnings at WARNING. The
logger name is vibeqc.run_job, so a dictConfig block can
target it specifically:
logging.config.dictConfig({
"version": 1,
"handlers": {
"scf_log": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "scf.log", "maxBytes": 10_000_000,
"backupCount": 5,
},
},
"loggers": {
"vibeqc.run_job": {"handlers": ["scf_log"], "level": "INFO"},
},
})
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, so
a silent run stays silent regardless of the active logging
config.
Routing, ProgressLogger¶
For finer control, tee the same trace to a persistent file, or
thread a single logger through nested calls, instantiate a
vibeqc.ProgressLogger directly:
import vibeqc as vq
plog = vq.ProgressLogger(log_path="lih.progress.log", verbose=True)
vq.run_rhf_periodic_scf(system, basis, kpoints, opts, progress=plog)
The same progress= kwarg is accepted by every periodic SCF entry
point (run_rhf_periodic_scf, run_rks_periodic_scf, the
EWALD_3D variants, etc.), pass True to get stdout, or a logger
for routing.
Coverage¶
Live per-iteration progress is available everywhere the SCF loop runs in Python, which is every EWALD_3D path, including the heavy multi-k bulk runs that motivated the feature:
Entry point |
Backend |
Live per-iter? |
|---|---|---|
|
Python |
yes |
|
Python |
yes |
|
Python |
yes |
|
Python |
yes |
|
Python |
yes |
|
C++ |
banner + post-hoc summary only |
|
C++ |
banner + post-hoc summary only |
|
wraps C++ |
banner + line-buffered |
The C++-driven SCFs don’t expose a Python progress callback today,
so the per-iteration trace is collected into result.scf_trace and
written to the .out file when the SCF returns. The pre-SCF banner
and post-SCF summary still emit live, so a remote-job operator at
least sees that the calculation is alive.
Performance debugging¶
The post-mortem companion to live progress logging. Live logging shows progress during a run; the perf log shows where the time went afterwards. The two pair: live for “is the SCF stuck?”, perf for “why is my LiH / pob-TZVP run taking 20 minutes, is it J, K, XC quadrature, or Bloch sums?”.
run_job writes a {output}.perf sibling when you pass
perf_log=True:
run_job(mol, basis="cc-pVDZ", method="rks", functional="pbe",
output="output-h2o", perf_log=True)
# -> output-h2o.out / .molden / .system / .perf
The file is plain text, sortable by total wall-time. A typical report:
========================================================================
vibe-qc performance / debug log
========================================================================
Total wall time: 19.599 s
OMP threads: 12
Phases tracked: 4
Phase summary (sorted by total wall time, descending)
------------------------------------------------------------------------
phase n wall cpu % wall par
----------------------------------------------------------------------
periodic.integrals_lattice 1 4.68 ms 9.86 ms 0.0% 0.18
periodic.compute_nuclear_lattice 1 4.17 ms 8.82 ms 0.0% 0.18
periodic.compute_overlap_lattice 1 0.30 ms 0.64 ms 0.0% 0.18
periodic.compute_kinetic_lattice 1 0.16 ms 0.35 ms 0.0% 0.18
Memory snapshots (RSS in MiB)
------------------------------------------------------------------------
label t (s) RSS (MiB)
------------------------------------------------------------
start_of_scf 0.013 149.3
end_of_scf 19.598 152.1
SCF iterations
------------------------------------------------------------------------
iter E (Ha) dE ||[F,DS]|| DIIS wall (s)
----------------------------------------------------------------------
1 -28.9250484403 -- 4.437e-02 - 3.895
2 -28.9261305496 -1.082e-03 3.304e-02 1 7.747
...
========================================================================
Sections, in order:
Header, total wall, OMP thread count, phase count.
Phase summary, one row per
PerfScopeopened during the run, sorted by wall-time. Theparcolumn is parallelism = CPU time / (wall time × threads); 1.0 means perfect OpenMP scaling, < 0.7 flags an under-parallelised hot path.Under-parallelised hot paths, auto-flag block listing any phase that consumed more than 5% of wall time and ran at parallelism < 0.7×. The under-parallelised hot paths users care about.
Memory snapshots, labeled RSS samples (
start_of_scf,end_of_scf, …) so you can see RSS growth caused by the Fock build separate from the basis-set / pre-flight overhead.SCF iterations, per-iteration table (energy, ΔE, ‖[F,DS]‖, DIIS subspace, wall-time-since-SCF-start). The post-mortem analogue of live progress logging’s per-iter emission.
The thread denominator comes from the native OpenMP runtime and explicit job
allocation limits such as OMP_NUM_THREADS, SLURM_CPUS_PER_TASK, or
PBS_NP; the tightest applicable limit wins. Host-wide available-core counts
are not treated as allocated CPUs. If no runtime/allocation signal is
available, the report conservatively uses one thread instead of guessing from
the machine size.
Three ways to enable¶
# (1) Env var — common case for one-off jobs:
VIBEQC_PERFLOG=output.perf python my-calc.py
# (2) Programmatic context manager — wrap a region:
import vibeqc as vq
with vq.perf_log("output.perf"):
result = vq.run_rhf(mol, basis, opts)
hess = vq.compute_hessian_rhf_analytic(...)
# All work inside the block accumulates into the same tracker;
# the report is written when the block exits.
# (3) run_job kwarg — one-shot:
vq.run_job(mol, basis="cc-pVDZ", method="rks", functional="pbe",
output="x", perf_log="x.perf") # explicit path
vq.run_job(mol, basis="cc-pVDZ", method="rks", functional="pbe",
output="x", perf_log=True) # → x.perf sibling
The three knobs feed the same vibeqc.PerfTracker accumulator.
Explicit perf_log= always wins over the env var; off by default.
Reading the report programmatically¶
The tracker is a plain Python object, call sites that want to script around the perf data can read it directly:
import vibeqc as vq
with vq.perf_log() as tracker:
vq.run_rhf(mol, basis, opts)
for phase in sorted(tracker.phases.values(),
key=lambda p: -p.wall_s):
print(f"{phase.name}: {phase.wall_s:.3f}s "
f"({phase.n_calls} calls)")
tracker.phases, tracker.scf_iters, and
tracker.memory_snapshots are public attributes,
full API in vibeqc.PerfTracker.
Coverage¶
What’s instrumented today:
Phase |
Driver |
Live perf rows? |
|---|---|---|
|
wraps everything inside |
yes |
|
ASE BFGS |
yes |
|
libint |
yes |
|
C++ molecular SCF (one row total) |
one row total |
|
molden writer |
yes |
|
Python lattice integrals |
yes |
|
Python |
per-iter wall in SCF table |
The C++ kernels (compute_eri, build_coulomb, build_exchange,
xc_eval, diag_k, s_inverse_sqrt_complex, bloch_sum) are
not instrumented yet, 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.
Structured machine-readable log¶
A third observability surface (after the human-readable .out and
the post-mortem .perf): one JSON record per SCF transition,
written line-flushed to {output}.scf.jsonl so dashboards and
analysis scripts can ingest convergence data without screen-
scraping the text log.
Format: NDJSON (one JSON object per line, no enclosing array).
Every record carries "event" plus a per-event payload. Event
names + field names are append-only, never renamed or removed,
so v0.6 callers stay forward-compatible with v0.7+ additions.
A typical sequence for a successful molecular SCF (one JSON record
per line, the format Pygments calls text, not jsonl):
{"event":"banner","timestamp":"...","vibeqc_version":"0.6.0.dev0","libint":"2.13.1","libxc":"7.0.0","spglib":"2.7.0","run_fingerprint":"abc1234567890abc"}
{"event":"job_start","timestamp":"...","method":"rhf","basis":"sto-3g","functional":null,"optimize":false,"threads":12,"n_atoms":2,"charge":0,"multiplicity":1,"n_electrons":2,"output_stem":"h2"}
{"event":"memory_estimate","timestamp":"...","total_gb":0.15,"raw_total_bytes":104858640,"headroom_factor":1.5,"by_category":{"ERI tensor":128,"Fock + density + 1e":256,"DIIS history":512,"MO workspace":144,"Python runtime + NumPy overhead":104857600}}
{"event":"scf_iter","timestamp":"...","iter":1,"energy":-0.71,"dE":null,"grad_norm":4.7e-16,"diis_subspace":1}
{"event":"scf_iter","timestamp":"...","iter":2,"energy":-1.117,"dE":-0.398,"grad_norm":0.0,"diis_subspace":2}
{"event":"scf_converged","timestamp":"...","n_iter":3,"energy":-1.1167143251,"converged":true}
{"event":"properties","timestamp":"...","mulliken":[6.7e-16,-4.4e-16],"loewdin":[7.8e-16,0.0],"dipole":{"x":0.0,"y":0.0,"z":-7.8e-16,"total":7.8e-16,"total_debye":2.0e-15}}
{"event":"job_end","timestamp":"...","total_wall_s":0.099,"scf_wall_s":0.004,"opt_wall_s":0.0,"n_iter":3,"converged":true,"energy":-1.1167143251,"out_path":"h2.out"}
Strict JSON: NaN / ±Infinity are coerced to null so the file
parses cleanly with jq, jq -c '.', and any other strict-JSON
tool. The first iteration’s dE is null (not 0), same
placeholder semantics as the human-readable trace.
The run_fingerprint field is a 16-character hex digest of the
identifying inputs (method + basis + functional + atoms + charge
multiplicity). Two runs with the same fingerprint are calculating the same thing, useful for “did this run change?” checks in CI dashboards.
Three ways to enable¶
# (1) Env var — common case for one-off jobs:
VIBEQC_STRUCTURED_LOG=output.scf.jsonl python my-calc.py
# (2) Programmatic context manager — wrap a region:
import vibeqc as vq
with vq.structured_log("output.scf.jsonl"):
vq.run_rhf(mol, basis, opts)
# All work inside the block emits to the same file; periodic SCFs
# emit per-iter rows live via the same context-var funnel that
# vibeqc.ProgressLogger uses.
# (3) run_job kwarg — one-shot:
vq.run_job(mol, basis="cc-pVDZ", method="rks", functional="pbe",
output="x", structured_log=True) # → x.scf.jsonl
vq.run_job(mol, basis="cc-pVDZ", method="rks", functional="pbe",
output="x", structured_log="other.jsonl") # explicit
Off by default, run_job only writes the file when the caller
opts in. Explicit structured_log= always wins over the env var.
Tail-friendly¶
The file is line-flushed: a tail -f output.scf.jsonl shows
records as they’re emitted (one per SCF iteration during the
SCF, plus banner / properties / job_end at boundaries). Pair
with jq for a live convergence monitor:
tail -f output.scf.jsonl | jq -c 'select(.event == "scf_iter") | [.iter, .energy, .dE, .grad_norm]'
Coverage¶
Event |
Source |
When emitted |
|---|---|---|
|
|
first record, carries linked-library versions + run_fingerprint |
|
|
after method resolution, before any work |
|
|
after the memory pre-flight |
|
C++ molecular SCF (replayed from |
per SCF iteration |
|
|
once after the SCF loop |
|
|
post-SCF, when properties succeed; carries Mulliken / Löwdin / dipole |
|
|
when the C++ SCF raises; the exception still propagates after the dump |
|
|
last record, total_wall_s + out_path. |
Crash dumps¶
When an SCF fails ungracefully, raised exception (NaN in the
density, severe linear dependence, OOM) or runs to max_iter
without converging, run_job writes a snapshot to
{output}.dump plus binary attachments. Three-line bug report:
attach output.dump + output.dump.density.npy + the input
script and the maintainer can reconstruct the exact failing
state via vibeqc.load_dump.
The dump is on by default: post-mortem reproducibility costs
zero bytes on success and saves a re-run on failure. Disable
per-call with crash_dump=False or globally with
VIBEQC_NO_CRASH_DUMP=1 in the environment.
File layout¶
For output="output-h2o" and a NaN failure at SCF iteration 5:
output-h2o.dump, TOML with[crash],[scf.last_iter],[geometry],[molecule],[options],[hint], and[attachments]sections.output-h2o.dump.density.npy, last-iteration density matrix.output-h2o.dump.fock.npy, last-iteration Fock matrix (when present).output-h2o.dump.mo.npy, current MO coefficients (when present).
A typical .dump body (real .dump files are TOML; the example
below is rendered as plain text because the illustrative ... and
nan placeholders below aren’t valid TOML literals on their own):
[crash]
when = "2026-04-30T20:27:26-05:00"
phase = "scf_iteration_5"
exception_type = "RuntimeError"
exception = "NaN in density matrix"
n_iters_completed = 4
[scf.last_iter]
iter = 4
energy = -74.123
delta_e = nan
grad_norm = 1.7e+02
diis_subspace = 4
[geometry]
atoms = [
{ Z = 8, x = 0.0, y = 0.0, z = 0.0 },
...
]
[molecule]
charge = 0
multiplicity = 1
n_atoms = 3
[options]
max_iter = 100
damping = 0.0
...
[hint]
likely_cause = "DIIS instability — try damping=0.5, level_shift=0.5, or DIIS=False to fall back to plain Roothaan iterations."
[attachments]
files = "output-h2o.dump.density.npy, output-h2o.dump.fock.npy, output-h2o.dump.mo.npy"
The [hint] block runs a small heuristic against the exception
text + type to produce a one-line likely_cause. It’s
best-effort and never blocks the dump if the keyword search
doesn’t match anything specific.
Reproducer recipe¶
import vibeqc as vq
dump = vq.load_dump("output-h2o.dump")
density = dump["arrays"]["density"] # numpy ndarray, last iteration
options = dump["options"] # dict, ready to feed back in
print(dump["crash"]["phase"], "→", dump["hint"]["likely_cause"])
vibeqc.load_dump returns a nested dict shaped like the TOML
sections, plus an extra "arrays" key carrying every sibling
.dump.<name>.npy rebuilt with numpy.load. Pair with the
input script in the bug report and the maintainer reconstructs
the failing state bit-for-bit.
Failure modes that trigger a dump¶
Failure |
Where the dump fires |
|---|---|
C++ SCF raises (NaN, lin-dep, memory error) |
|
SCF returns non-converged (max_iter exceeded) |
|
Pre-SCF errors (basis-set construction, memory pre-flight abort) |
not currently captured, the .out file holds the error message; a future patch will widen the dump scope |
The exception path always re-raises, crash_dump=True does NOT
swallow failures; it just makes them debuggable. The max-iter
path returns the non-converged result so callers that explicitly
want to inspect a failed iterate keep working.
run_job parameters¶
Parameter |
Default |
Purpose |
|---|---|---|
|
required |
|
|
required |
libint-recognized basis name |
|
|
|
|
|
XC functional name for RKS/UKS (e.g. |
|
|
path stem; files become |
|
|
run BFGS (via ASE) before the final SCF |
|
|
optimizer convergence in eV/Å (ASE convention) |
|
|
optimizer iteration limit |
|
|
emit the .molden file |
|
|
emit |
|
|
emit |
|
|
volumetric cubes; |
|
|
grid spacing + padding for cubes; ignored when |
|
|
bundle structure + density + the |
|
|
emit |
|
|
pre-flight only: build the OutputPlan, write |
|
|
live progress logger; |
|
|
integer 0..9 (PySCF convention) gating how much live detail emits; 0 silent, 4 default with per-iter rows, 5 adds memory snapshots; |
|
|
route progress through |
|
|
post-mortem perf breakdown; |
|
|
machine-readable NDJSON; |
|
|
post-mortem dump on SCF failure ( |
|
|
record live hostname in |
|
|
fine SCF control, override the relevant options struct |
method="auto" resolves to:
functionalset + multiplicity 1 → RKSfunctionalset + multiplicity ≥ 2 → UKSno
functional+ multiplicity 1 → RHFno
functional+ multiplicity ≥ 2 → UHF
The return value is the underlying SCF result object (RHFResult,
UHFResult, RKSResult, or UKSResult), so you can continue in
Python after the call to inspect MO coefficients, density matrices, or
feed the result to downstream post-SCF analysis.
When to use run_job vs the low-level drivers¶
run_job optimises for the 80% case: one method on one geometry,
producing a log and an orbital file. If you want to
sweep over basis sets / functionals in one script,
compose your own output format,
or call the SCF drivers with non-default numerical integration grids,
reach for the low-level API (run_rhf, run_rks, etc.) and
format_scf_trace directly. For periodic systems, the matching
high-level entry point is run_periodic_job, same artefact
family (.out / .system / .molden / extended .xyz /
.POSCAR / .xsf / .bibtex / .references), same dry-run +
citation surfaces, accepts a PeriodicSystem instead of a
Molecule. Everything run_job does internally is
re-usable via the same public API.