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/vib_cache.py ADDED
@@ -0,0 +1,247 @@
1
+ """
2
+ Vibrational animation disk cache.
3
+
4
+ Caches rendered vibrational-animation HTML per calculation result directory
5
+ so mode switches on repeat visits and history replay are instant — no
6
+ re-render cost.
7
+
8
+ Layout
9
+ ------
10
+ ::
11
+
12
+ <result_dir>/
13
+ └── vib_frames/
14
+ ├── index.json ← manifest with schema version, render params, modes
15
+ ├── mode_001.html
16
+ ├── mode_002.html
17
+ └── ...
18
+
19
+ `index.json` schema (version 1)::
20
+
21
+ {
22
+ "_schema_version": 1,
23
+ "n_frames": 24,
24
+ "amplitude": 0.4,
25
+ "renderer": "py3dmol",
26
+ "modes": {
27
+ "1": {"cached": true, "file": "mode_001.html", "freq_cm1": 1623.4},
28
+ "2": {"cached": false}
29
+ }
30
+ }
31
+
32
+ Cache key
33
+ ---------
34
+ ``(result_dir, mode_number, n_frames, amplitude, renderer)``. If any render
35
+ parameter changes between save and read (e.g. user customises amplitude
36
+ later), the cache is treated as stale and a miss is returned.
37
+
38
+ Robustness
39
+ ----------
40
+ - Atomic writes (write-to-`.tmp` then `os.replace`) so a crash mid-write
41
+ cannot leave the cache in a corrupted state.
42
+ - Graceful fallback: missing/malformed index → empty dict → cache miss.
43
+ All filesystem errors log a warning and return ``None`` / ``False`` —
44
+ never raise. Caller falls back to a fresh render.
45
+ - Schema-version mismatch is treated as a full cache invalidation (saved
46
+ index discarded on the next write).
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import json
52
+ import logging
53
+ import os
54
+ from pathlib import Path
55
+
56
+ _SCHEMA_VERSION = 1
57
+ _LOG = logging.getLogger(__name__)
58
+
59
+ # Floating-point tolerance when comparing saved vs requested amplitude. Avoids
60
+ # spurious cache misses when 0.4 round-trips through JSON serialization.
61
+ _AMPLITUDE_TOL = 1e-6
62
+
63
+
64
+ def cache_dir(result_dir: Path) -> Path:
65
+ """Return the `vib_frames/` directory inside a result directory."""
66
+ return Path(result_dir) / "vib_frames"
67
+
68
+
69
+ def load_index(result_dir: Path) -> dict:
70
+ """Load the cache index manifest. Returns ``{}`` on missing/malformed."""
71
+ path = cache_dir(result_dir) / "index.json"
72
+ if not path.exists():
73
+ return {}
74
+ try:
75
+ data = json.loads(path.read_text(encoding="utf-8"))
76
+ except (OSError, json.JSONDecodeError) as exc:
77
+ _LOG.warning(
78
+ "Failed to read vib cache index at %s (%s); ignoring",
79
+ path,
80
+ exc,
81
+ )
82
+ return {}
83
+ return data if isinstance(data, dict) else {}
84
+
85
+
86
+ def has_cached(
87
+ result_dir: Path,
88
+ mode_number: int,
89
+ *,
90
+ n_frames: int,
91
+ amplitude: float,
92
+ renderer: str,
93
+ fps: int,
94
+ ) -> bool:
95
+ """Return True if a cached HTML exists matching all render parameters."""
96
+ idx = load_index(result_dir)
97
+ if idx.get("_schema_version") != _SCHEMA_VERSION:
98
+ return False
99
+ if idx.get("n_frames") != n_frames:
100
+ return False
101
+ if not _amplitude_matches(idx.get("amplitude"), amplitude):
102
+ return False
103
+ if idx.get("renderer") != renderer:
104
+ return False
105
+ if idx.get("fps") != fps:
106
+ return False
107
+ modes = idx.get("modes", {})
108
+ entry = modes.get(str(mode_number))
109
+ if not isinstance(entry, dict) or not entry.get("cached"):
110
+ return False
111
+ fname = entry.get("file")
112
+ if not isinstance(fname, str):
113
+ return False
114
+ return (cache_dir(result_dir) / fname).exists()
115
+
116
+
117
+ def get_cached_html(
118
+ result_dir: Path,
119
+ mode_number: int,
120
+ *,
121
+ n_frames: int,
122
+ amplitude: float,
123
+ renderer: str,
124
+ fps: int,
125
+ ) -> str | None:
126
+ """Return the cached HTML string for a mode, or None on any miss."""
127
+ if not has_cached(
128
+ result_dir,
129
+ mode_number,
130
+ n_frames=n_frames,
131
+ amplitude=amplitude,
132
+ renderer=renderer,
133
+ fps=fps,
134
+ ):
135
+ return None
136
+ idx = load_index(result_dir)
137
+ entry = idx["modes"][str(mode_number)]
138
+ html_path = cache_dir(result_dir) / entry["file"]
139
+ try:
140
+ return html_path.read_text(encoding="utf-8")
141
+ except OSError as exc:
142
+ _LOG.warning(
143
+ "Failed to read cached html at %s (%s)",
144
+ html_path,
145
+ exc,
146
+ )
147
+ return None
148
+
149
+
150
+ def save_cached_html(
151
+ result_dir: Path,
152
+ mode_number: int,
153
+ html: str,
154
+ *,
155
+ freq_cm1: float | None,
156
+ n_frames: int,
157
+ amplitude: float,
158
+ renderer: str,
159
+ fps: int,
160
+ ) -> None:
161
+ """Save HTML to the cache and update the manifest. Non-fatal on failure.
162
+
163
+ If the existing manifest has different render parameters (different
164
+ n_frames, amplitude, or renderer), it is discarded and replaced — the
165
+ previous mode HTML files are left on disk but become unreferenced
166
+ (cleaned up next time the user deletes the result dir).
167
+ """
168
+ cdir = cache_dir(result_dir)
169
+ try:
170
+ cdir.mkdir(parents=True, exist_ok=True)
171
+ except OSError as exc:
172
+ _LOG.warning(
173
+ "Failed to create vib cache dir %s (%s); skipping save",
174
+ cdir,
175
+ exc,
176
+ )
177
+ return
178
+
179
+ idx = load_index(result_dir)
180
+ if (
181
+ idx.get("_schema_version") != _SCHEMA_VERSION
182
+ or idx.get("n_frames") != n_frames
183
+ or not _amplitude_matches(idx.get("amplitude"), amplitude)
184
+ or idx.get("renderer") != renderer
185
+ or idx.get("fps") != fps
186
+ ):
187
+ idx = {
188
+ "_schema_version": _SCHEMA_VERSION,
189
+ "n_frames": n_frames,
190
+ "amplitude": amplitude,
191
+ "renderer": renderer,
192
+ "fps": fps,
193
+ "modes": {},
194
+ }
195
+
196
+ fname = f"mode_{mode_number:03d}.html"
197
+ html_path = cdir / fname
198
+ tmp_html = html_path.with_suffix(html_path.suffix + ".tmp")
199
+ try:
200
+ tmp_html.write_text(html, encoding="utf-8")
201
+ os.replace(tmp_html, html_path)
202
+ except OSError as exc:
203
+ _LOG.warning(
204
+ "Failed to save cached html to %s (%s)",
205
+ html_path,
206
+ exc,
207
+ )
208
+ try:
209
+ if tmp_html.exists():
210
+ tmp_html.unlink()
211
+ except OSError:
212
+ pass
213
+ return
214
+
215
+ modes = idx.setdefault("modes", {})
216
+ modes[str(mode_number)] = {
217
+ "cached": True,
218
+ "file": fname,
219
+ "freq_cm1": float(freq_cm1) if freq_cm1 is not None else None,
220
+ }
221
+
222
+ index_path = cdir / "index.json"
223
+ tmp_idx = index_path.with_suffix(index_path.suffix + ".tmp")
224
+ try:
225
+ tmp_idx.write_text(json.dumps(idx, indent=2), encoding="utf-8")
226
+ os.replace(tmp_idx, index_path)
227
+ except OSError as exc:
228
+ _LOG.warning(
229
+ "Failed to save cache index to %s (%s)",
230
+ index_path,
231
+ exc,
232
+ )
233
+ try:
234
+ if tmp_idx.exists():
235
+ tmp_idx.unlink()
236
+ except OSError:
237
+ pass
238
+
239
+
240
+ def _amplitude_matches(saved: object, requested: float) -> bool:
241
+ """Compare amplitudes with tolerance to avoid float-equality issues."""
242
+ if saved is None:
243
+ return False
244
+ try:
245
+ return abs(float(saved) - requested) < _AMPLITUDE_TOL
246
+ except (TypeError, ValueError):
247
+ return False