Memory budget

Every run_job calculation runs through a pre-flight memory check that (a) reports the estimated peak memory in the text output and (b) aborts with an explanation if that estimate exceeds the memory available to the process. On Linux this includes finite scheduler or container cgroup limits, even when the host itself has substantially more free RAM. This is a guardrail against the most common catastrophic failure mode of a QC code, calculations that silently thrash to disk and freeze the host.

What you’ll see in the .out file

Job: RKS / PBE  basis=cc-pvdz
Atoms (bohr)
------------------------------------------------------
   1  Z=  8       0.00000000      0.00000000      0.00000000
   2  Z=  1       0.00000000      1.43000000     -0.98000000
   3  Z=  1       0.00000000     -1.43000000     -0.98000000
charge=0  multiplicity=1  n_electrons=10

vibe-qc estimates this calculation will require ~0.13 GB of memory:
    ERI tensor            0.00 GB
    Fock + density + 1e   0.00 GB
    DIIS history          0.00 GB
    MO workspace          0.00 GB
    DFT grid + chi        0.10 GB
    Python runtime + NumPy overhead   0.10 GB
Available on this machine: 24.0 GB. Proceeding.

  iter     energy (Ha)            dE          ||[F,DS]||   DIIS
  ...

The headline figure already carries a 1.5x default safety headroom over the sum of the per-category numbers. Set VIBEQC_MEMORY_HEADROOM=2.0 (or another value >= 1.0) to tune the factor for a site or scheduler wrapper.

When the estimate exceeds available RAM

vibe-qc estimates this calculation will require ~218.4 GB of memory:
    ERI tensor      186.0 GB
    ...
Available on this machine: 7.2 GB. ABORTING.

InsufficientMemoryError: Set `options.memory_override = True` (or pass
`memory_override=True` to `run_job`) to proceed anyway. Consider a
smaller basis or, once shipped in v0.6+, density fitting / on-disk
scratch.

Overriding the check

Pass memory_override=True to run_job:

from vibeqc import Molecule, run_job

mol = Molecule.from_xyz("large.xyz")
run_job(
    mol, basis="def2-tzvp", method="rhf",
    output="huge",
    memory_override=True,    # accept the risk of swap / freeze
)

The output then reads Proceeding (override) instead of Proceeding so anyone reading the log later knows what happened.

Estimators covered

Method

Dominant cost

Notes

RHF / UHF

Dense ERI tensor or direct-SCF shell-pair scratch

Direct SCF charges about 1 KB per shell pair

RKS / UKS

HF baseline + DFT grid, weights, coordinates, libxc scratch

meta-GGA adds tau buffers

MP2 / UMP2

OVOV tensor or DF auxiliary integrals and OV workspace

spin-channel storage for UMP2

CCSD / CCSD(T)

T1/T2 amplitudes, D1/D2 intermediates, triples workspace

DF auxiliary integrals are charged separately

DLPNO-MP2 / DLPNO-CCSD(T)

PAO/PNO domains, pair lists, auxiliary pair workspaces, triples domains

conservative local-domain estimate

CAS/CI/FCI

CI or determinant vectors, RDMs, MO integral transforms

exponential methods report their vector storage

Periodic GDF / GPW / GAPW

GDF Lpq factors, bounded Ewald-J FT cache/batches, or FFT-grid collocation/cache arrays

Lpq and grid estimates use dry-run/live preflight; exact Ewald-J uses explicit cache and batch targets

NEB uses a separate per-image-worker model and is intentionally not folded into the single-point estimator.

For molecular grid consumers, including RKS/UKS integration and COSX workspace, the estimate follows the active angular scheme. Product grids use n_theta * n_phi; unpruned Lebedev grids use the bundled point count for lebedev_order. Angularly pruned and ORCA-style five-region grids conservatively charge the densest active angular tier, so preflight remains an upper bound even though some radial shells use fewer points.

Native molecular RKS and UKS evaluate AO values, AO derivatives, density contractions, libxc vectors, and Fock projections in contiguous batches of at most 4,096 points. Analytic molecular XC gradients use the same bound, including their AO-Hessian tables. This does not coarsen or prune the grid: every original Becke point and weight is included in the same order, and only the association of the accumulated floating-point sums changes. Grid coordinates, weights, and atom-ownership indices remain full-grid arrays.

VV10-paired functionals are the nonlocal exception. Their density and gradient invariants are collected over the full grid, the VV10 double integral is evaluated once with all cross-grid pairs present, and only the AO projection of the resulting potential is batched. ROKS, TDDFT response setup, and explicitly enabled molecular Newton/TRAH XC kernels still retain whole-grid AO tables and are charged as dense routes. If the automatic large-RKS tail recovery decides to enable TRAH after a non-converged first attempt, it performs a second dense memory check before starting that retry.

The 3D true-multi-k pure-DFT GDF route retains its analytic-FT Ewald J cache only when the full tensor plus one similarly sized construction temporary fits VIBEQC_J_EWALD3D_CACHE_MIB (4096 MiB by default). Larger cases contract reciprocal vectors and output cells in batches, so they never retain an (n_cells, n_AO, n_AO, n_G) complex tensor. Those batches use the separate VIBEQC_J_EWALD3D_FT_CELL_CHUNK_MIB target (512 MiB by default). Lowering either positive value trades reuse/batch size for memory; neither changes the VIBEQC_J_EWALD3D_KE reciprocal cutoff or the energy convention.

Reading the probe yourself

The cross-platform “how much memory is available right now” probe is exposed for scripting:

>>> import vibeqc
>>> vibeqc.available_memory_bytes() / 1024**3
24.3...

It first obtains a host-wide value from psutil.virtual_memory().available (install with pip install psutil to get the highest-quality number), then falls back to /proc/meminfo on Linux and os.sysconf on macOS. On Linux, finite cgroup v2 (memory.max and memory.current) or cgroup v1 memory-controller limits bound that host value. Parent cgroup limits are included, which is important when a scheduler gives each task an unlimited leaf inside a limited job cgroup. The returned value is therefore no greater than either the host’s available memory or the allocation’s remaining memory.

The probe returns 0 if every applicable source fails. check_memory treats that as “unknown” and silently proceeds rather than false-aborting on an unsupported platform. A known exhausted cgroup is not unknown and fails the preflight check.

Writing your own estimator

If you call the low-level SCF drivers (run_rhf, run_rks, …) directly instead of through run_job, you can invoke the estimator yourself:

from vibeqc import estimate_memory, check_memory

est = estimate_memory(mol, basis, method="rhf", options=rhf_options)
print(est)                       # human-readable block
check_memory(est)                # raises InsufficientMemoryError if over budget
# ... your own driver call ...

The estimator returns a MemoryEstimate dataclass with a by_category dict, so you can inspect exactly where the memory goes.