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:

  1. Banner, vibe-qc + libint + libxc + spglib versions, for provenance. Identical to vibeqc.print_banner().

  2. Job header, method + basis.

  3. Initial atom table, Z + Cartesian bohr + charge + multiplicity

    • electron count.

  4. Optimization block (only when optimize=True), target convergence, final optimized geometry.

  5. SCF trace, iteration-by-iteration energy, ΔE, commutator norm, DIIS history length. Flags converged / NOT converged.

  6. Energy components (DFT only), nuclear repulsion, electronic, Coulomb J, HF-exchange K, XC, total.

  7. Orbital-energy table, all occupied MOs and up to n_virtual=5 virtual MOs (override via n_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.

  8. References block (v0.8.x+), ## References section 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 .bibtex sibling.

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 / @software entry per cited work, in citation order. Drop into \bibliography{output-h2o.bibtex} and \cite{...} each entry by its bibtex_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 (A for 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’s m = -L..+L convention 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. Single data_vibeqc block 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 keys mulliken / loewdin / mayer / wiberg / npa / dipole / errors, each shaped as a list of records (or a single object for dipole / errors). Drop-in for json.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 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=...):

write_cube=

Files written

True / "density"

{stem}.density.cube (total ρ(r))

"homo" / "lumo"

The corresponding MO cube

"homo-1" / "lumo+2" / …

Offset MO labels

<int>

That MO index (0-based)

["density", "homo", "lumo", 7]

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 .out file is still written)

1

banner + warnings + final SCF status only

2

add per-stage milestones + info() lines

3

add per-stage timing on stage exit

4

default, add per-iteration SCF rows

5

add inline RSS-memory snapshots

6+

phase-level wall-clock breakdown live (overlaps the post-mortem .perf log)

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?

run_rhf_periodic_scf (EWALD_3D)

Python

yes

run_rhf_periodic_gamma_scf (EWALD_3D)

Python

yes

run_rks_periodic_scf / run_rks_periodic_gamma_scf (EWALD_3D)

Python

yes

run_uhf_periodic_*_ewald3d

Python

yes

run_uks_periodic_*_ewald3d

Python

yes

*_periodic_*_scf (DIRECT_TRUNCATED)

C++

banner + post-hoc summary only

run_rhf / run_uhf / run_rks / run_uks (molecular)

C++

banner + post-hoc summary only

run_job (molecular)

wraps C++

banner + line-buffered .out + post-hoc summary

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 PerfScope opened during the run, sorted by wall-time. The par column 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?

run_job.total

wraps everything inside run_job

yes

geometry_optimization

ASE BFGS

yes

basis_set_construction

libint

yes

scf.{rhf,uhf,rks,uks}

C++ molecular SCF (one row total)

one row total

write_molden

molden writer

yes

periodic.integrals_lattice (+ S/T/V sub-scopes)

Python lattice integrals

yes

periodic.*_periodic_*_ewald3d SCF iteration loop

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

banner

run_job

first record, carries linked-library versions + run_fingerprint

job_start

run_job

after method resolution, before any work

memory_estimate

run_job

after the memory pre-flight

scf_iter

C++ molecular SCF (replayed from result.scf_trace) + Python periodic SCFs (live via ProgressLogger.iteration)

per SCF iteration

scf_converged

ProgressLogger.converged funnel

once after the SCF loop

properties

run_job

post-SCF, when properties succeed; carries Mulliken / Löwdin / dipole

scf_failed

run_job

when the C++ SCF raises; the exception still propagates after the dump

job_end

run_job

last record, total_wall_s + out_path. energy is the bare SCF/solver energy; when a post-SCF correction is active (D3-BJ / D4 / gCP / SRB) the record also carries the explicit split e_scf, e_dispersion, e_total (+ e_gcp / e_srb for 3c composites). Harvesters should prefer e_total when present.

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)

run_job’s try/except wraps the SCF call → dump + re-raise

SCF returns non-converged (max_iter exceeded)

run_job checks result.converged after success; dumps if False, returns the result normally (does NOT raise)

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

molecule

required

Molecule in bohr coordinates

basis=

required

libint-recognized basis name

method=

"auto"

"rhf" / "uhf" / "rks" / "uks" / "auto"

functional=

None

XC functional name for RKS/UKS (e.g. "PBE", "B3LYP")

output=

"output"

path stem; files become {output}.out, {output}.molden, {output}.traj

optimize=

False

run BFGS (via ASE) before the final SCF

fmax=

0.05

optimizer convergence in eV/Å (ASE convention)

max_opt_steps=

200

optimizer iteration limit

write_molden_file=

True

emit the .molden file

write_xyz_file=

True

emit {output}.xyz final geometry (Å, plus energy=<Ha> in the comment line)

write_population_file=

True

emit {output}.population.{txt,json} siblings with Mulliken / Löwdin / Mayer / dipole

write_cube=

False

volumetric cubes; True / "density" / "homo" / "lumo" / int / list (see cube section)

cube_spacing= / cube_padding=

0.2 / 4.0 (bohr)

grid spacing + padding for cubes; ignored when write_cube=False

output_qvf=

False

bundle structure + density + the write_cube= grids + basis/coefficients into one {output}.qvf archive for vibe-view (vibe-qc’s native visualization format)

citations=

True

emit {output}.bibtex + {output}.references siblings and append a ## References block to .out

dry_run=

False

pre-flight only: build the OutputPlan, write {output}.system with [outputs].status="dry_run", return None without running the SCF. VIBEQC_DRY_RUN=1 env var does the same; used by vq submit --vibeqc-preflight

progress=

None (resolves to live-on)

live progress logger; False to silence stdout, a ProgressLogger to route, or set VIBEQC_LIVE_LOGGING=0 globally for batch scripts

verbose=

None (resolves to 4)

integer 0..9 (PySCF convention) gating how much live detail emits; 0 silent, 4 default with per-iter rows, 5 adds memory snapshots; None defers to VIBEQC_VERBOSE=N env var

use_logging=

False

route progress through logging.getLogger("vibeqc.run_job") instead of bare stdout; composes with stdlib RotatingFileHandler / syslog / dictConfig

perf_log=

None

post-mortem perf breakdown; True writes {output}.perf, a path writes there explicitly, None defers to VIBEQC_PERFLOG=path env var

structured_log=

False

machine-readable NDJSON; True writes {output}.scf.jsonl, a path writes there explicitly, None/False defers to VIBEQC_STRUCTURED_LOG=path env var

crash_dump=

True

post-mortem dump on SCF failure ({output}.dump + .dump.density.npy etc.); pass False (or set VIBEQC_NO_CRASH_DUMP=1) to disable

record_hostname=

True

record live hostname in {output}.system; pass False (or set VIBEQC_NO_HOSTNAME=1) to emit hostname = "<redacted>"

rhf_options= / uhf_options= / rks_options= / uks_options=

None

fine SCF control, override the relevant options struct

method="auto" resolves to:

  • functional set + multiplicity 1 → RKS

  • functional set + multiplicity ≥ 2 → UKS

  • no functional + multiplicity 1 → RHF

  • no 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.