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: .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.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 bundled reference 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.11.0"
codename = "Sun's Stingray"
git_sha = "c90398f"
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..."
wall_time_s = 0.082
# … one row per actually-written artefact, with bytes + sha256 +
# wall-time-since-job-start.
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). Engineering’s bundled docs runs use the env var so the
docs/_static/examples/<slug>/<slug>.system files don’t leak
machine names. 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"])
Tutorial 29 walks through comparing your run’s manifest to the bundled reference for the same example.
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, four#-commented sections: Mulliken charges, Löwdin charges, Mayer bond orders (top-N, default threshold 0.1), 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/dipole/errors, each shaped as a list of records (or a single object fordipole/errors). Drop-in forjson.load(...).
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 errors dict / 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.
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":1.16e-06,"raw_total_bytes":1040,"headroom_factor":1.2,"by_category":{"ERI tensor":128,"Fock + density + 1e":256,"DIIS history":512,"MO workspace":144}}
{"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 |
|
|
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.