Core concepts: the checkpoint & workflow model#

In the Quickstart you wrote qc.chk.new(...).scf(ref="r").run() and read mychk.scf.energy. That one line already used every big idea in qc-rs. This chapter slows right down and explains the model in detail, because once it clicks, the whole toolkit becomes predictable — every feature is just another step you add or another result you read.

The checkpoint#

Everything revolves around one object, the checkpoint. Think of it as a small laboratory notebook for one molecule. It holds three things:

  1. The molecule — the geometry, basis set, charge, and spin you gave to qc.chk.new(...). This is fixed input.

  2. The current electronic state — the result of the latest step that produced electrons: the molecular orbitals (current_mo) and the electron density (current_density). A guess, an SCF, or a CASSCF all update this. Properties read this state.

  3. Result records — the outcomes of the steps you have run: the SCF energy and whether it converged, computed properties, an optimization trajectory, and so on.

A freshly created checkpoint has a molecule but no electronic state yet — nothing has been computed.

Two phases: build, then run#

A qc-rs calculation happens in two clearly separated phases. This separation is the single most important thing to understand.

1. Build — add pending steps#

Calls like qc.scf(mychk, ...) (or the method form mychk.scf(...)) do not compute anything. They return a new checkpoint carrying a pending step — a note that says “an SCF is wanted here.” The original checkpoint is untouched (checkpoints are effectively immutable — a step gives you a new one):

base    = qc.chk.new(atom="O 0 0 0.1173; H 0 0.7572 -0.4692; H 0 -0.7572 -0.4692",
                     ao="cc-pvdz", unit="angstrom")
pending = base.scf(ref="r")     # a NEW checkpoint with a pending SCF step

pending is base          # False — base is unchanged
pending.scf.energy       # None — nothing has run yet

Because building is cheap and side-effect-free, you can assemble a multi-step calculation freely — chain several steps, branch, or pass a checkpoint into a function — before anything runs.

2. Run — materialize the results#

.run() actually does the work. Afterwards the result accessors return real values:

done = pending.run()
done.scf.energy          # -76.026772
done.scf.converged       # True

Important

Pending step vs. result accessor The same name means different things before and after .run(). mychk.scf(...) adds a step (it takes arguments and returns a new checkpoint); mychk.scf.energy reads the finished step’s result (and is None until you run). Keeping “asking for work” separate from “reading the answer” is exactly what lets a calculation be composed freely and re-run safely.

What .run() really does#

The pending steps form a small dependency graph — a DAG (directed acyclic graph). When you call .run(), qc-rs performs a sequence of well-defined actions:

  1. Resolve dependencies. It works out what each step needs. An SCF needs a starting guess for the orbitals and the one-electron integrals; a property needs a converged density; and so on.

  2. Auto-insert sensible defaults. For any dependency you did not request explicitly, qc-rs inserts a safe default. In particular, if an SCF has no starting state it auto-inserts the sad guess (superposition of atomic densities), and it inserts a default integrals step. This is why mychk.scf(ref="r").run() works on its own — you almost never write guess(...) or ints(...) by hand.

  3. Order and execute. It topologically orders the graph, runs each step in turn, and records the result on the checkpoint.

  4. Skip what is already done. Any result that is still valid is reused, not recomputed; only missing or stale steps run.

  5. Prune. Completed pending nodes are cleared, so the checkpoint is ready to accept more steps.

Two consequences are worth committing to memory:

  • .run() is safe to call again. Valid results are reused, so re-running never repeats finished work. This is also how a restart works after an interruption: load the checkpoint and .run() continues from where it stopped.

  • You specify only what you care about. The guess and integrals are filled in for you. If you do want control — a different guess, a specific integral strategy — you add those steps explicitly, and .run() uses them instead of the defaults.

Note

Staleness Results carry a notion of generation. If you change something a step depends on, downstream results are marked stale and recomputed on the next .run(), while everything unaffected is kept. You get correctness (nothing outdated survives) without paying to redo unaffected work.

Two ways to write the same thing#

Every workflow verb has a functional form and a method-chain form. They are equivalent — pick whichever reads better in context:

# functional                      # method-chain
qc.scf(mychk, ref="r").run()      mychk.scf(ref="r").run()

The method-chain form shines in one-liners; the functional form can read more clearly when a checkpoint flows through a helper function.

Reading results, and computing properties#

After .run(), results live on named accessors that mirror the step:

done.scf.energy       # the total energy
done.scf.converged    # did the SCF converge?

Analysis is different: it lives in the qc.prop.<group>.<leaf> namespace and is computed lazily the first time you ask, then cached. It reads the checkpoint’s current electronic state:

qc.prop.chrg.mulliken(done)       # {'charges': [...], 'atom_labels': [...]}
done.prop.chrg.mulliken()         # the same thing, method-chain form

There are fourteen property groups (charges, bond orders, aromaticity, orbitals, QTAIM, ELF, multipoles, spin, conceptual DFT, ESP, real-space fields, spectra, geometry, …) — the whole suite is Molecular properties. You can also compute a bundle eagerly while running, with scf(prop=...).

Saving and loading#

A checkpoint — the molecule, the current electronic state, and every result — can be written to disk and restored later, with no recomputation:

done.save("water")                # persist the checkpoint to disk (an HDF5 .qch5 file)
later = qc.chk.load("water")      # restore it, e.g. in a fresh session
later.scf.energy                  # -76.026772 — read straight back, nothing recomputed

Because a loaded checkpoint carries its results, you can also add more steps to it and .run() — the existing results are reused, and only the new work runs.

Why this design?#

Separating build from run, and pending steps from results, buys three things a beginner comes to appreciate:

  • Composability — a calculation is a short pipeline you extend one step at a time; building is cheap and free of surprises.

  • Restartability.run() reuses valid results, so long jobs resume instead of restarting, and a saved checkpoint is a resumable snapshot.

  • Provenance — the checkpoint records which electronic state produced which result, so an analysis is never ambiguous about what it was computed from.

With this model in hand, the rest of the manual is essentially a catalogue: steps you can add (guess, ints, scf, opt, lct, td, …) and results you can read (energies, gradients, and the property suite). Next, get the theory behind them in Part II — Foundations, or dive into the User guide.