The output logger (vibeqc.output)¶
vibe-qc has one output surface. Every user-facing byte a calculation
emits – the banner, the SCF trace, energies, properties, timings – is
formatted in Python by vibeqc.output and written through a single
channel. Nothing in the codebase calls print() for .out content, opens
the .out file directly, or hand-formats a number. This page explains how
that works, both for users (log levels, where output goes) and for
contributors (how a module emits its own output).
Where output goes¶
A run writes a family of sibling files next to the output stem (see
Output files for the full list). The human-readable
one is {stem}.out. It is produced by an output channel: the runner
opens one at job start, and every module that wants to say something
writes into it.
from vibeqc.output import write, flush
write(" Total energy: -76.0261408 Ha\n")
flush()
write() targets the channel the runner installed. Outside a run (a
notebook, a unit test, a library import) no channel is active and
write() is a silent no-op, so any writer stays callable anywhere. If you
need to catch a “forgot to install a channel” bug, set
VIBEQC_STRICT_OUTPUT=1 and the no-op becomes a loud error instead.
Log levels¶
Every message carries a level, and each channel has a threshold. A message appears only when its level is at or below the threshold. The four levels, from always-shown to diagnostic-only:
Level |
Shown when the channel is… |
Use for |
|---|---|---|
|
always (even |
essential results: final energy, converged/not-converged, fatal errors |
|
|
the normal |
|
|
opt-in detail: per-shell basis dumps, full orbital tables, per-stage timings |
|
|
developer diagnostics: intermediate norms, gauge checks |
An untagged write(text) is STANDARD. So a channel left at its default
standard threshold shows exactly the QUIET and STANDARD messages –
the same content the .out has always had. Raising the threshold to
verbose reveals the VERBOSE extras on top; lowering it to quiet
strips everything but the essentials.
Setting the level¶
As a user, set the default with the environment variable (a name or
0-3):
VIBEQC_OUTPUT_LEVEL=verbose python run.py # standard + verbose
VIBEQC_OUTPUT_LEVEL=quiet python run.py # essentials only
VIBEQC_OUTPUT_LEVEL=debug python run.py # everything
In code, output_level(...) sets the default that newly-opened channels
adopt, and channel.set_level(...) changes a channel already open:
from vibeqc.output import output_level, Level
output_level("verbose") # or Level.VERBOSE, or 2
output_level() # read it back -> Level.VERBOSE
Tagging a message¶
A module decides which band its output belongs to by passing level=:
from vibeqc.output import write, Level
write(" Total energy: -76.0261408 Ha\n") # STANDARD (default)
write(" Converged in 12 iterations\n", Level.QUIET) # always shown
write(basis_shell_dump, Level.VERBOSE) # only in verbose+
write(f" ||[F,DS]|| = {res:.3e}\n", Level.DEBUG) # only in debug
That is the whole contract for making output level-aware: tag the write, and the channel filters it. There is no separate logger object to obtain and no per-module configuration.
Formatting numbers¶
Precision, field width, and units are not hand-written into f-strings.
A quantity is a Quantity, and a FormatPolicy decides how it renders –
which is what keeps the same physical value from being printed at five
widths under four labels. Tables compute their own column widths.
from vibeqc.output import OutputDocument, Quantity, Table, Column, write
doc = OutputDocument()
doc.section("Energy components", unit_for="energy")
doc.scalar("Nuclear repulsion", Quantity(9.1671, "energy"))
doc.scalar("Total energy", Quantity(-76.0261408, "energy"))
write(doc.render())
t = Table([Column("iter"), Column("energy"), Column("dE")])
t.add_row(1, Quantity(-76.02, "energy"), Quantity(-1.0e-3, "energy_delta"))
write(t.render() + "\n")
Quantity kinds today: energy, energy_delta, gradient, length,
temperature, dimensionless. Units belong to a physical dimension and
precision to a kind, so DEFAULT_POLICY.with_unit("energy", "eV") moves
total energies and SCF energy changes together – a table can never mix
Hartree and eV across its columns. To render a whole document in eV at 6
decimals, pass a policy to render() rather than editing the module that
produced the numbers.
For contributors: the rules¶
The full, authoritative version lives in
CLAUDE.md § 16
and AGENTS.md rule 11. In short:
Emit through the channel, never
print().print()for user-facing output is only acceptable in the console-script entry points (_cli.pyand friends), whose job is stdout.Never hand-format a number. Use
Quantity+ the document primitives. A total energy isQuantity(value, "energy"), notf"{e:16.10f} Ha".The C++ core emits nothing. It fills result structs and returns numbers across pybind11; Python renders them. This is enforced by
tests/test_cpp_emits_no_user_output.py.Do not change the shape of
vibeqc.outputyourself. The channel, the document primitives, the writer/manifest, andprogress.pyare owned by the IO dev chat. If you need a new quantity kind, a table variant, or a new lifecycle hook, raise it under Asks pending inhandovers/HANDOVER_OUTPUT_LOGGER.md. Using the module is every chat’s job; changing its shape is the IO chat’s.
The .out format is frozen¶
tests/test_out_format_snapshot.py locks the .out format against
committed golden files (molecular RHF/RKS, double hybrid, periodic RHF).
It freezes the scaffold – labels, widths, alignment, separators – not
the numbers, so a legitimate energy change passes but a format change
trips. If you deliberately change the format, regenerate with
VIBEQC_REGEN_GOLDEN=1 pytest tests/test_out_format_snapshot.py and review
the diff before committing.
Live progress vs the .out record¶
Two surfaces, on purpose:
The
.outfile is the complete batch record, written through the channel. It is columnar, with headers.The live stdout stream (
vibeqc.progress.ProgressLogger, theprogress=/verbose=knobs on the runners) is for watching a run happen. Its per-iteration lines are self-describing (iter 2 E = ... Ha dE = ... [0.3s]) so each stands alone in atail -fwhere a columnar header has scrolled off. These are different requirements, so the two formats differ by design; they are not expected to be identical.
See also¶
Output files – the full sibling-file family.
docs/design_output_module.md– the output module’s design contract.