SCF Fock-build modes¶
vibe-qc offers two Fock-build modes for molecular SCF, the
strategy that constructs the J (Coulomb) and K (exchange) matrices each
iteration. Density fitting is a separate route: when density_fit=True, its
RI-J or RIJCOSX kernels supersede the four-index path and the mode is ignored.
Within that route, cosx=True selects COSX-K. The high-level
run_job(cosx=True) convenience also enables density fitting automatically.
For a worked resource comparison, including the 170-basis-function H2O2/cc-pVQZ timing and memory measurements, see Direct SCF or in-core integrals?.
Three modes¶
Mode |
Enum value |
Memory |
Best for |
|---|---|---|---|
|
|
- |
Default, picks based on system size |
|
|
O(n_bf⁴) |
Small systems whose dense tensor fits comfortably |
|
|
O(n_shells² + n_bf²) |
Large systems, bounded integral memory |
All molecular SCF drivers (RHF, UHF, RKS, UKS) support the mode via
identical scf_mode / scf_mode_auto_threshold fields on their
options struct. Range-separated RKS and UKS are a deliberate exception to
the requested mode: their attenuated-exchange kernel requires the direct
builder, as described below.
AUTO, the default¶
from vibeqc import RHFOptions, SCFMode
opts = RHFOptions()
# opts.scf_mode defaults to SCFMode.AUTO
# opts.scf_mode_auto_threshold defaults to 140 BF
When scf_mode = AUTO (the default), vibe-qc counts the number of
basis functions after the basis set is constructed and decides:
(n_{bf} \le 140) ->
CONVENTIONAL, the in-core 4-index ERI tensor is built once and the per-iteration cost is a tensor contraction.(n_{bf} > 140) ->
DIRECT, the in-core tensor would be multi-GB; switch to on-the-fly Schwarz-screened libint quartet evaluation that stays O(n_shells² + n_bf²) in memory.
The threshold is a resource policy, not a physical boundary. At the default cutoff, a dense 140-function tensor is already 2.862 GiB before any other process memory. To cap that tensor below 1 GiB, use 107, the largest basis count satisfying that limit:
from vibeqc import RHFOptions
opts = RHFOptions()
opts.scf_mode_auto_threshold = 107 # switch to DIRECT above about 1 GiB
CONVENTIONAL, the in-core path¶
from vibeqc import RHFOptions, SCFMode
opts = RHFOptions()
opts.scf_mode = SCFMode.CONVENTIONAL
The full 4-index electron-repulsion integral tensor (μν|λσ) is computed once at the start and reused across SCF iterations. Fast for small systems, but the tensor grows as n_bf⁴:
n_bf |
Full tensor size |
|---|---|
24 |
2.53 MiB |
95 |
621.42 MiB |
107 |
1000.06 MiB |
108 |
1.014 GiB |
170 |
6.223 GiB |
200 |
11.921 GiB |
250 |
29.104 GiB |
394 |
179.546 GiB |
These are \(n_\mathrm{bf}^4\times8\) bytes for the dense tensor alone. Add Fock, density, DIIS, eigensolver, runtime, and requested-output workspaces when sizing a job. Lower the AUTO cutoff when the tensor exceeds the workflow’s budget, or set DIRECT explicitly.
DIRECT, the integral-driven path¶
from vibeqc import RHFOptions, SCFMode
opts = RHFOptions()
opts.scf_mode = SCFMode.DIRECT
Instead of storing the ERI tensor, the direct Fock build considers the unique shell quartets on each SCF iteration and evaluates only those that survive screening. An 8-fold-symmetric loop skips the vast majority of quartets using the strict Cauchy-Schwarz bound:
[ |\langle \mu\nu | \lambda\sigma \rangle| ;\le; Q(s_\mu, s_\nu) \cdot Q(s_\lambda, s_\sigma) ]
where (Q(s_a, s_b)) is precomputed from the diagonal shell-pair integrals at construction time and reused across iterations.
Direct SCF options¶
All fields live on the per-method options struct (RHFOptions,
UHFOptions, RKSOptions, UKSOptions):
Field |
Default |
Description |
|---|---|---|
|
1e-10 |
Per-quartet skip bound; lower = fewer skipped quartets |
|
1e-7 |
Loose threshold for early SCF iterations (two-phase Schwarz screening) |
|
1e-3 |
Gradient-norm cutoff, switch from loose to tight below this |
|
|
Use a difference-density ΔP Fock build on the direct path |
|
8 |
Full-density rebuild every N iterations to limit recurrence error |
Two-phase Schwarz tightening¶
By default, the direct Fock build starts with the looser
schwarz_threshold_loose (1e-7) so the per-shell density envelope
catches mid-SCF quartets aggressively. Once the SCF gradient norm
drops below schwarz_threshold_tighten_at, the builder switches to
the tight schwarz_threshold and discards the incremental cache. An energy
difference across that threshold and cache change compares two different
numerical Fock maps, so it is excluded from convergence testing.
Häser and Ahlrichs analyse iteration-dependent screening and show that the
error in a screened difference-Fock update can inherit errors from earlier
increments (Eqs. 30-35). That analysis motivates the coarse-to-tight
refinement; it does not prescribe vibe-qc’s thresholds or transition policy.
Set
schwarz_threshold_loose <= schwarz_threshold to disable the
coarsening and use a uniform tight threshold throughout.
Incremental ΔP Fock build¶
When incremental_fock = True, the builder caches the previous
density D_prev and two-electron Fock matrix G_2e_prev. Each iteration
computes ΔD = D − D_prev and returns:
[ G_{2e}[D] = G_{2e}[D_\text{prev}] + G_{2e}[\Delta D] ]
This recurrence is Eq. 20 of Almlöf, Fægri, and Korsell. Their discussion on p. 389 also recommends a final energy-oriented iteration after a non-energy monitor is satisfied. The exact certification policy below is vibe-qc’s implementation of that principle, not a prescription quoted from the paper.
The direct kernel applies the published density-weighted Schwarz test to the difference density. For an incremental update with (s = \max |\Delta D| < 1), vibe-qc first screens (\Delta D / s), builds that normalized contribution, and multiplies the result by (s). This is a vibe-qc relative-error policy: the omitted part of an update scales to zero with its amplitude, instead of each small update being allowed the same absolute omission and accumulating a persistent drift floor. It is derived from the linearity of (G_{2e}); it is not a scaling formula prescribed by Almlöf et al. or Häser and Ahlrichs.
Amplitude normalization deliberately prevents a uniform rescaling of (\Delta D) from making the retained quartet set artificially sparser. Incremental builds still benefit when the change density becomes spatially localized, while accuracy no longer depends on how one density change is split across SCF iterations.
A full rebuild happens every incremental_fock_reset_freq iterations
(default 8) to limit error inherited from screened increments as well as
ordinary arithmetic accumulation. The reset interval is a vibe-qc
implementation policy, not a reset rule attributed to Almlöf et al. When the
two-phase screen changes to its tight threshold, the driver also discards the
incremental cache. Set
incremental_fock=False for a controlled full-build comparison.
from vibeqc import RHFOptions
opts = RHFOptions()
opts.incremental_fock = True
opts.incremental_fock_reset_freq = 8
Final full-Fock certification¶
Loose-screened and incremental builds are acceleration phases. A molecular direct SCF calculation enters a certified full-Fock phase when either:
the orbital gradient first satisfies
conv_tol_grad, making the row eligible for ordinary convergence; orthe restart monitor detects a high-gradient stall while a coarse Fock map is still active.
The driver then forces the configured tight Schwarz threshold, disables
incremental updates, resets the Fock builder, and clears extrapolation and
mixing state. The first tight, nonincremental full-density row has an energy
difference relative to the preceding coarse-map row. That cross-map value is
reported but cannot certify convergence. The following row provides an energy
difference between two comparable tight, nonincremental full-density builds.
Only then can the calculation converge, and both the original
conv_tol_energy and conv_tol_grad must pass unchanged.
Consequently, certification requires two comparable same-map rows. It does not relax either tolerance or infer an empirical energy-noise floor. A standard diagnostic identifies a gradient-triggered or stall-triggered move into this phase.
When density_fit or cosx is active¶
At the options-struct level, density_fit=True selects the DF J/K
infrastructure and causes it to ignore scf_mode. Inside that route,
cosx=True replaces fitted exchange with COSX-K. A low-level options object
with cosx=True but density_fit=False does not activate COSX; set both and
provide aux_basis. The high-level run_job(cosx=True) argument performs
those two steps and auto-selects an auxiliary basis when possible.
Range-separated hybrids force DIRECT¶
RKS and UKS range-separated hybrids need an erf-attenuated exchange build.
Only the direct builder currently implements that kernel, so these jobs force
DIRECT when density_fit=False, even if AUTO would choose conventional or
the options request CONVENTIONAL. Combining a range-separated hybrid with
density_fit=True currently raises an explicit unsupported-route error because
the required attenuated three-center integrals are not implemented.
Performance¶
On compute-small (i9-10885H, 4 threads), RHF direct / def2-SVP, same-box ORCA comparison:
This sweep uses def2-SVP throughout, so its H2O row has 24 basis functions. The H2O/6-31G* examples use 18 basis functions instead.
System |
n_bf |
vibe-qc (s) |
ORCA (s) |
Ratio |
|---|---|---|---|---|
H₂O |
24 |
2.0 |
15.0 |
0.13 |
glycine |
95 |
5.0 |
13.0 |
0.39 |
naphthalene |
180 |
31.1 |
45.1 |
0.69 |
n-decane |
250 |
226.3 |
67.2 |
3.37 |
n-hexadecane |
394 |
639.2 |
185.3 |
3.45 |
This historical sweep placed the speed crossover against ORCA near 200 BF on that host. The current AUTO policy switches at 140 BF because the CPU-time crossover is not a safe memory cutoff: the dense tensor is already 2.862 GiB at 140 BF. Energy agreement against ORCA was ≤ 2.4 µHa across all converged cells.
That wall-time crossover does not define a safe memory crossover. A matched one-core H2O2/cc-pVQZ case at 170 basis functions used 45.550 s wall time, 45.032 s active CPU time, and 6611.05 MiB peak RSS with vibe-qc’s conventional path. ORCA 6.1.1 used 52.669 s wall time, 51.232 s active CPU time, and 126.25 MiB peak RSS in the same allocation. The energy difference was \(1.53\times10^{-10}\) Ha. The worked tutorial derives the \(170^4\) allocation, separates the different-host direct validation, and defines a safe crossover sweep.
Component timing¶
Set component_timing = True on RHFOptions or RKSOptions to record
where a molecular restricted SCF spends its wall time. The option is off by
default. Enabling it does not change energies, densities or iteration counts.
opts = vq.RHFOptions()
opts.density_fit = True
opts.aux_basis = "def2-universal-jkfit"
opts.component_timing = True
result = vq.run_rhf(mol, basis, opts)
ct = result.component_timing
for phase in ct.phases:
print(phase.path, phase.inclusive_s, phase.exclusive_s, phase.calls)
print(ct.wall_s, ct.unclassified_s)
Phase paths join nested names with /. The inclusive time of a phase
contains its nested phases, while its exclusive time does not. The exclusive
times of all phases plus unclassified_s equal wall_s, which runs from
driver entry to return.
Phase |
Covers |
|---|---|
|
Integrals, XC grid, J/K builder, DF tensor and COSX caches, initial guess and orthogonalization |
|
One RHF |
|
Separate DF and COSX kernels, nested under the driver phase that called them |
|
RKS Coulomb, exchange and XC builds. |
|
DIIS-family extrapolation, Fock diagonalization or a second-order orbital step, and density construction |
|
The post-convergence Fock rebuild and the stability analysis |
ct.iterations repeats the phase table per SCF iteration. An iteration runs
from one loop start to the next, so it includes that iteration’s
diagonalization. For staged COSX grids, segment numbers the grid stages.
ct.memory holds tracked DF container sizes at the end of construction:
df.live_tracked_bytes is the retained tensors and
df.construction_tracked_peak_bytes adds the temporary ones alive at that
point. Neither includes BLAS or solver scratch, and neither is process RSS.
Timers run only on the thread that called the driver. The G0 CPU profiling
harness in examples/regression/gpu_profile.py combines these records with
runtime provenance and process RSS; see the
G0 plan.
Bounded CPU profiling campaigns¶
The G0 harness can pin stability_check=False on every timed route with
submit --no-stability-check. Add --measure-stability for one separate
fresh-process driver run per case at that system’s largest thread count.
That run requests stability explicitly and reports the native
stability_wall_s separately from its total driver time. Unsupported
stability responses remain refusals in batch_status.json; they are not
zero-cost measurements or successful stability verdicts. These options do
not change the production driver’s default stability policy.
Use --system-settings with an external JSON file mapping each system to
its scheduling entries. Entries inherit --warmup, --repeats and
--per-run-budget-s; threads is required. For example, the large-system
campaign can use:
{
"n-hexadecane": [
{"threads": 4, "warmup": 1, "repeats": 3, "per_run_budget_s": 3600},
{"threads": 1, "warmup": 0, "repeats": 1, "per_run_budget_s": 7200}
]
}
--compare-timing-off --timing-off-cases CASE_ID,... limits paired runs to
selected cases, such as one naphthalene method per resolved route. Without
--timing-off-cases, the comparison applies to every selected case.
AUTO deduplication occurs inside the measured runtime; the worker writes
batch_resolved.json with the actual cases, aliases and run schedule.
Submit with --python pointing to an immutable runtime launcher, and pair
--runtime-root with --runtime-artifact to pin its deployed vq bundle.
--mem-mb reserves the job’s memory through vq. Installed wheels without
Git metadata are identified by the deployed bundle’s source revision,
manifest digest and verification receipt. The harness checks the installed
package inventory, native module and interpreter against the manifest both
before and after each calculation, outside the measured SCF interval. Raw
provenance stays in the external evidence root.
After fetching the job, summarize with --batch-json pointing to its
batch_resolved.json, plus --expected-source-sha and
--expected-native-sha256. Missing planned runs remain incomplete; separate
stability runs cannot enter the SCF repeat or instrumentation comparison.
Keep all campaign inputs, raw output and operational configuration outside
the checkout. Publish only the redacted, measured summary after checking
its completeness and identity results.
Fixed-density CPU noise¶
examples/regression/gpu_noise.py measures CPU arithmetic noise before a
replacement kernel is compared. Stage it with gpu_profile.py and a direct
case from the G0 manifest in an external vq workspace. Use the same pinned
immutable runtime for fixture export and replay. The worker commands are:
"$RUNTIME_PYTHON" gpu_noise.py export case.json --out-dir evidence/fixture
"$RUNTIME_PYTHON" gpu_noise.py replay evidence/fixture/fixture.json \
--route df --threads 1 --repeats 3 --out-dir evidence/replay
"$RUNTIME_PYTHON" gpu_noise.py replay evidence/fixture/fixture.json \
--route df --threads 2 --repeats 3 --out-dir evidence/replay
"$RUNTIME_PYTHON" gpu_noise.py summarize evidence --threads 1,2 \
--expected-source-sha "$SOURCE_SHA" --expected-native-sha256 "$NATIVE_SHA256" \
--json-out noise-summary.json
Set OMP_NUM_THREADS for each process and set OPENBLAS_NUM_THREADS,
MKL_NUM_THREADS, VECLIB_MAXIMUM_THREADS and BLIS_NUM_THREADS to one
before launching it. Reserve N CPUs and sufficient memory through vq;
each replay invocation is a fresh process. Apply an explicit queue and
subprocess wall budget appropriate to the case, as for the timing campaign.
The accepted routes are direct, df, rijcosx and resolved auto.
Fixture export runs the direct SCF with the G0 recipe and stability disabled.
It requires convergence and the pinned residual target, then captures the
returned density of a second run capped at half the converged iteration
count. It also exports SAD and two small opposite symmetric perturbations
of the converged density. The perturbations preserve Tr(D S) and have
Frobenius norm 1e-7 * max(1, ||D||_F); they are not projected onto positive
semidefinite matrices. The native restart normalizer prepares the frozen inputs once; its changes
are recorded. The density path is retained throughout replay.
Archive and matrix hashes keep these exact inputs fixed across routes and
thread counts. No GPU dependencies are needed.
Direct replay disables incremental caching and fixes the tight Schwarz
threshold. COSX uses the FITTED final grid of the pinned level; its K is the
iterated exchange operator, excluding the post-SCF one-center correction.
The latter still needs a separate noise qualification. PBE has no K sample.
Vxc uses the native evaluate_rks_xc_potential interface when available.
It returns the production restricted XC matrix, nonlinear energy and observed
batch worker count at the supplied spin-summed density, without normalization
or a restart check. Finite symmetric indefinite perturbations are accepted;
the production pointwise density cutoffs still apply. VV10, when present,
uses the supplied grid rather than a separate coarse grid. The G0 cases
use PBE or PBE0 and do not include VV10.
Older runtimes fall back to the unpolarised RKS zero-iteration path with zero H, J and K, so the returned Fock matrix is Vxc without subtracting large terms. The harness checks that the supplied density is unchanged. The SCF restart interface refuses indefinite perturbations; these remain explicit incomplete rows until replayed with a runtime containing the direct interface. The recorded evaluator identity prevents combining the two paths as one noise measurement.
The summary reports worst pairwise matrix max-absolute errors in Hartree,
relative Frobenius errors, symmetry defects, and energy differences within
each thread count and between OMP=1 and N. J and K use Tr(D J)/2 and
-a_x Tr(D K)/4, with RHF a_x=1, PBE0 a_x=1/4 and PBE a_x=0.
For Vxc, Tr(D delta Vxc) is a separate diagnostic; the energy comparison
uses the nonlinear native E_xc. A zero reference norm yields relative
error zero only if both matrices vanish, otherwise JSON null.
MEASURED means the recorded input has at least three valid repeats at both
requested thread counts. It does not certify coverage of other systems,
methods, routes, spin conventions or hardware, or set a scientific tolerance.
Missing thread samples remain INCOMPLETE; mismatched input, runtime,
settings or sleep evidence are refused. Publish only redacted summaries.
The G0 operation contracts must combine these measured floors with explicit
scientific budgets before admitting a replacement implementation.
References¶
Almlöf, Fægri, Korsell, J. Comput. Chem. 3, 385 (1982), doi:10.1002/jcc.540030314. Difference-Fock recurrence (Eq. 20) and the final energy-oriented iteration recommendation (p. 389).
Häser & Ahlrichs, J. Comput. Chem. 10, 104 (1989), doi:10.1002/jcc.540100111. Cauchy-Schwarz screening and inherited screened-increment errors (Eqs. 30-35).
These references are cited automatically in the .bibtex and .references
output files when the requested mode is DIRECT or when AUTO resolves to
DIRECT.