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/comparison.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Side-by-side comparison of multiple quantum chemistry calculations.
|
|
3
|
+
|
|
4
|
+
Produces:
|
|
5
|
+
|
|
6
|
+
* An HTML summary table comparing method, basis, energy, gap, and
|
|
7
|
+
convergence across multiple jobs.
|
|
8
|
+
* A grouped bar chart (matplotlib) for energy and HOMO–LUMO gap
|
|
9
|
+
comparisons.
|
|
10
|
+
|
|
11
|
+
All inputs are plain data — either :class:`~quantui.storage.JobMetadata`
|
|
12
|
+
objects enriched with parsed results, or lightweight :class:`CalcSummary`
|
|
13
|
+
dicts built from ``results.npz`` files.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import List, Optional, Sequence, Tuple
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
HARTREE_TO_EV: float = 27.211386245988
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Lightweight comparison record
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CalcSummary:
|
|
37
|
+
"""
|
|
38
|
+
One row in a comparison table.
|
|
39
|
+
|
|
40
|
+
Can be built from a :class:`JobMetadata` + ``results.npz``, from a
|
|
41
|
+
:class:`SessionResult`, or by hand.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
label: str
|
|
45
|
+
formula: str
|
|
46
|
+
method: str
|
|
47
|
+
basis: str
|
|
48
|
+
energy_hartree: Optional[float] = None
|
|
49
|
+
homo_lumo_gap_ev: Optional[float] = None
|
|
50
|
+
converged: Optional[bool] = None
|
|
51
|
+
n_iterations: Optional[int] = None
|
|
52
|
+
status: str = ""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def energy_ev(self) -> Optional[float]:
|
|
56
|
+
if self.energy_hartree is not None:
|
|
57
|
+
return self.energy_hartree * HARTREE_TO_EV
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# Builders — turn different data sources into CalcSummary
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def summary_from_job_metadata(metadata, *, label: str = "") -> CalcSummary:
|
|
67
|
+
"""
|
|
68
|
+
Create a :class:`CalcSummary` from a :class:`JobMetadata`.
|
|
69
|
+
|
|
70
|
+
If the job's ``results.npz`` file exists, the energy and convergence
|
|
71
|
+
data are loaded. Otherwise only metadata is included.
|
|
72
|
+
"""
|
|
73
|
+
formula = (
|
|
74
|
+
metadata.job_name.split("_")[0]
|
|
75
|
+
if "_" in metadata.job_name
|
|
76
|
+
else metadata.job_name
|
|
77
|
+
)
|
|
78
|
+
calc = CalcSummary(
|
|
79
|
+
label=label or metadata.job_name,
|
|
80
|
+
formula=formula,
|
|
81
|
+
method=metadata.method,
|
|
82
|
+
basis=metadata.basis,
|
|
83
|
+
status=metadata.status,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
results_path = Path(metadata.paths.get("results", ""))
|
|
87
|
+
if results_path.exists():
|
|
88
|
+
try:
|
|
89
|
+
data = np.load(results_path, allow_pickle=False)
|
|
90
|
+
calc.energy_hartree = float(data["energy"])
|
|
91
|
+
calc.converged = bool(data["converged"])
|
|
92
|
+
if "mo_energy" in data:
|
|
93
|
+
mo_e = data["mo_energy"]
|
|
94
|
+
if mo_e.ndim == 2:
|
|
95
|
+
mo_e = mo_e[0]
|
|
96
|
+
mo_occ = data.get("mo_occ")
|
|
97
|
+
if mo_occ is not None:
|
|
98
|
+
if mo_occ.ndim == 2:
|
|
99
|
+
mo_occ = mo_occ[0]
|
|
100
|
+
n_occ = int((mo_occ > 0).sum())
|
|
101
|
+
else:
|
|
102
|
+
n_occ = int((mo_e < 0).sum())
|
|
103
|
+
if 0 < n_occ < len(mo_e):
|
|
104
|
+
calc.homo_lumo_gap_ev = float(
|
|
105
|
+
(mo_e[n_occ] - mo_e[n_occ - 1]) * HARTREE_TO_EV
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logger.warning("Could not load results for %s: %s", calc.label, exc)
|
|
109
|
+
|
|
110
|
+
return calc
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def summary_from_session_result(result, *, label: str = "") -> CalcSummary:
|
|
114
|
+
"""
|
|
115
|
+
Create a :class:`CalcSummary` from a :class:`SessionResult`.
|
|
116
|
+
"""
|
|
117
|
+
return CalcSummary(
|
|
118
|
+
label=label or f"{result.formula} {result.method}/{result.basis}",
|
|
119
|
+
formula=result.formula,
|
|
120
|
+
method=result.method,
|
|
121
|
+
basis=result.basis,
|
|
122
|
+
energy_hartree=result.energy_hartree,
|
|
123
|
+
homo_lumo_gap_ev=result.homo_lumo_gap_ev,
|
|
124
|
+
converged=result.converged,
|
|
125
|
+
n_iterations=result.n_iterations,
|
|
126
|
+
status="COMPLETED",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def summary_from_saved_result(data: dict, *, label: str = "") -> CalcSummary:
|
|
131
|
+
"""
|
|
132
|
+
Create a :class:`CalcSummary` from a saved ``result.json`` dict.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
data:
|
|
137
|
+
Dict loaded from ``result.json`` via
|
|
138
|
+
:func:`~quantui.results_storage.load_result`.
|
|
139
|
+
label:
|
|
140
|
+
Optional display label. Defaults to
|
|
141
|
+
``"<formula> <method>/<basis> (<timestamp>)"``.
|
|
142
|
+
"""
|
|
143
|
+
formula = data.get("formula", "?")
|
|
144
|
+
method = data.get("method", "?")
|
|
145
|
+
basis = data.get("basis", "?")
|
|
146
|
+
ts = data.get("timestamp", "")
|
|
147
|
+
auto_label = f"{formula} {method}/{basis}" + (f" ({ts})" if ts else "")
|
|
148
|
+
return CalcSummary(
|
|
149
|
+
label=label or auto_label,
|
|
150
|
+
formula=formula,
|
|
151
|
+
method=method,
|
|
152
|
+
basis=basis,
|
|
153
|
+
energy_hartree=data.get("energy_hartree"),
|
|
154
|
+
homo_lumo_gap_ev=data.get("homo_lumo_gap_ev"),
|
|
155
|
+
converged=data.get("converged"),
|
|
156
|
+
n_iterations=data.get("n_iterations"),
|
|
157
|
+
status="COMPLETED",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ============================================================================
|
|
162
|
+
# HTML comparison table
|
|
163
|
+
# ============================================================================
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def comparison_table_html(summaries: Sequence[CalcSummary]) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Return an HTML ``<table>`` comparing calculation summaries.
|
|
169
|
+
|
|
170
|
+
Suitable for ``IPython.display.HTML``.
|
|
171
|
+
"""
|
|
172
|
+
if not summaries:
|
|
173
|
+
return "<p><em>No calculations to compare.</em></p>"
|
|
174
|
+
|
|
175
|
+
header = (
|
|
176
|
+
"<tr>"
|
|
177
|
+
"<th style='text-align:left; padding:6px 12px;'>Label</th>"
|
|
178
|
+
"<th style='text-align:left; padding:6px 12px;'>Formula</th>"
|
|
179
|
+
"<th style='text-align:left; padding:6px 12px;'>Method / Basis</th>"
|
|
180
|
+
"<th style='text-align:right; padding:6px 12px;'>Energy (Ha)</th>"
|
|
181
|
+
"<th style='text-align:right; padding:6px 12px;'>HOMO–LUMO gap (eV)</th>"
|
|
182
|
+
"<th style='text-align:center; padding:6px 12px;'>Converged</th>"
|
|
183
|
+
"<th style='text-align:center; padding:6px 12px;'>Iterations</th>"
|
|
184
|
+
"</tr>"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
rows: List[str] = []
|
|
188
|
+
for s in summaries:
|
|
189
|
+
e_str = f"{s.energy_hartree:.8f}" if s.energy_hartree is not None else "—"
|
|
190
|
+
g_str = f"{s.homo_lumo_gap_ev:.4f}" if s.homo_lumo_gap_ev is not None else "—"
|
|
191
|
+
c_str = "✅" if s.converged else ("❌" if s.converged is not None else "—")
|
|
192
|
+
i_str = str(s.n_iterations) if s.n_iterations is not None else "—"
|
|
193
|
+
rows.append(
|
|
194
|
+
f"<tr>"
|
|
195
|
+
f"<td style='padding:4px 12px;'>{_esc(s.label)}</td>"
|
|
196
|
+
f"<td style='padding:4px 12px;'>{_esc(s.formula)}</td>"
|
|
197
|
+
f"<td style='padding:4px 12px;'>{_esc(s.method)} / {_esc(s.basis)}</td>"
|
|
198
|
+
f"<td style='padding:4px 12px; text-align:right; font-family:monospace;'>{e_str}</td>"
|
|
199
|
+
f"<td style='padding:4px 12px; text-align:right; font-family:monospace;'>{g_str}</td>"
|
|
200
|
+
f"<td style='padding:4px 12px; text-align:center;'>{c_str}</td>"
|
|
201
|
+
f"<td style='padding:4px 12px; text-align:center;'>{i_str}</td>"
|
|
202
|
+
f"</tr>"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
'<div style="overflow-x:auto;">'
|
|
207
|
+
'<table style="border-collapse:collapse; width:100%; font-size:14px;">'
|
|
208
|
+
f"<thead style='background:#f0f4f8;'>{header}</thead>"
|
|
209
|
+
f"<tbody>{''.join(rows)}</tbody>"
|
|
210
|
+
"</table></div>"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _esc(text: str) -> str:
|
|
215
|
+
"""Minimal HTML escaping."""
|
|
216
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ============================================================================
|
|
220
|
+
# Matplotlib comparison chart
|
|
221
|
+
# ============================================================================
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def plot_comparison(
|
|
225
|
+
summaries: Sequence[CalcSummary],
|
|
226
|
+
*,
|
|
227
|
+
figsize: Tuple[float, float] = (10, 5),
|
|
228
|
+
title: Optional[str] = None,
|
|
229
|
+
):
|
|
230
|
+
"""
|
|
231
|
+
Return a matplotlib Figure with grouped bars for energy and HOMO–LUMO gap.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
summaries : sequence of CalcSummary
|
|
236
|
+
Two or more calculations to compare.
|
|
237
|
+
figsize : tuple
|
|
238
|
+
Figure size.
|
|
239
|
+
title : str, optional
|
|
240
|
+
Overall title.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
matplotlib.figure.Figure
|
|
245
|
+
"""
|
|
246
|
+
import matplotlib.pyplot as plt
|
|
247
|
+
|
|
248
|
+
labels = [s.label for s in summaries]
|
|
249
|
+
energies = [s.energy_hartree for s in summaries]
|
|
250
|
+
gaps = [s.homo_lumo_gap_ev for s in summaries]
|
|
251
|
+
|
|
252
|
+
has_energies = any(e is not None for e in energies)
|
|
253
|
+
has_gaps = any(g is not None for g in gaps)
|
|
254
|
+
|
|
255
|
+
n_panels = int(has_energies) + int(has_gaps)
|
|
256
|
+
if n_panels == 0:
|
|
257
|
+
fig, ax = plt.subplots(figsize=(6, 2))
|
|
258
|
+
ax.text(
|
|
259
|
+
0.5,
|
|
260
|
+
0.5,
|
|
261
|
+
"No energy data available for comparison.",
|
|
262
|
+
ha="center",
|
|
263
|
+
va="center",
|
|
264
|
+
fontsize=12,
|
|
265
|
+
transform=ax.transAxes,
|
|
266
|
+
)
|
|
267
|
+
ax.axis("off")
|
|
268
|
+
return fig
|
|
269
|
+
|
|
270
|
+
fig, axes = plt.subplots(1, n_panels, figsize=figsize)
|
|
271
|
+
if n_panels == 1:
|
|
272
|
+
axes = [axes]
|
|
273
|
+
|
|
274
|
+
idx = 0
|
|
275
|
+
x = np.arange(len(labels))
|
|
276
|
+
|
|
277
|
+
if has_energies:
|
|
278
|
+
ax = axes[idx]
|
|
279
|
+
vals = [e if e is not None else 0.0 for e in energies]
|
|
280
|
+
colours = ["#2171b5" if e is not None else "#bdbdbd" for e in energies]
|
|
281
|
+
ax.bar(x, vals, color=colours, edgecolor="white")
|
|
282
|
+
ax.set_xticks(x)
|
|
283
|
+
ax.set_xticklabels(labels, rotation=30, ha="right", fontsize=9)
|
|
284
|
+
ax.set_ylabel("Total Energy (Ha)")
|
|
285
|
+
ax.set_title("Energy Comparison")
|
|
286
|
+
for xi, v, e in zip(x, vals, energies):
|
|
287
|
+
if e is not None:
|
|
288
|
+
ax.text(xi, v, f"{v:.4f}", ha="center", va="bottom", fontsize=8)
|
|
289
|
+
idx += 1
|
|
290
|
+
|
|
291
|
+
if has_gaps:
|
|
292
|
+
ax = axes[idx]
|
|
293
|
+
vals = [g if g is not None else 0.0 for g in gaps]
|
|
294
|
+
colours = ["#e6550d" if g is not None else "#bdbdbd" for g in gaps]
|
|
295
|
+
ax.bar(x, vals, color=colours, edgecolor="white")
|
|
296
|
+
ax.set_xticks(x)
|
|
297
|
+
ax.set_xticklabels(labels, rotation=30, ha="right", fontsize=9)
|
|
298
|
+
ax.set_ylabel("HOMO–LUMO Gap (eV)")
|
|
299
|
+
ax.set_title("HOMO–LUMO Gap Comparison")
|
|
300
|
+
for xi, v, g in zip(x, vals, gaps):
|
|
301
|
+
if g is not None:
|
|
302
|
+
ax.text(xi, v, f"{v:.2f}", ha="center", va="bottom", fontsize=8)
|
|
303
|
+
|
|
304
|
+
fig.suptitle(title or "Calculation Comparison", fontsize=14, fontweight="bold")
|
|
305
|
+
fig.tight_layout()
|
|
306
|
+
return fig
|