Pre-flight linter for quantum-chemistry setups — catches silent garbage-in (impossible electron count/spin, Bohr/Ångström unit errors, bare atoms, non-reproducible conformers) before an SCF runs for hours. Rust core, Python API.
Project description
nqf-lint
A pre-flight linter for quantum-chemistry cluster setups.
A quantum-chemistry calculation can run for hours and return a number that looks fine while the input was silently broken — an oxygen entered with no hydrogens, an electron count made impossible by a forgotten pseudopotential core, a metal floating with no coordination. The SCF either fails cryptically or, worse, converges to a physically meaningless state that becomes a data point in someone's benchmark.
nqf-lint reads a declared cluster and runs cheap, deterministic checks that catch
these failure modes before the expensive calculation. It is written in Rust, has
no runtime dependencies, and exits with a nonzero code on error so it drops straight
into a Makefile or CI.
Every check corresponds to a real bug found in a production pipeline.
Usage
nqf-lint geometry.xyz # a standard XYZ geometry
nqf-lint cluster.json # the tool's own cluster spec (charge/spin/ECP)
nqf-lint build_cluster.py # Python source (conformer determinism)
XYZ is the universal format every quantum-chemistry package reads and writes,
so the linter runs on real inputs, not only its own JSON. XYZ carries no charge or
spin, so put them on the comment line — either as a bare <charge> <mult> pair or
as charge=.. mult=.. — to enable the electron-count checks; without them, those
checks abstain and the geometry checks still run.
3
charge=0 mult=1
O 0.000 0.000 0.000
H 0.757 0.586 0.000
H -0.757 0.586 0.000
The JSON spec adds what XYZ cannot express — the ECP list and the metal oxidation state — for the ECP-aware parity and d¹⁰ spin checks:
{
"atoms": [
{ "element": "Hg", "x": 0.0, "y": 0.0, "z": 0.0 },
{ "element": "O", "x": 2.30, "y": 0.0, "z": 0.0 },
{ "element": "H", "x": 2.60, "y": 0.80, "z": 0.0 },
{ "element": "H", "x": 2.60, "y": -0.80, "z": 0.0 }
],
"charge": 2,
"spin_multiplicity": 1,
"ecp_elements": ["Hg"]
}
What it checks
The geometry checks (bare heteroatom, metal coordination, overlapping atoms) run
on any input — .xyz or .json. The electron-count checks need charge/spin, and
the ECP/d¹⁰ checks need the extra fields only .json carries.
Geometry + electron count:
| check | catches |
|---|---|
| electron parity | (N + M) must be odd. Forgotten ECP core subtraction or a wrong formal charge silently flips parity — the SCF then fails or converges to nonsense. Counts electrons with the ECP core removed. |
| bare heteroatom | a C/N/O/P/S with no bonding partner in covalent range: waters entered as bare oxygens, dropped hydrogens, or a fragment sliced through a bond. |
| metal coordination | a metal center with zero neighbours in range is floating — the fragment was built wrong. |
| metal spin state | a closed-shell d¹⁰ cation (Zn²⁺, Hg²⁺, Cu⁺, …) declared open-shell. Only fires when the oxidation state is given and the ion is unambiguous; abstains for ligand-field-dependent ions like Ni²⁺. |
| overlapping atoms | two nuclei closer than 0.5 Å — below any real bond (H–H is 0.74 Å). A duplicated atom or a coordinate error that blows up the SCF or double-counts electrons. |
| unit scale (Å vs Bohr) | coordinates in Bohr (1 Bohr = 0.529 Å) read as Ångström, inflating every distance ~1.89×. Anchored on hydrogen (an X–H bond is ~1.0 Å in every molecule): if the closest H sits > 1.5 Å from any atom, the geometry is almost certainly Bohr. Reports the converted value. |
Python source (.py):
| check | catches |
|---|---|
| conformer seed | EmbedMolecule / EmbedMultipleConfs with no fixed randomSeed. The embedding is nondeterministic: the same SMILES gives a different geometry each run, so any descriptor computed on it is irreproducible. This is the bug that turned a published ρ=0.855 into a lottery. |
It never guesses. When it cannot verify something — an unknown ECP core, an unknown element — it abstains with a warning rather than emit a false verdict, because "cannot verify" is exactly where silent bugs live.
Design principle
Every check must fail loudly on a known-bad input and pass a known-good one. The test
suite is built from real bugs: the mining cluster whose "waters" were bare oxygens
({Co:1, O:7, …}, zero hydrogens), the Hg cluster whose charge was left at the
all-electron value after switching on an ECP, the metal built with no ligand nearby.
A check that cannot demonstrate both directions does not ship.
Build
cargo test # 26 tests, each a real bug
cargo build --release
./target/release/nqf-lint examples/bad_mining_cluster.json # → 4 errors, exit 4
./target/release/nqf-lint examples/good_hg_cluster.json # → clean, exit 0
./target/release/nqf-lint examples/bad_floating_metal.xyz # → 2 errors, exit 2
./target/release/nqf-lint examples/bad_bohr_units.xyz # → flags Bohr units
./target/release/nqf-lint examples/water.xyz # → clean, exit 0
Limitations
This tool is deliberately narrow and states what it cannot verify.
- The electron-parity check assumes LANL2DZ core sizes. With a different ECP (SDD, def2-ECP, …) the number of replaced core electrons differs, and the parity verdict would be wrong for a known element on a non-LANL2DZ ECP. For elements it is unsure about, the tool abstains (a warning) rather than guess. If you use another ECP, treat parity findings on those atoms as advisory and verify by hand.
- Bonding is judged by distance, not a real bond-perception model. A terminal metal-oxo/nitrido with an unusually long M=X bond (> 1.75 Å) could be flagged as a "bare heteroatom". Rare, but possible.
- The conformer-seed check is a source-text heuristic, not a Python parser. It
recognises the common seeding patterns (
randomSeed=kwarg,params.randomSeed =); an exotic way of setting the seed could be missed and reported as unseeded.
The guiding rule is the opposite of the bug it exists to catch: when it cannot be sure, it says so, instead of emitting a confident wrong answer.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nqf_lint-0.2.0.tar.gz.
File metadata
- Download URL: nqf_lint-0.2.0.tar.gz
- Upload date:
- Size: 22.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c877a823d37bf93f8a2bd676ce0b8873c03a0f0dc4334b52a3a980faa90ca042
|
|
| MD5 |
4376fa7058825307742fc2e262800c89
|
|
| BLAKE2b-256 |
a37dd1445d0ceeb93b0a42ddd979d023e8556625fba5dc13e358c2201c44ed7b
|
File details
Details for the file nqf_lint-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: nqf_lint-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 385.5 kB
- Tags: CPython 3.8+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79bd700f8e5ba70dfef751524c0fecfd3c7de90be4c7fc86b79037fa28f52335
|
|
| MD5 |
576663725e566f0a6db551adc02ded5b
|
|
| BLAKE2b-256 |
50fbfc84c12cac138a620cb77dd78af62d3646f8f7fc35265d63058a48334865
|