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/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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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