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/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
|