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.
- nqf_lint-0.2.0/.github/workflows/ci.yml +26 -0
- nqf_lint-0.2.0/.gitignore +2 -0
- nqf_lint-0.2.0/Cargo.lock +243 -0
- nqf_lint-0.2.0/Cargo.toml +38 -0
- nqf_lint-0.2.0/LICENSE +21 -0
- nqf_lint-0.2.0/PKG-INFO +146 -0
- nqf_lint-0.2.0/README.md +125 -0
- nqf_lint-0.2.0/examples/bad_bohr_units.xyz +5 -0
- nqf_lint-0.2.0/examples/bad_floating_metal.xyz +4 -0
- nqf_lint-0.2.0/examples/bad_mining_cluster.json +13 -0
- nqf_lint-0.2.0/examples/bad_spin_state.json +13 -0
- nqf_lint-0.2.0/examples/good_hg_cluster.json +16 -0
- nqf_lint-0.2.0/examples/water.xyz +5 -0
- nqf_lint-0.2.0/pyproject.toml +33 -0
- nqf_lint-0.2.0/src/lib.rs +742 -0
- nqf_lint-0.2.0/src/main.rs +73 -0
- nqf_lint-0.2.0/src/python.rs +124 -0
- nqf_lint-0.2.0/src/source.rs +265 -0
- nqf_lint-0.2.0/src/xyz.rs +179 -0
|
@@ -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,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.
|
nqf_lint-0.2.0/PKG-INFO
ADDED
|
@@ -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.
|
nqf_lint-0.2.0/README.md
ADDED
|
@@ -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,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
|
+
}
|