quantui 0.5.1__py3-none-any.whl
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.
- quantui/__init__.py +311 -0
- quantui/analytics.py +609 -0
- quantui/app.py +5650 -0
- quantui/app_analysis.py +662 -0
- quantui/app_builders.py +2465 -0
- quantui/app_exports.py +194 -0
- quantui/app_formatters.py +493 -0
- quantui/app_history.py +624 -0
- quantui/app_runflow.py +1544 -0
- quantui/app_visualization.py +2620 -0
- quantui/ase_bridge.py +236 -0
- quantui/benchmarks.py +1543 -0
- quantui/c_stderr.py +124 -0
- quantui/cactus.py +88 -0
- quantui/calc_log.py +1116 -0
- quantui/calculator.py +204 -0
- quantui/cancellation.py +88 -0
- quantui/cli.py +288 -0
- quantui/comparison.py +306 -0
- quantui/config.py +725 -0
- quantui/data/js/3Dmol-min.js +2 -0
- quantui/data/js/3Dmol-min.js.LICENSE.txt +5 -0
- quantui/data/library/library.sqlite +0 -0
- quantui/data/manifests/bulk_qm9.json +1 -0
- quantui/data/manifests/curated.json +15482 -0
- quantui/data/manifests/presets.json +816 -0
- quantui/descriptor_cards.py +186 -0
- quantui/freq_calc.py +712 -0
- quantui/freq_ir_workers.py +229 -0
- quantui/gpu_offload.py +278 -0
- quantui/help_content.py +474 -0
- quantui/ir_plot.py +130 -0
- quantui/issue_tracker.py +170 -0
- quantui/live_log.py +387 -0
- quantui/log_utils.py +492 -0
- quantui/molecule.py +577 -0
- quantui/molecule_library.py +433 -0
- quantui/nmr_calc.py +437 -0
- quantui/optimizer.py +670 -0
- quantui/orbital_visualization.py +1102 -0
- quantui/pes_scan.py +420 -0
- quantui/preopt.py +355 -0
- quantui/progress.py +111 -0
- quantui/pubchem.py +1157 -0
- quantui/reorganization_energy.py +435 -0
- quantui/results_storage.py +902 -0
- quantui/security.py +14 -0
- quantui/session_calc.py +622 -0
- quantui/structure_providers.py +277 -0
- quantui/tddft_calc.py +307 -0
- quantui/user_settings.py +238 -0
- quantui/utils.py +287 -0
- quantui/vib_cache.py +247 -0
- quantui/visualization_py3dmol.py +593 -0
- quantui/viz_assets.py +101 -0
- quantui/viz_backend_router.py +243 -0
- quantui-0.5.1.dist-info/METADATA +533 -0
- quantui-0.5.1.dist-info/RECORD +62 -0
- quantui-0.5.1.dist-info/WHEEL +5 -0
- quantui-0.5.1.dist-info/entry_points.txt +2 -0
- quantui-0.5.1.dist-info/licenses/LICENSE +21 -0
- quantui-0.5.1.dist-info/top_level.txt +1 -0
quantui/config.py
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuantUI Configuration Module
|
|
3
|
+
|
|
4
|
+
Configuration constants and defaults for the local teaching interface.
|
|
5
|
+
SLURM resource limits, job history paths, and cluster settings have been
|
|
6
|
+
removed — this version runs calculations in the current Jupyter session.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
# Project root
|
|
13
|
+
PROJECT_ROOT = Path(__file__).parent.parent
|
|
14
|
+
|
|
15
|
+
# Supported quantum chemistry methods
|
|
16
|
+
SUPPORTED_METHODS = [
|
|
17
|
+
"RHF",
|
|
18
|
+
"UHF",
|
|
19
|
+
"B3LYP",
|
|
20
|
+
"PBE",
|
|
21
|
+
"PBE0",
|
|
22
|
+
"M06-2X",
|
|
23
|
+
"wB97X-D",
|
|
24
|
+
"CAM-B3LYP",
|
|
25
|
+
"M06-L",
|
|
26
|
+
"HSE06",
|
|
27
|
+
"PBE-D3",
|
|
28
|
+
"MP2",
|
|
29
|
+
"CCSD",
|
|
30
|
+
"CCSD(T)",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Post-HF wavefunction methods. Single-point-only in QuantUI: session_calc.py
|
|
34
|
+
# is the only entry point that special-cases them (an RHF/ROHF reference,
|
|
35
|
+
# auto-dispatched by PySCF's scf.RHF() factory for open-shell input, plus an
|
|
36
|
+
# mp.MP2/cc.CCSD post-SCF step). optimizer.py, freq_calc.py, tddft_calc.py,
|
|
37
|
+
# and nmr_calc.py have no such special-casing — without an early guard,
|
|
38
|
+
# selecting one of these methods there falls through to the DFT branch,
|
|
39
|
+
# which sets e.g. mf.xc = "CCSD" and fails deep inside PySCF with a cryptic
|
|
40
|
+
# "LibXCFunctional: name 'CCSD' not found" instead of a clear message.
|
|
41
|
+
POST_HF_METHODS: frozenset = frozenset({"MP2", "CCSD", "CCSD(T)"})
|
|
42
|
+
|
|
43
|
+
# Educational metadata for each method — shown to students in the UI
|
|
44
|
+
METHOD_INFO = {
|
|
45
|
+
"RHF": {
|
|
46
|
+
"type": "hf",
|
|
47
|
+
"label": "RHF — Restricted Hartree-Fock",
|
|
48
|
+
"description": (
|
|
49
|
+
"Classical wavefunction method for closed-shell molecules. "
|
|
50
|
+
"All electrons are paired; no electron correlation beyond mean-field. "
|
|
51
|
+
"Fast and exact within HF theory."
|
|
52
|
+
),
|
|
53
|
+
"use_for": "Closed-shell molecules with all electrons paired (singlet state).",
|
|
54
|
+
},
|
|
55
|
+
"UHF": {
|
|
56
|
+
"type": "hf",
|
|
57
|
+
"label": "UHF — Unrestricted Hartree-Fock",
|
|
58
|
+
"description": (
|
|
59
|
+
"HF for open-shell systems. Alpha and beta electrons occupy "
|
|
60
|
+
"different spatial orbitals. Can suffer from spin contamination."
|
|
61
|
+
),
|
|
62
|
+
"use_for": "Radicals, triplet states, any molecule with unpaired electrons.",
|
|
63
|
+
},
|
|
64
|
+
"B3LYP": {
|
|
65
|
+
"type": "dft",
|
|
66
|
+
"label": "B3LYP — DFT Hybrid Functional",
|
|
67
|
+
"description": (
|
|
68
|
+
"The most widely used DFT functional. Mixes HF exchange (20%) with "
|
|
69
|
+
"Becke exchange and Lee-Yang-Parr correlation. Good all-around accuracy "
|
|
70
|
+
"at moderate cost."
|
|
71
|
+
),
|
|
72
|
+
"use_for": "General organic chemistry, ground state geometries and energetics.",
|
|
73
|
+
},
|
|
74
|
+
"PBE": {
|
|
75
|
+
"type": "dft",
|
|
76
|
+
"label": "PBE — DFT GGA Functional",
|
|
77
|
+
"description": (
|
|
78
|
+
"Generalized gradient approximation functional. No HF exchange, so "
|
|
79
|
+
"faster than hybrids. Works well for larger systems and metals."
|
|
80
|
+
),
|
|
81
|
+
"use_for": "Large molecules, solid-state systems, when speed matters most.",
|
|
82
|
+
},
|
|
83
|
+
"PBE0": {
|
|
84
|
+
"type": "dft",
|
|
85
|
+
"label": "PBE0 — DFT Hybrid Functional",
|
|
86
|
+
"description": (
|
|
87
|
+
"Hybrid version of PBE with 25% HF exchange. More accurate than "
|
|
88
|
+
"pure PBE for most molecular properties; similar cost to B3LYP."
|
|
89
|
+
),
|
|
90
|
+
"use_for": "General purpose; better charge-transfer and band gaps than B3LYP.",
|
|
91
|
+
},
|
|
92
|
+
"M06-2X": {
|
|
93
|
+
"type": "dft",
|
|
94
|
+
"label": "M06-2X — Meta-Hybrid DFT",
|
|
95
|
+
"description": (
|
|
96
|
+
"Minnesota meta-hybrid functional with 54% HF exchange. Excellent "
|
|
97
|
+
"for thermochemistry and reaction barrier heights in organic systems. "
|
|
98
|
+
"Not recommended for transition metals."
|
|
99
|
+
),
|
|
100
|
+
"use_for": "Organic reaction energies, conformational analysis, barrier heights.",
|
|
101
|
+
},
|
|
102
|
+
"wB97X-D": {
|
|
103
|
+
"type": "dft",
|
|
104
|
+
"label": "wB97X-D — Range-Separated Hybrid + D3 Dispersion",
|
|
105
|
+
"description": (
|
|
106
|
+
"Range-separated hybrid functional with empirical D3 dispersion correction. "
|
|
107
|
+
"Excellent for non-covalent interactions, charge-transfer excitations, "
|
|
108
|
+
"and systems where long-range exchange matters."
|
|
109
|
+
),
|
|
110
|
+
"use_for": "Non-covalent interactions, excited states, large organic molecules.",
|
|
111
|
+
},
|
|
112
|
+
"CAM-B3LYP": {
|
|
113
|
+
"type": "dft",
|
|
114
|
+
"label": "CAM-B3LYP — Coulomb-Attenuating B3LYP",
|
|
115
|
+
"description": (
|
|
116
|
+
"Range-separated version of B3LYP. More reliable than B3LYP for "
|
|
117
|
+
"charge-transfer excited states and Rydberg transitions. "
|
|
118
|
+
"Good general-purpose alternative to B3LYP."
|
|
119
|
+
),
|
|
120
|
+
"use_for": "Charge-transfer states, UV-Vis spectra, long-range interactions.",
|
|
121
|
+
},
|
|
122
|
+
"M06-L": {
|
|
123
|
+
"type": "dft",
|
|
124
|
+
"label": "M06-L — Local Meta-GGA DFT",
|
|
125
|
+
"description": (
|
|
126
|
+
"Local (no HF exchange) Minnesota meta-GGA. Faster than hybrid "
|
|
127
|
+
"functionals for the same system size. Good for transition metals "
|
|
128
|
+
"and main-group thermochemistry."
|
|
129
|
+
),
|
|
130
|
+
"use_for": "Larger molecules where hybrid cost is prohibitive; transition metals.",
|
|
131
|
+
},
|
|
132
|
+
"HSE06": {
|
|
133
|
+
"type": "dft",
|
|
134
|
+
"label": "HSE06 — Screened Hybrid DFT",
|
|
135
|
+
"description": (
|
|
136
|
+
"Heyd-Scuseria-Ernzerhof screened hybrid. Uses short-range HF exchange "
|
|
137
|
+
"only, making it efficient for large systems. Often used for solids; "
|
|
138
|
+
"also accurate for molecular band gaps."
|
|
139
|
+
),
|
|
140
|
+
"use_for": "Band gaps, large molecules, when PBE0 is too expensive.",
|
|
141
|
+
},
|
|
142
|
+
"PBE-D3": {
|
|
143
|
+
"type": "dft",
|
|
144
|
+
"label": "PBE-D3 — PBE + D3 Dispersion Correction",
|
|
145
|
+
"description": (
|
|
146
|
+
"PBE GGA functional with Grimme's D3BJ empirical dispersion correction. "
|
|
147
|
+
"Dramatically improves non-covalent interaction energies over plain PBE "
|
|
148
|
+
"at negligible extra cost."
|
|
149
|
+
),
|
|
150
|
+
"use_for": "Van der Waals complexes, stacking interactions, large organic molecules.",
|
|
151
|
+
},
|
|
152
|
+
"MP2": {
|
|
153
|
+
"type": "wavefunction",
|
|
154
|
+
"label": "MP2 — 2nd-Order Møller-Plesset",
|
|
155
|
+
"description": (
|
|
156
|
+
"Post-HF wavefunction method that adds electron correlation via 2nd-order "
|
|
157
|
+
"perturbation theory. More accurate than HF for energetics and geometries, "
|
|
158
|
+
"but scales as O(N⁵). Avoid for molecules with > ~20 heavy atoms."
|
|
159
|
+
),
|
|
160
|
+
"use_for": "Accurate energetics for small closed-shell molecules; bond dissociation.",
|
|
161
|
+
},
|
|
162
|
+
"CCSD": {
|
|
163
|
+
"type": "wavefunction",
|
|
164
|
+
"label": "CCSD — Coupled Cluster with Singles and Doubles",
|
|
165
|
+
"description": (
|
|
166
|
+
"Post-HF coupled-cluster method that includes all single and double "
|
|
167
|
+
"excitations from the HF reference. Often called the gold standard for "
|
|
168
|
+
"single-reference systems — significantly more accurate than MP2 — but "
|
|
169
|
+
"scales as O(N⁶). Memory and runtime both grow steeply with basis size; "
|
|
170
|
+
"expect very small molecules (~10 heavy atoms or fewer) only."
|
|
171
|
+
),
|
|
172
|
+
"use_for": "High-accuracy benchmarks for small closed-shell molecules.",
|
|
173
|
+
},
|
|
174
|
+
"CCSD(T)": {
|
|
175
|
+
"type": "wavefunction",
|
|
176
|
+
"label": "CCSD(T) — CCSD with Perturbative Triples",
|
|
177
|
+
"description": (
|
|
178
|
+
"Adds a perturbative correction for connected triple excitations on top of "
|
|
179
|
+
"CCSD. Routinely called the 'gold standard' of single-reference electronic "
|
|
180
|
+
"structure when paired with a large basis set. Scales as O(N⁷); the (T) "
|
|
181
|
+
"correction alone is typically the cost bottleneck. Reserve for the "
|
|
182
|
+
"smallest molecules where benchmark-quality energies are required."
|
|
183
|
+
),
|
|
184
|
+
"use_for": "Reference-quality energies and barrier heights for tiny molecules.",
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Supported basis sets
|
|
189
|
+
SUPPORTED_BASIS_SETS = [
|
|
190
|
+
"STO-3G",
|
|
191
|
+
"3-21G",
|
|
192
|
+
"6-31G",
|
|
193
|
+
"6-31G*",
|
|
194
|
+
"6-31G**",
|
|
195
|
+
"cc-pVDZ",
|
|
196
|
+
"cc-pVTZ",
|
|
197
|
+
"def2-SVP",
|
|
198
|
+
"def2-TZVP",
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def pople_notation_alias(basis: str) -> str:
|
|
203
|
+
"""Return the parenthesis spelling of a starred Pople basis, else ``""``.
|
|
204
|
+
|
|
205
|
+
``6-31G*`` and ``6-31G(d)`` are the *same basis set* written two ways, and a
|
|
206
|
+
student who learned one notation has no way to know the other is not a
|
|
207
|
+
different, missing option. Both spellings are accepted by PySCF (verified
|
|
208
|
+
2026-07-29: identical AO counts for the ``*``/``(d)`` and ``**``/``(d,p)``
|
|
209
|
+
pairs), so either is safe to show and to type.
|
|
210
|
+
|
|
211
|
+
The mapping is purely notational, so it is derived rather than tabulated —
|
|
212
|
+
that way it also covers names not currently in ``SUPPORTED_BASIS_SETS``
|
|
213
|
+
(``6-311G**``, ``6-31+G*``, …) if the dropdown grows:
|
|
214
|
+
|
|
215
|
+
- trailing ``**`` → ``(d,p)`` — polarisation on heavy atoms *and* hydrogens
|
|
216
|
+
- trailing ``*`` → ``(d)`` — polarisation on heavy atoms only
|
|
217
|
+
|
|
218
|
+
Returns an empty string for anything without a trailing star (``6-31G``,
|
|
219
|
+
``STO-3G``, ``cc-pVDZ``, ``def2-SVP``), which have no alternate spelling.
|
|
220
|
+
"""
|
|
221
|
+
if basis.endswith("**"):
|
|
222
|
+
return f"{basis[:-2]}(d,p)"
|
|
223
|
+
if basis.endswith("*"):
|
|
224
|
+
return f"{basis[:-1]}(d)"
|
|
225
|
+
return ""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Implicit solvent options — name → dielectric constant (ε)
|
|
229
|
+
SOLVENT_OPTIONS: Dict[str, float] = {
|
|
230
|
+
"Water": 78.39,
|
|
231
|
+
"Ethanol": 24.55,
|
|
232
|
+
"THF": 7.58,
|
|
233
|
+
"DMSO": 46.70,
|
|
234
|
+
"Acetonitrile": 35.69,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# TMS isotropic shielding reference constants for NMR chemical shift computation.
|
|
238
|
+
# Key: "method/basis" → {element: σ_TMS (ppm)}. δ = σ_TMS − σ_molecule.
|
|
239
|
+
# Source: Cheeseman et al., J. Chem. Phys. 104 (1996) 5497; CCCBDB.
|
|
240
|
+
NMR_REFERENCE_SHIELDINGS: Dict[str, Dict[str, float]] = {
|
|
241
|
+
"B3LYP/6-31G*": {"H": 31.72, "C": 183.71},
|
|
242
|
+
"B3LYP/6-311G**": {"H": 31.60, "C": 188.94},
|
|
243
|
+
"B3LYP/cc-pVDZ": {"H": 31.54, "C": 186.12},
|
|
244
|
+
"B3LYP/def2-SVP": {"H": 31.65, "C": 184.20},
|
|
245
|
+
"RHF/6-31G*": {"H": 32.00, "C": 196.00},
|
|
246
|
+
"RHF/STO-3G": {"H": 30.50, "C": 195.00},
|
|
247
|
+
"PBE0/6-31G*": {"H": 31.60, "C": 184.50},
|
|
248
|
+
"PBE/6-31G*": {"H": 31.50, "C": 185.00},
|
|
249
|
+
}
|
|
250
|
+
NMR_DEFAULT_REFERENCE: Dict[str, float] = {"H": 31.72, "C": 183.71} # B3LYP/6-31G*
|
|
251
|
+
|
|
252
|
+
# Default calculation settings
|
|
253
|
+
DEFAULT_METHOD = "RHF"
|
|
254
|
+
DEFAULT_BASIS = "6-31G"
|
|
255
|
+
DEFAULT_CHARGE = 0
|
|
256
|
+
DEFAULT_MULTIPLICITY = 1
|
|
257
|
+
|
|
258
|
+
# Geometry optimization defaults
|
|
259
|
+
DEFAULT_FMAX: float = 0.05 # eV/Å force convergence threshold
|
|
260
|
+
DEFAULT_OPT_STEPS: int = 200 # maximum BFGS optimizer steps
|
|
261
|
+
|
|
262
|
+
# Widget styling
|
|
263
|
+
WIDGET_LAYOUT = {
|
|
264
|
+
"width": "400px",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
DESCRIPTION_WIDTH = "150px"
|
|
268
|
+
|
|
269
|
+
# ── External structure resolvers ─────────────────────────────────────────────
|
|
270
|
+
# Network + throttle settings shared by the PubChem client (and, later, the
|
|
271
|
+
# NCI CACTUS resolver). All timeouts/limits live here per constraint #5.
|
|
272
|
+
PUBCHEM_TIMEOUT_S: float = 15.0 # per-request HTTP timeout
|
|
273
|
+
PUBCHEM_AVAILABILITY_TIMEOUT_S: float = 5.0 # quick reachability probe
|
|
274
|
+
PUBCHEM_MAX_RETRIES: int = 3 # bounded retries on 503 / throttling
|
|
275
|
+
PUBCHEM_BACKOFF_BASE_S: float = 0.5 # exponential back-off base (×2**attempt)
|
|
276
|
+
PUBCHEM_BACKOFF_MAX_S: float = 8.0 # cap on a single back-off sleep
|
|
277
|
+
# Proactive client-side rate limit. PUG-REST allows ~5 req/s; stay conservative
|
|
278
|
+
# so a classroom of simultaneous users doesn't trip the server-side throttle.
|
|
279
|
+
PUBCHEM_MIN_REQUEST_INTERVAL_S: float = 0.25
|
|
280
|
+
|
|
281
|
+
# NCI CACTUS Chemical Identifier Resolver — chained fallback after PubChem
|
|
282
|
+
# (resolves name / CAS / InChI / SMILES → 3D SDF; no API key). CACTUS is often
|
|
283
|
+
# slow/down, so keep the read timeout short — it's only a fallback and must not
|
|
284
|
+
# hang the search. Connect timeout fails fast when the host is unreachable.
|
|
285
|
+
CACTUS_TIMEOUT_S: float = 8.0
|
|
286
|
+
CACTUS_CONNECT_TIMEOUT_S: float = 4.0
|
|
287
|
+
|
|
288
|
+
# Bohr radius, in Angstrom — the exact value ``pyscf.data.nist.BOHR`` uses
|
|
289
|
+
# internally, so unit conversions here stay consistent with what PySCF
|
|
290
|
+
# actually computed with. (L audit fix: optimizer.py and freq_calc.py each
|
|
291
|
+
# hand-typed their own slightly different literal for this.)
|
|
292
|
+
BOHR_TO_ANGSTROM: float = 0.52917721092
|
|
293
|
+
|
|
294
|
+
# Bundled-library size budget + heavy-atom ceilings.
|
|
295
|
+
# These are QC *starting* geometries, so keep them runnable in a classroom.
|
|
296
|
+
LIBRARY_SIZE_BUDGET_BYTES: int = 10 * 1024 * 1024 # 10 MB
|
|
297
|
+
LIBRARY_HEAVY_ATOM_CEILING_CURATED: int = 30 # named drugs run a bit larger
|
|
298
|
+
LIBRARY_HEAVY_ATOM_CEILING_BULK: int = 9 # QM9 caps here anyway
|
|
299
|
+
|
|
300
|
+
# Common chemical name → Hill formula, for History search (HIST.7).
|
|
301
|
+
# The bundled QM9-derived library names its simplest molecules by formula
|
|
302
|
+
# (benzene is stored as "C6H6", water as "H2O", …), so a name search can't
|
|
303
|
+
# resolve them via the library. This curated map fills that gap for classroom
|
|
304
|
+
# staples; named organics the library *does* carry (toluene, aspirin, caffeine)
|
|
305
|
+
# resolve via an exact library-name lookup instead — see
|
|
306
|
+
# ``app_history.resolve_query_formulas``. Formulas are Hill notation matching
|
|
307
|
+
# ``Molecule.get_formula`` exactly (no-carbon formulas are fully alphabetical,
|
|
308
|
+
# so ammonia is "H3N", sulfur dioxide "O2S", hydrogen fluoride "FH").
|
|
309
|
+
COMMON_NAME_TO_FORMULA: Dict[str, str] = {
|
|
310
|
+
"hydrogen": "H2",
|
|
311
|
+
"oxygen": "O2",
|
|
312
|
+
"nitrogen": "N2",
|
|
313
|
+
"chlorine": "Cl2",
|
|
314
|
+
"fluorine": "F2",
|
|
315
|
+
"water": "H2O",
|
|
316
|
+
"ammonia": "H3N",
|
|
317
|
+
"methane": "CH4",
|
|
318
|
+
"carbon dioxide": "CO2",
|
|
319
|
+
"carbon monoxide": "CO",
|
|
320
|
+
"hydrogen fluoride": "FH",
|
|
321
|
+
"hydrogen chloride": "ClH",
|
|
322
|
+
"hydrogen peroxide": "H2O2",
|
|
323
|
+
"hydrogen cyanide": "CHN",
|
|
324
|
+
"ozone": "O3",
|
|
325
|
+
"nitric oxide": "NO",
|
|
326
|
+
"nitrogen dioxide": "NO2",
|
|
327
|
+
"sulfur dioxide": "O2S",
|
|
328
|
+
"hydrogen sulfide": "H2S",
|
|
329
|
+
"benzene": "C6H6",
|
|
330
|
+
"ethane": "C2H6",
|
|
331
|
+
"ethylene": "C2H4",
|
|
332
|
+
"ethene": "C2H4",
|
|
333
|
+
"acetylene": "C2H2",
|
|
334
|
+
"ethyne": "C2H2",
|
|
335
|
+
"propane": "C3H8",
|
|
336
|
+
"butane": "C4H10",
|
|
337
|
+
"methanol": "CH4O",
|
|
338
|
+
"ethanol": "C2H6O",
|
|
339
|
+
"formaldehyde": "CH2O",
|
|
340
|
+
"acetaldehyde": "C2H4O",
|
|
341
|
+
"formic acid": "CH2O2",
|
|
342
|
+
"acetic acid": "C2H4O2",
|
|
343
|
+
"acetone": "C3H6O",
|
|
344
|
+
"propene": "C3H6",
|
|
345
|
+
"propyne": "C3H4",
|
|
346
|
+
"glycine": "C2H5NO2",
|
|
347
|
+
"urea": "CH4N2O",
|
|
348
|
+
"phosphine": "H3P",
|
|
349
|
+
"silane": "H4Si",
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Molecule presets — bundled library.
|
|
353
|
+
# The former inline literal now lives in the indexed package-data store
|
|
354
|
+
# (quantui/data/library/library.sqlite, seeded from
|
|
355
|
+
# quantui/data/manifests/presets.json). ``config.MOLECULE_LIBRARY`` is a lazy
|
|
356
|
+
# back-compat shim resolved via module ``__getattr__`` (PEP 562): on first
|
|
357
|
+
# access it loads the curated/preset entries from the store in the original
|
|
358
|
+
# {formula: {atoms, coordinates, charge, multiplicity, description}} shape, so
|
|
359
|
+
# every existing consumer keeps working unchanged.
|
|
360
|
+
MOLECULE_LIBRARY: Dict[str, Dict[str, Any]] # populated lazily by __getattr__
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def __getattr__(name: str) -> Any:
|
|
364
|
+
if name == "MOLECULE_LIBRARY":
|
|
365
|
+
from quantui.molecule_library import get_preset_dict
|
|
366
|
+
|
|
367
|
+
return get_preset_dict()
|
|
368
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Atomic numbers for the full periodic table (Z=1..118). Single source of
|
|
372
|
+
# truth for element validity + electron counting — previously QuantUI only
|
|
373
|
+
# recognized elements up to Kr (Z=36), which rejected valid structures
|
|
374
|
+
# resolved via PubChem/CACTUS/SMILES for any heavier element (iodine in
|
|
375
|
+
# thyroxine, tin/antimony in organometallics, gold/platinum complexes,
|
|
376
|
+
# etc.), even though the "Invalid atom symbol" error text in molecule.py
|
|
377
|
+
# explicitly listed iodine as a supported example.
|
|
378
|
+
ATOMIC_NUMBERS: Dict[str, int] = {
|
|
379
|
+
"H": 1,
|
|
380
|
+
"He": 2,
|
|
381
|
+
"Li": 3,
|
|
382
|
+
"Be": 4,
|
|
383
|
+
"B": 5,
|
|
384
|
+
"C": 6,
|
|
385
|
+
"N": 7,
|
|
386
|
+
"O": 8,
|
|
387
|
+
"F": 9,
|
|
388
|
+
"Ne": 10,
|
|
389
|
+
"Na": 11,
|
|
390
|
+
"Mg": 12,
|
|
391
|
+
"Al": 13,
|
|
392
|
+
"Si": 14,
|
|
393
|
+
"P": 15,
|
|
394
|
+
"S": 16,
|
|
395
|
+
"Cl": 17,
|
|
396
|
+
"Ar": 18,
|
|
397
|
+
"K": 19,
|
|
398
|
+
"Ca": 20,
|
|
399
|
+
"Sc": 21,
|
|
400
|
+
"Ti": 22,
|
|
401
|
+
"V": 23,
|
|
402
|
+
"Cr": 24,
|
|
403
|
+
"Mn": 25,
|
|
404
|
+
"Fe": 26,
|
|
405
|
+
"Co": 27,
|
|
406
|
+
"Ni": 28,
|
|
407
|
+
"Cu": 29,
|
|
408
|
+
"Zn": 30,
|
|
409
|
+
"Ga": 31,
|
|
410
|
+
"Ge": 32,
|
|
411
|
+
"As": 33,
|
|
412
|
+
"Se": 34,
|
|
413
|
+
"Br": 35,
|
|
414
|
+
"Kr": 36,
|
|
415
|
+
"Rb": 37,
|
|
416
|
+
"Sr": 38,
|
|
417
|
+
"Y": 39,
|
|
418
|
+
"Zr": 40,
|
|
419
|
+
"Nb": 41,
|
|
420
|
+
"Mo": 42,
|
|
421
|
+
"Tc": 43,
|
|
422
|
+
"Ru": 44,
|
|
423
|
+
"Rh": 45,
|
|
424
|
+
"Pd": 46,
|
|
425
|
+
"Ag": 47,
|
|
426
|
+
"Cd": 48,
|
|
427
|
+
"In": 49,
|
|
428
|
+
"Sn": 50,
|
|
429
|
+
"Sb": 51,
|
|
430
|
+
"Te": 52,
|
|
431
|
+
"I": 53,
|
|
432
|
+
"Xe": 54,
|
|
433
|
+
"Cs": 55,
|
|
434
|
+
"Ba": 56,
|
|
435
|
+
"La": 57,
|
|
436
|
+
"Ce": 58,
|
|
437
|
+
"Pr": 59,
|
|
438
|
+
"Nd": 60,
|
|
439
|
+
"Pm": 61,
|
|
440
|
+
"Sm": 62,
|
|
441
|
+
"Eu": 63,
|
|
442
|
+
"Gd": 64,
|
|
443
|
+
"Tb": 65,
|
|
444
|
+
"Dy": 66,
|
|
445
|
+
"Ho": 67,
|
|
446
|
+
"Er": 68,
|
|
447
|
+
"Tm": 69,
|
|
448
|
+
"Yb": 70,
|
|
449
|
+
"Lu": 71,
|
|
450
|
+
"Hf": 72,
|
|
451
|
+
"Ta": 73,
|
|
452
|
+
"W": 74,
|
|
453
|
+
"Re": 75,
|
|
454
|
+
"Os": 76,
|
|
455
|
+
"Ir": 77,
|
|
456
|
+
"Pt": 78,
|
|
457
|
+
"Au": 79,
|
|
458
|
+
"Hg": 80,
|
|
459
|
+
"Tl": 81,
|
|
460
|
+
"Pb": 82,
|
|
461
|
+
"Bi": 83,
|
|
462
|
+
"Po": 84,
|
|
463
|
+
"At": 85,
|
|
464
|
+
"Rn": 86,
|
|
465
|
+
"Fr": 87,
|
|
466
|
+
"Ra": 88,
|
|
467
|
+
"Ac": 89,
|
|
468
|
+
"Th": 90,
|
|
469
|
+
"Pa": 91,
|
|
470
|
+
"U": 92,
|
|
471
|
+
"Np": 93,
|
|
472
|
+
"Pu": 94,
|
|
473
|
+
"Am": 95,
|
|
474
|
+
"Cm": 96,
|
|
475
|
+
"Bk": 97,
|
|
476
|
+
"Cf": 98,
|
|
477
|
+
"Es": 99,
|
|
478
|
+
"Fm": 100,
|
|
479
|
+
"Md": 101,
|
|
480
|
+
"No": 102,
|
|
481
|
+
"Lr": 103,
|
|
482
|
+
"Rf": 104,
|
|
483
|
+
"Db": 105,
|
|
484
|
+
"Sg": 106,
|
|
485
|
+
"Bh": 107,
|
|
486
|
+
"Hs": 108,
|
|
487
|
+
"Mt": 109,
|
|
488
|
+
"Ds": 110,
|
|
489
|
+
"Rg": 111,
|
|
490
|
+
"Cn": 112,
|
|
491
|
+
"Nh": 113,
|
|
492
|
+
"Fl": 114,
|
|
493
|
+
"Mc": 115,
|
|
494
|
+
"Lv": 116,
|
|
495
|
+
"Ts": 117,
|
|
496
|
+
"Og": 118,
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Valid atomic symbols — every element in ATOMIC_NUMBERS. Derived rather
|
|
500
|
+
# than hand-maintained separately so the two can never drift apart again.
|
|
501
|
+
VALID_ATOMS = list(ATOMIC_NUMBERS.keys())
|
|
502
|
+
|
|
503
|
+
# Quick-start templates for the notebook UI
|
|
504
|
+
# The `notes` and `learning_goals` fields are shown to students.
|
|
505
|
+
# The `calc_settings` fields pre-fill the method/basis dropdowns.
|
|
506
|
+
QUICK_START_TEMPLATES: Dict[str, Dict[str, Any]] = {
|
|
507
|
+
"beginner_water": {
|
|
508
|
+
"name": "Beginner: Water Molecule",
|
|
509
|
+
"description": "Perfect first calculation — fast and reliable",
|
|
510
|
+
"molecule": {
|
|
511
|
+
"xyz": "O 0.0 0.0 0.0\nH 0.757 0.587 0.0\nH -0.757 0.587 0.0",
|
|
512
|
+
"charge": 0,
|
|
513
|
+
"multiplicity": 1,
|
|
514
|
+
},
|
|
515
|
+
"calc_settings": {"method": "RHF", "basis": "6-31G"},
|
|
516
|
+
"notes": "Water (H2O) with 6-31G basis. Expected energy: ~-76.03 Ha.",
|
|
517
|
+
"learning_goals": [
|
|
518
|
+
"Run your first local calculation",
|
|
519
|
+
"Understand basic molecule structure",
|
|
520
|
+
"See SCF convergence in action",
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
"basis_comparison": {
|
|
524
|
+
"name": "Learn: Basis Set Effect (CO2)",
|
|
525
|
+
"description": "Compare different basis sets on carbon dioxide",
|
|
526
|
+
"molecule": {
|
|
527
|
+
"xyz": "C 0.0 0.0 0.0\nO 1.16 0.0 0.0\nO -1.16 0.0 0.0",
|
|
528
|
+
"charge": 0,
|
|
529
|
+
"multiplicity": 1,
|
|
530
|
+
},
|
|
531
|
+
"calc_settings": {"method": "RHF", "basis": "STO-3G"},
|
|
532
|
+
"notes": "Try with STO-3G first, then re-run with 6-31G to see the difference!",
|
|
533
|
+
"learning_goals": [
|
|
534
|
+
"Understand basis set accuracy trade-offs",
|
|
535
|
+
"Compare computational costs",
|
|
536
|
+
"See energy convergence with basis size",
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
"radical_oxygen": {
|
|
540
|
+
"name": "Advanced: Oxygen Radical (O2)",
|
|
541
|
+
"description": "Learn about multiplicity with triplet oxygen",
|
|
542
|
+
"molecule": {
|
|
543
|
+
"xyz": "O 0.0 0.0 0.0\nO 1.21 0.0 0.0",
|
|
544
|
+
"charge": 0,
|
|
545
|
+
"multiplicity": 3,
|
|
546
|
+
},
|
|
547
|
+
"calc_settings": {"method": "UHF", "basis": "6-31G"},
|
|
548
|
+
"notes": "O2 has two unpaired electrons (triplet state). Try mult=1 first to see it fail!",
|
|
549
|
+
"learning_goals": [
|
|
550
|
+
"Understand multiplicity for radicals",
|
|
551
|
+
"Learn UHF vs RHF",
|
|
552
|
+
"See why O2 is paramagnetic",
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
"benzene": {
|
|
556
|
+
"name": "Intermediate: Benzene Ring",
|
|
557
|
+
"description": "Medium-sized aromatic molecule",
|
|
558
|
+
"molecule": {
|
|
559
|
+
"xyz": (
|
|
560
|
+
"C 0.000 1.396 0.000\nC 1.209 0.698 0.000\nC 1.209 -0.698 0.000\n"
|
|
561
|
+
"C 0.000 -1.396 0.000\nC -1.209 -0.698 0.000\nC -1.209 0.698 0.000\n"
|
|
562
|
+
"H 0.000 2.479 0.000\nH 2.147 1.240 0.000\nH 2.147 -1.240 0.000\n"
|
|
563
|
+
"H 0.000 -2.479 0.000\nH -2.147 -1.240 0.000\nH -2.147 1.240 0.000"
|
|
564
|
+
),
|
|
565
|
+
"charge": 0,
|
|
566
|
+
"multiplicity": 1,
|
|
567
|
+
},
|
|
568
|
+
"calc_settings": {"method": "RHF", "basis": "6-31G"},
|
|
569
|
+
"notes": "Benzene (C6H6) — classic aromatic molecule. Larger system, takes longer locally.",
|
|
570
|
+
"learning_goals": [
|
|
571
|
+
"Work with larger molecules",
|
|
572
|
+
"Understand aromatic systems",
|
|
573
|
+
"See how calculation time scales with molecule size",
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# Logging configuration
|
|
579
|
+
LOG_LEVEL = "INFO"
|
|
580
|
+
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
581
|
+
|
|
582
|
+
# PySCF calculation script template
|
|
583
|
+
# Used by calculator.py to generate a standalone .py file students can
|
|
584
|
+
# download and run independently ("Export Script" feature).
|
|
585
|
+
PYSCF_SCRIPT_TEMPLATE = """#!/usr/bin/env python3
|
|
586
|
+
\"\"\"
|
|
587
|
+
PySCF Calculation Script
|
|
588
|
+
Generated by QuantUI
|
|
589
|
+
|
|
590
|
+
Calculation: {job_name}
|
|
591
|
+
Method: {method}
|
|
592
|
+
Basis: {basis}
|
|
593
|
+
\"\"\"
|
|
594
|
+
|
|
595
|
+
import sys
|
|
596
|
+
from pathlib import Path
|
|
597
|
+
from pyscf import gto, scf, dft
|
|
598
|
+
import numpy as np
|
|
599
|
+
|
|
600
|
+
def main():
|
|
601
|
+
# Define molecule
|
|
602
|
+
mol = gto.Mole()
|
|
603
|
+
mol.atom = '''
|
|
604
|
+
{geometry}
|
|
605
|
+
'''
|
|
606
|
+
mol.basis = '{basis}'
|
|
607
|
+
mol.charge = {charge}
|
|
608
|
+
mol.spin = {spin}
|
|
609
|
+
mol.verbose = 4 # Detailed output
|
|
610
|
+
mol.build()
|
|
611
|
+
|
|
612
|
+
print("=" * 60)
|
|
613
|
+
print("Molecule Information")
|
|
614
|
+
print("=" * 60)
|
|
615
|
+
print(f"Number of electrons: {{mol.nelectron}}")
|
|
616
|
+
print(f"Nuclear repulsion energy: {{mol.energy_nuc():.8f}} Ha")
|
|
617
|
+
print()
|
|
618
|
+
|
|
619
|
+
# Run calculation
|
|
620
|
+
print("=" * 60)
|
|
621
|
+
print("Starting {method}/{basis} Calculation")
|
|
622
|
+
print("=" * 60)
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
method = '{method}'
|
|
626
|
+
# Display name → PySCF xc string + external D3 dispersion. Matches
|
|
627
|
+
# quantui/session_calc.py resolve_xc + maybe_apply_d3. Important
|
|
628
|
+
# for methods that PySCF doesn't accept directly (notably
|
|
629
|
+
# wB97X-D — on dftd3's black-list; PBE-D3 — D3 must be applied
|
|
630
|
+
# externally via pyscf.dftd3).
|
|
631
|
+
_XC_ALIAS = {{
|
|
632
|
+
'M06-L': 'm06l',
|
|
633
|
+
'wB97X-D': 'wb97x',
|
|
634
|
+
'CAM-B3LYP': 'camb3lyp',
|
|
635
|
+
'PBE-D3': 'pbe',
|
|
636
|
+
}}
|
|
637
|
+
_NEEDS_D3 = {{'PBE-D3', 'wB97X-D'}}
|
|
638
|
+
|
|
639
|
+
if method == 'RHF':
|
|
640
|
+
mf = scf.RHF(mol)
|
|
641
|
+
elif method == 'UHF':
|
|
642
|
+
mf = scf.UHF(mol)
|
|
643
|
+
else:
|
|
644
|
+
# DFT: auto-select RKS/UKS based on spin
|
|
645
|
+
mf = dft.RKS(mol) if mol.spin == 0 else dft.UKS(mol)
|
|
646
|
+
mf.xc = _XC_ALIAS.get(method, method)
|
|
647
|
+
if method in _NEEDS_D3:
|
|
648
|
+
try:
|
|
649
|
+
from pyscf import dftd3 as _dftd3
|
|
650
|
+
mf = _dftd3.dftd3(mf)
|
|
651
|
+
except ImportError:
|
|
652
|
+
print(
|
|
653
|
+
"WARNING: pyscf.dftd3 not available; "
|
|
654
|
+
"running {{method}} without D3 dispersion."
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
energy = mf.kernel()
|
|
658
|
+
|
|
659
|
+
if mf.converged:
|
|
660
|
+
print()
|
|
661
|
+
print("=" * 60)
|
|
662
|
+
print("Calculation Results")
|
|
663
|
+
print("=" * 60)
|
|
664
|
+
print(f"SCF converged: Yes")
|
|
665
|
+
print(f"Total energy: {{energy:.8f}} Ha")
|
|
666
|
+
mo_e = mf.mo_energy if not isinstance(mf.mo_energy, list) else mf.mo_energy[0]
|
|
667
|
+
mo_o = mf.mo_occ if not isinstance(mf.mo_occ, list) else mf.mo_occ[0]
|
|
668
|
+
n_occ = int((mo_o > 0).sum())
|
|
669
|
+
if 0 < n_occ < len(mo_e):
|
|
670
|
+
gap = (mo_e[n_occ] - mo_e[n_occ - 1]) * 27.211386
|
|
671
|
+
print(f"HOMO-LUMO gap: {{gap:.4f}} eV")
|
|
672
|
+
print()
|
|
673
|
+
|
|
674
|
+
# Save results next to the script so the path is predictable
|
|
675
|
+
results_path = str(Path(__file__).parent / 'results.npz')
|
|
676
|
+
np.savez(results_path,
|
|
677
|
+
energy=energy,
|
|
678
|
+
mo_energy=np.array(mf.mo_energy),
|
|
679
|
+
mo_coeff=np.array(mf.mo_coeff),
|
|
680
|
+
converged=mf.converged)
|
|
681
|
+
print(f"Results saved to {{results_path}}")
|
|
682
|
+
print("=" * 60)
|
|
683
|
+
|
|
684
|
+
sys.exit(0)
|
|
685
|
+
else:
|
|
686
|
+
print("ERROR: SCF did not converge!")
|
|
687
|
+
sys.exit(1)
|
|
688
|
+
|
|
689
|
+
except Exception as e:
|
|
690
|
+
print(f"ERROR: Calculation failed with exception: {{e}}")
|
|
691
|
+
import traceback
|
|
692
|
+
traceback.print_exc()
|
|
693
|
+
sys.exit(1)
|
|
694
|
+
|
|
695
|
+
if __name__ == "__main__":
|
|
696
|
+
main()
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
# ============================================================================
|
|
700
|
+
# Local Configuration Override (optional)
|
|
701
|
+
# ============================================================================
|
|
702
|
+
# Create config.local.py in the project root to override settings without
|
|
703
|
+
# modifying this tracked file. config.local.py is git-ignored.
|
|
704
|
+
#
|
|
705
|
+
# Supported overrides: PUBCHEM_API_KEY, DEBUG
|
|
706
|
+
#
|
|
707
|
+
# Example config.local.py:
|
|
708
|
+
# PUBCHEM_API_KEY = "your-key-here"
|
|
709
|
+
# DEBUG = True
|
|
710
|
+
|
|
711
|
+
import importlib.util # noqa: E402
|
|
712
|
+
|
|
713
|
+
_local_config_path = PROJECT_ROOT / "config.local.py"
|
|
714
|
+
|
|
715
|
+
if _local_config_path.exists():
|
|
716
|
+
spec = importlib.util.spec_from_file_location("config_local", _local_config_path)
|
|
717
|
+
if spec and spec.loader:
|
|
718
|
+
config_local = importlib.util.module_from_spec(spec)
|
|
719
|
+
spec.loader.exec_module(config_local)
|
|
720
|
+
|
|
721
|
+
for attr in ("PUBCHEM_API_KEY", "DEBUG"):
|
|
722
|
+
if hasattr(config_local, attr):
|
|
723
|
+
value = getattr(config_local, attr)
|
|
724
|
+
if value is not None:
|
|
725
|
+
globals()[attr] = value
|