Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nqf_lint-0.2.0.tar.gz (22.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nqf_lint-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl (385.5 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

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

Hashes for nqf_lint-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c877a823d37bf93f8a2bd676ce0b8873c03a0f0dc4334b52a3a980faa90ca042
MD5 4376fa7058825307742fc2e262800c89
BLAKE2b-256 a37dd1445d0ceeb93b0a42ddd979d023e8556625fba5dc13e358c2201c44ed7b

See more details on using hashes here.

File details

Details for the file nqf_lint-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for nqf_lint-0.2.0-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 79bd700f8e5ba70dfef751524c0fecfd3c7de90be4c7fc86b79037fa28f52335
MD5 576663725e566f0a6db551adc02ded5b
BLAKE2b-256 50fbfc84c12cac138a620cb77dd78af62d3646f8f7fc35265d63058a48334865

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page