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.
Files changed (62) hide show
  1. quantui/__init__.py +311 -0
  2. quantui/analytics.py +609 -0
  3. quantui/app.py +5650 -0
  4. quantui/app_analysis.py +662 -0
  5. quantui/app_builders.py +2465 -0
  6. quantui/app_exports.py +194 -0
  7. quantui/app_formatters.py +493 -0
  8. quantui/app_history.py +624 -0
  9. quantui/app_runflow.py +1544 -0
  10. quantui/app_visualization.py +2620 -0
  11. quantui/ase_bridge.py +236 -0
  12. quantui/benchmarks.py +1543 -0
  13. quantui/c_stderr.py +124 -0
  14. quantui/cactus.py +88 -0
  15. quantui/calc_log.py +1116 -0
  16. quantui/calculator.py +204 -0
  17. quantui/cancellation.py +88 -0
  18. quantui/cli.py +288 -0
  19. quantui/comparison.py +306 -0
  20. quantui/config.py +725 -0
  21. quantui/data/js/3Dmol-min.js +2 -0
  22. quantui/data/js/3Dmol-min.js.LICENSE.txt +5 -0
  23. quantui/data/library/library.sqlite +0 -0
  24. quantui/data/manifests/bulk_qm9.json +1 -0
  25. quantui/data/manifests/curated.json +15482 -0
  26. quantui/data/manifests/presets.json +816 -0
  27. quantui/descriptor_cards.py +186 -0
  28. quantui/freq_calc.py +712 -0
  29. quantui/freq_ir_workers.py +229 -0
  30. quantui/gpu_offload.py +278 -0
  31. quantui/help_content.py +474 -0
  32. quantui/ir_plot.py +130 -0
  33. quantui/issue_tracker.py +170 -0
  34. quantui/live_log.py +387 -0
  35. quantui/log_utils.py +492 -0
  36. quantui/molecule.py +577 -0
  37. quantui/molecule_library.py +433 -0
  38. quantui/nmr_calc.py +437 -0
  39. quantui/optimizer.py +670 -0
  40. quantui/orbital_visualization.py +1102 -0
  41. quantui/pes_scan.py +420 -0
  42. quantui/preopt.py +355 -0
  43. quantui/progress.py +111 -0
  44. quantui/pubchem.py +1157 -0
  45. quantui/reorganization_energy.py +435 -0
  46. quantui/results_storage.py +902 -0
  47. quantui/security.py +14 -0
  48. quantui/session_calc.py +622 -0
  49. quantui/structure_providers.py +277 -0
  50. quantui/tddft_calc.py +307 -0
  51. quantui/user_settings.py +238 -0
  52. quantui/utils.py +287 -0
  53. quantui/vib_cache.py +247 -0
  54. quantui/visualization_py3dmol.py +593 -0
  55. quantui/viz_assets.py +101 -0
  56. quantui/viz_backend_router.py +243 -0
  57. quantui-0.5.1.dist-info/METADATA +533 -0
  58. quantui-0.5.1.dist-info/RECORD +62 -0
  59. quantui-0.5.1.dist-info/WHEEL +5 -0
  60. quantui-0.5.1.dist-info/entry_points.txt +2 -0
  61. quantui-0.5.1.dist-info/licenses/LICENSE +21 -0
  62. 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