nqf-lint 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: dtolnay/rust-toolchain@stable
14
+ with:
15
+ components: clippy, rustfmt
16
+ - name: Format
17
+ run: cargo fmt --check
18
+ - name: Clippy
19
+ run: cargo clippy -- -D warnings
20
+ - name: Test
21
+ run: cargo test --release
22
+ - name: Lint the example clusters (self-check)
23
+ run: |
24
+ cargo build --release
25
+ ./target/release/nqf-lint examples/good_hg_cluster.json
26
+ ! ./target/release/nqf-lint examples/bad_mining_cluster.json
@@ -0,0 +1,2 @@
1
+ target/
2
+ Cargo.lock
@@ -0,0 +1,243 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.4"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
16
+
17
+ [[package]]
18
+ name = "heck"
19
+ version = "0.5.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
22
+
23
+ [[package]]
24
+ name = "indoc"
25
+ version = "2.0.7"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
28
+ dependencies = [
29
+ "rustversion",
30
+ ]
31
+
32
+ [[package]]
33
+ name = "itoa"
34
+ version = "1.0.18"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
37
+
38
+ [[package]]
39
+ name = "libc"
40
+ version = "0.2.189"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
43
+
44
+ [[package]]
45
+ name = "memchr"
46
+ version = "2.8.3"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
49
+
50
+ [[package]]
51
+ name = "memoffset"
52
+ version = "0.9.1"
53
+ source = "registry+https://github.com/rust-lang/crates.io-index"
54
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
55
+ dependencies = [
56
+ "autocfg",
57
+ ]
58
+
59
+ [[package]]
60
+ name = "nqf-lint"
61
+ version = "0.2.0"
62
+ dependencies = [
63
+ "pyo3",
64
+ "serde",
65
+ "serde_json",
66
+ ]
67
+
68
+ [[package]]
69
+ name = "once_cell"
70
+ version = "1.21.4"
71
+ source = "registry+https://github.com/rust-lang/crates.io-index"
72
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
73
+
74
+ [[package]]
75
+ name = "portable-atomic"
76
+ version = "1.14.0"
77
+ source = "registry+https://github.com/rust-lang/crates.io-index"
78
+ checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
79
+
80
+ [[package]]
81
+ name = "proc-macro2"
82
+ version = "1.0.106"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
85
+ dependencies = [
86
+ "unicode-ident",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "pyo3"
91
+ version = "0.23.5"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
94
+ dependencies = [
95
+ "cfg-if",
96
+ "indoc",
97
+ "libc",
98
+ "memoffset",
99
+ "once_cell",
100
+ "portable-atomic",
101
+ "pyo3-build-config",
102
+ "pyo3-ffi",
103
+ "pyo3-macros",
104
+ "unindent",
105
+ ]
106
+
107
+ [[package]]
108
+ name = "pyo3-build-config"
109
+ version = "0.23.5"
110
+ source = "registry+https://github.com/rust-lang/crates.io-index"
111
+ checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
112
+ dependencies = [
113
+ "once_cell",
114
+ "target-lexicon",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-ffi"
119
+ version = "0.23.5"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
122
+ dependencies = [
123
+ "libc",
124
+ "pyo3-build-config",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "pyo3-macros"
129
+ version = "0.23.5"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
132
+ dependencies = [
133
+ "proc-macro2",
134
+ "pyo3-macros-backend",
135
+ "quote",
136
+ "syn",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "pyo3-macros-backend"
141
+ version = "0.23.5"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
144
+ dependencies = [
145
+ "heck",
146
+ "proc-macro2",
147
+ "pyo3-build-config",
148
+ "quote",
149
+ "syn",
150
+ ]
151
+
152
+ [[package]]
153
+ name = "quote"
154
+ version = "1.0.46"
155
+ source = "registry+https://github.com/rust-lang/crates.io-index"
156
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
157
+ dependencies = [
158
+ "proc-macro2",
159
+ ]
160
+
161
+ [[package]]
162
+ name = "rustversion"
163
+ version = "1.0.23"
164
+ source = "registry+https://github.com/rust-lang/crates.io-index"
165
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
166
+
167
+ [[package]]
168
+ name = "serde"
169
+ version = "1.0.228"
170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
171
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
172
+ dependencies = [
173
+ "serde_core",
174
+ "serde_derive",
175
+ ]
176
+
177
+ [[package]]
178
+ name = "serde_core"
179
+ version = "1.0.228"
180
+ source = "registry+https://github.com/rust-lang/crates.io-index"
181
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
182
+ dependencies = [
183
+ "serde_derive",
184
+ ]
185
+
186
+ [[package]]
187
+ name = "serde_derive"
188
+ version = "1.0.228"
189
+ source = "registry+https://github.com/rust-lang/crates.io-index"
190
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
191
+ dependencies = [
192
+ "proc-macro2",
193
+ "quote",
194
+ "syn",
195
+ ]
196
+
197
+ [[package]]
198
+ name = "serde_json"
199
+ version = "1.0.150"
200
+ source = "registry+https://github.com/rust-lang/crates.io-index"
201
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
202
+ dependencies = [
203
+ "itoa",
204
+ "memchr",
205
+ "serde",
206
+ "serde_core",
207
+ "zmij",
208
+ ]
209
+
210
+ [[package]]
211
+ name = "syn"
212
+ version = "2.0.119"
213
+ source = "registry+https://github.com/rust-lang/crates.io-index"
214
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
215
+ dependencies = [
216
+ "proc-macro2",
217
+ "quote",
218
+ "unicode-ident",
219
+ ]
220
+
221
+ [[package]]
222
+ name = "target-lexicon"
223
+ version = "0.12.16"
224
+ source = "registry+https://github.com/rust-lang/crates.io-index"
225
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
226
+
227
+ [[package]]
228
+ name = "unicode-ident"
229
+ version = "1.0.24"
230
+ source = "registry+https://github.com/rust-lang/crates.io-index"
231
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
232
+
233
+ [[package]]
234
+ name = "unindent"
235
+ version = "0.2.4"
236
+ source = "registry+https://github.com/rust-lang/crates.io-index"
237
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
238
+
239
+ [[package]]
240
+ name = "zmij"
241
+ version = "1.0.23"
242
+ source = "registry+https://github.com/rust-lang/crates.io-index"
243
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,38 @@
1
+ # Standalone crate — não faz parte do workspace da raiz (é um produto separado,
2
+ # candidato a repositório próprio no GitHub).
3
+ [workspace]
4
+
5
+ [package]
6
+ name = "nqf-lint"
7
+ version = "0.2.0"
8
+ edition = "2021"
9
+ authors = ["Cleiton Augusto"]
10
+ description = "Pre-flight linter for quantum-chemistry cluster/calculation setups — catches silent garbage-in before an SCF runs for hours and returns a wrong number."
11
+ license = "MIT"
12
+ readme = "README.md"
13
+ repository = "https://github.com/cleitonaugusto/nqf-lint"
14
+ homepage = "https://github.com/cleitonaugusto/nqf-lint"
15
+ keywords = ["quantum-chemistry", "linter", "chemistry", "cli", "validation"]
16
+ categories = ["command-line-utilities", "science", "development-tools"]
17
+
18
+ [[bin]]
19
+ name = "nqf-lint"
20
+ path = "src/main.rs"
21
+
22
+ [lib]
23
+ name = "nqf_lint"
24
+ path = "src/lib.rs"
25
+ # rlib for the CLI binary and downstream Rust crates; cdylib for the Python
26
+ # extension module built by maturin (only when the `python` feature is on).
27
+ crate-type = ["rlib", "cdylib"]
28
+
29
+ [dependencies]
30
+ serde = { version = "1", features = ["derive"] }
31
+ serde_json = "1"
32
+ # Optional: pulled in only for the Python bindings (feature `python`). A normal
33
+ # `cargo build` / crates.io build never compiles it, keeping the crate light.
34
+ # abi3-py38: one wheel works on CPython 3.8+ (stable ABI), instead of one per version.
35
+ pyo3 = { version = "0.23", features = ["extension-module", "abi3-py38"], optional = true }
36
+
37
+ [features]
38
+ python = ["dep:pyo3"]
nqf_lint-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cleiton Augusto Correa Bezerra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: nqf-lint
3
+ Version: 0.2.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Science/Research
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
10
+ Classifier: Topic :: Software Development :: Quality Assurance
11
+ License-File: LICENSE
12
+ Summary: 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.
13
+ Keywords: quantum-chemistry,linter,chemistry,computational-chemistry,validation
14
+ Home-Page: https://github.com/cleitonaugusto/nqf-lint
15
+ Author: Cleiton Augusto
16
+ License: MIT
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Project-URL: Homepage, https://github.com/cleitonaugusto/nqf-lint
20
+ Project-URL: Repository, https://github.com/cleitonaugusto/nqf-lint
21
+
22
+ # nqf-lint
23
+
24
+ **A pre-flight linter for quantum-chemistry cluster setups.**
25
+
26
+ A quantum-chemistry calculation can run for hours and return a number that *looks*
27
+ fine while the input was silently broken — an oxygen entered with no hydrogens, an
28
+ electron count made impossible by a forgotten pseudopotential core, a metal floating
29
+ with no coordination. The SCF either fails cryptically or, worse, converges to a
30
+ physically meaningless state that becomes a data point in someone's benchmark.
31
+
32
+ `nqf-lint` reads a declared cluster and runs cheap, deterministic checks that catch
33
+ these failure modes **before** the expensive calculation. It is written in Rust, has
34
+ no runtime dependencies, and exits with a nonzero code on error so it drops straight
35
+ into a `Makefile` or CI.
36
+
37
+ Every check corresponds to a real bug found in a production pipeline.
38
+
39
+ ## Usage
40
+
41
+ ```
42
+ nqf-lint geometry.xyz # a standard XYZ geometry
43
+ nqf-lint cluster.json # the tool's own cluster spec (charge/spin/ECP)
44
+ nqf-lint build_cluster.py # Python source (conformer determinism)
45
+ ```
46
+
47
+ **XYZ** is the universal format every quantum-chemistry package reads and writes,
48
+ so the linter runs on real inputs, not only its own JSON. XYZ carries no charge or
49
+ spin, so put them on the comment line — either as a bare `<charge> <mult>` pair or
50
+ as `charge=.. mult=..` — to enable the electron-count checks; without them, those
51
+ checks abstain and the geometry checks still run.
52
+
53
+ ```
54
+ 3
55
+ charge=0 mult=1
56
+ O 0.000 0.000 0.000
57
+ H 0.757 0.586 0.000
58
+ H -0.757 0.586 0.000
59
+ ```
60
+
61
+ The **JSON** spec adds what XYZ cannot express — the ECP list and the metal
62
+ oxidation state — for the ECP-aware parity and d¹⁰ spin checks:
63
+
64
+ ```json
65
+ {
66
+ "atoms": [
67
+ { "element": "Hg", "x": 0.0, "y": 0.0, "z": 0.0 },
68
+ { "element": "O", "x": 2.30, "y": 0.0, "z": 0.0 },
69
+ { "element": "H", "x": 2.60, "y": 0.80, "z": 0.0 },
70
+ { "element": "H", "x": 2.60, "y": -0.80, "z": 0.0 }
71
+ ],
72
+ "charge": 2,
73
+ "spin_multiplicity": 1,
74
+ "ecp_elements": ["Hg"]
75
+ }
76
+ ```
77
+
78
+ ## What it checks
79
+
80
+ The geometry checks (bare heteroatom, metal coordination, overlapping atoms) run
81
+ on any input — `.xyz` or `.json`. The electron-count checks need charge/spin, and
82
+ the ECP/d¹⁰ checks need the extra fields only `.json` carries.
83
+
84
+ **Geometry + electron count:**
85
+
86
+ | check | catches |
87
+ |---|---|
88
+ | **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. |
89
+ | **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. |
90
+ | **metal coordination** | a metal center with zero neighbours in range is floating — the fragment was built wrong. |
91
+ | **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²⁺. |
92
+ | **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. |
93
+ | **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. |
94
+
95
+ **Python source (`.py`):**
96
+
97
+ | check | catches |
98
+ |---|---|
99
+ | **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. |
100
+
101
+ It **never guesses**. When it cannot verify something — an unknown ECP core, an
102
+ unknown element — it abstains with a warning rather than emit a false verdict,
103
+ because "cannot verify" is exactly where silent bugs live.
104
+
105
+ ## Design principle
106
+
107
+ Every check must fail loudly on a known-bad input and pass a known-good one. The test
108
+ suite is built from real bugs: the mining cluster whose "waters" were bare oxygens
109
+ (`{Co:1, O:7, …}`, zero hydrogens), the Hg cluster whose charge was left at the
110
+ all-electron value after switching on an ECP, the metal built with no ligand nearby.
111
+ A check that cannot demonstrate both directions does not ship.
112
+
113
+ ## Build
114
+
115
+ ```
116
+ cargo test # 26 tests, each a real bug
117
+ cargo build --release
118
+ ./target/release/nqf-lint examples/bad_mining_cluster.json # → 4 errors, exit 4
119
+ ./target/release/nqf-lint examples/good_hg_cluster.json # → clean, exit 0
120
+ ./target/release/nqf-lint examples/bad_floating_metal.xyz # → 2 errors, exit 2
121
+ ./target/release/nqf-lint examples/bad_bohr_units.xyz # → flags Bohr units
122
+ ./target/release/nqf-lint examples/water.xyz # → clean, exit 0
123
+ ```
124
+
125
+ ## Limitations
126
+
127
+ This tool is deliberately narrow and states what it cannot verify.
128
+
129
+ - **The electron-parity check assumes LANL2DZ core sizes.** With a different ECP
130
+ (SDD, def2-ECP, …) the number of replaced core electrons differs, and the parity
131
+ verdict would be wrong for a *known* element on a non-LANL2DZ ECP. For elements it
132
+ is unsure about, the tool abstains (a warning) rather than guess. If you use another
133
+ ECP, treat parity findings on those atoms as advisory and verify by hand.
134
+ - **Bonding is judged by distance, not a real bond-perception model.** A terminal
135
+ metal-oxo/nitrido with an unusually long M=X bond (> 1.75 Å) could be flagged as a
136
+ "bare heteroatom". Rare, but possible.
137
+ - **The conformer-seed check is a source-text heuristic, not a Python parser.** It
138
+ recognises the common seeding patterns (`randomSeed=` kwarg, `params.randomSeed =`);
139
+ an exotic way of setting the seed could be missed and reported as unseeded.
140
+
141
+ The guiding rule is the opposite of the bug it exists to catch: when it cannot be
142
+ sure, it says so, instead of emitting a confident wrong answer.
143
+
144
+ ## License
145
+
146
+ MIT.
@@ -0,0 +1,125 @@
1
+ # nqf-lint
2
+
3
+ **A pre-flight linter for quantum-chemistry cluster setups.**
4
+
5
+ A quantum-chemistry calculation can run for hours and return a number that *looks*
6
+ fine while the input was silently broken — an oxygen entered with no hydrogens, an
7
+ electron count made impossible by a forgotten pseudopotential core, a metal floating
8
+ with no coordination. The SCF either fails cryptically or, worse, converges to a
9
+ physically meaningless state that becomes a data point in someone's benchmark.
10
+
11
+ `nqf-lint` reads a declared cluster and runs cheap, deterministic checks that catch
12
+ these failure modes **before** the expensive calculation. It is written in Rust, has
13
+ no runtime dependencies, and exits with a nonzero code on error so it drops straight
14
+ into a `Makefile` or CI.
15
+
16
+ Every check corresponds to a real bug found in a production pipeline.
17
+
18
+ ## Usage
19
+
20
+ ```
21
+ nqf-lint geometry.xyz # a standard XYZ geometry
22
+ nqf-lint cluster.json # the tool's own cluster spec (charge/spin/ECP)
23
+ nqf-lint build_cluster.py # Python source (conformer determinism)
24
+ ```
25
+
26
+ **XYZ** is the universal format every quantum-chemistry package reads and writes,
27
+ so the linter runs on real inputs, not only its own JSON. XYZ carries no charge or
28
+ spin, so put them on the comment line — either as a bare `<charge> <mult>` pair or
29
+ as `charge=.. mult=..` — to enable the electron-count checks; without them, those
30
+ checks abstain and the geometry checks still run.
31
+
32
+ ```
33
+ 3
34
+ charge=0 mult=1
35
+ O 0.000 0.000 0.000
36
+ H 0.757 0.586 0.000
37
+ H -0.757 0.586 0.000
38
+ ```
39
+
40
+ The **JSON** spec adds what XYZ cannot express — the ECP list and the metal
41
+ oxidation state — for the ECP-aware parity and d¹⁰ spin checks:
42
+
43
+ ```json
44
+ {
45
+ "atoms": [
46
+ { "element": "Hg", "x": 0.0, "y": 0.0, "z": 0.0 },
47
+ { "element": "O", "x": 2.30, "y": 0.0, "z": 0.0 },
48
+ { "element": "H", "x": 2.60, "y": 0.80, "z": 0.0 },
49
+ { "element": "H", "x": 2.60, "y": -0.80, "z": 0.0 }
50
+ ],
51
+ "charge": 2,
52
+ "spin_multiplicity": 1,
53
+ "ecp_elements": ["Hg"]
54
+ }
55
+ ```
56
+
57
+ ## What it checks
58
+
59
+ The geometry checks (bare heteroatom, metal coordination, overlapping atoms) run
60
+ on any input — `.xyz` or `.json`. The electron-count checks need charge/spin, and
61
+ the ECP/d¹⁰ checks need the extra fields only `.json` carries.
62
+
63
+ **Geometry + electron count:**
64
+
65
+ | check | catches |
66
+ |---|---|
67
+ | **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. |
68
+ | **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. |
69
+ | **metal coordination** | a metal center with zero neighbours in range is floating — the fragment was built wrong. |
70
+ | **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²⁺. |
71
+ | **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. |
72
+ | **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. |
73
+
74
+ **Python source (`.py`):**
75
+
76
+ | check | catches |
77
+ |---|---|
78
+ | **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. |
79
+
80
+ It **never guesses**. When it cannot verify something — an unknown ECP core, an
81
+ unknown element — it abstains with a warning rather than emit a false verdict,
82
+ because "cannot verify" is exactly where silent bugs live.
83
+
84
+ ## Design principle
85
+
86
+ Every check must fail loudly on a known-bad input and pass a known-good one. The test
87
+ suite is built from real bugs: the mining cluster whose "waters" were bare oxygens
88
+ (`{Co:1, O:7, …}`, zero hydrogens), the Hg cluster whose charge was left at the
89
+ all-electron value after switching on an ECP, the metal built with no ligand nearby.
90
+ A check that cannot demonstrate both directions does not ship.
91
+
92
+ ## Build
93
+
94
+ ```
95
+ cargo test # 26 tests, each a real bug
96
+ cargo build --release
97
+ ./target/release/nqf-lint examples/bad_mining_cluster.json # → 4 errors, exit 4
98
+ ./target/release/nqf-lint examples/good_hg_cluster.json # → clean, exit 0
99
+ ./target/release/nqf-lint examples/bad_floating_metal.xyz # → 2 errors, exit 2
100
+ ./target/release/nqf-lint examples/bad_bohr_units.xyz # → flags Bohr units
101
+ ./target/release/nqf-lint examples/water.xyz # → clean, exit 0
102
+ ```
103
+
104
+ ## Limitations
105
+
106
+ This tool is deliberately narrow and states what it cannot verify.
107
+
108
+ - **The electron-parity check assumes LANL2DZ core sizes.** With a different ECP
109
+ (SDD, def2-ECP, …) the number of replaced core electrons differs, and the parity
110
+ verdict would be wrong for a *known* element on a non-LANL2DZ ECP. For elements it
111
+ is unsure about, the tool abstains (a warning) rather than guess. If you use another
112
+ ECP, treat parity findings on those atoms as advisory and verify by hand.
113
+ - **Bonding is judged by distance, not a real bond-perception model.** A terminal
114
+ metal-oxo/nitrido with an unusually long M=X bond (> 1.75 Å) could be flagged as a
115
+ "bare heteroatom". Rare, but possible.
116
+ - **The conformer-seed check is a source-text heuristic, not a Python parser.** It
117
+ recognises the common seeding patterns (`randomSeed=` kwarg, `params.randomSeed =`);
118
+ an exotic way of setting the seed could be missed and reported as unseeded.
119
+
120
+ The guiding rule is the opposite of the bug it exists to catch: when it cannot be
121
+ sure, it says so, instead of emitting a confident wrong answer.
122
+
123
+ ## License
124
+
125
+ MIT.
@@ -0,0 +1,5 @@
1
+ 3
2
+ water written in Bohr (atomic units) instead of Angstrom — a classic footgun
3
+ O 0.00000 0.00000 0.0
4
+ H 1.43044 1.10738 0.0
5
+ H -1.43044 1.10738 0.0
@@ -0,0 +1,4 @@
1
+ 2
2
+ Zn and O written too far apart to be bonded — a broken fragment build
3
+ Zn 0.000 0.000 0.000
4
+ O 3.000 0.000 0.000
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": "O bug real da auditoria de mineração: 'águas' entradas como oxigênios NUS, todo H descartado, e carga formal deixada em 0 (paridade impossível). Composição observada em produção: {Co:1, O:7, ...}, zero H.",
3
+ "atoms": [
4
+ { "element": "Co", "x": 0.0, "y": 0.0, "z": 0.0 },
5
+ { "element": "O", "x": 2.10, "y": 0.0, "z": 0.0 },
6
+ { "element": "O", "x": -2.10,"y": 0.0, "z": 0.0 },
7
+ { "element": "O", "x": 0.0, "y": 2.10, "z": 0.0 },
8
+ { "element": "O", "x": 0.0, "y": -2.10,"z": 0.0 }
9
+ ],
10
+ "charge": 0,
11
+ "spin_multiplicity": 4,
12
+ "ecp_elements": []
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": "Zn2+ é d10 (camada fechada) e DEVE ser singleto (M=1). Aqui foi declarado tripleto (M=3) — erro real de estado de spin, a mesma classe de bug que o benchmark de Ni cometeu ao rodar singleto onde não devia.",
3
+ "atoms": [
4
+ { "element": "Zn", "x": 0.0, "y": 0.0, "z": 0.0 },
5
+ { "element": "O", "x": 2.10, "y": 0.0, "z": 0.0 },
6
+ { "element": "H", "x": 2.40, "y": 0.80, "z": 0.0 },
7
+ { "element": "H", "x": 2.40, "y": -0.80,"z": 0.0 }
8
+ ],
9
+ "charge": 2,
10
+ "spin_multiplicity": 3,
11
+ "ecp_elements": [],
12
+ "metal_oxidation_state": 2
13
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "_comment": "Cluster Hg2+ bem-formado: duas águas reais (O + 2 H), carga +2, singleto, Hg com ECP LANL2DZ. Deve passar limpo.",
3
+ "atoms": [
4
+ { "element": "Hg", "x": 0.0, "y": 0.0, "z": 0.0 },
5
+ { "element": "O", "x": 2.30, "y": 0.0, "z": 0.0 },
6
+ { "element": "H", "x": 2.60, "y": 0.80, "z": 0.0 },
7
+ { "element": "H", "x": 2.60, "y": -0.80,"z": 0.0 },
8
+ { "element": "O", "x": -2.30,"y": 0.0, "z": 0.0 },
9
+ { "element": "H", "x": -2.60,"y": 0.80, "z": 0.0 },
10
+ { "element": "H", "x": -2.60,"y": -0.80,"z": 0.0 }
11
+ ],
12
+ "charge": 2,
13
+ "spin_multiplicity": 1,
14
+ "ecp_elements": ["Hg"],
15
+ "metal_oxidation_state": 2
16
+ }
@@ -0,0 +1,5 @@
1
+ 3
2
+ charge=0 mult=1
3
+ O 0.000 0.000 0.000
4
+ H 0.757 0.586 0.000
5
+ H -0.757 0.586 0.000