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
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Result-card HTML formatters used by QuantUIApp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _result_extra_rows(get: Any) -> str:
|
|
10
|
+
"""Build the shared 'extra' result-card rows from an accessor.
|
|
11
|
+
|
|
12
|
+
``get(key, default=None)`` reads a field from either a result object
|
|
13
|
+
(``getattr``) or a saved ``result.json`` dict (``dict.get``). Used by BOTH
|
|
14
|
+
:func:`format_result` (live) and :func:`format_past_result` (history) so the
|
|
15
|
+
two cards can never drift again — a past regression had the compute
|
|
16
|
+
device / dipole / Mulliken rows existed only on the live card. Rows:
|
|
17
|
+
post-HF correlation breakdown (MP2 / CCSD / (T)), solvent, compute device
|
|
18
|
+
(always shown), dipole moment, Mulliken charges.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def _num(label: str, value: str) -> str:
|
|
22
|
+
return (
|
|
23
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">{label}</td>'
|
|
24
|
+
f'<td style="color:#000">{value}</td></tr>'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
rows = ""
|
|
28
|
+
energy = get("energy_hartree", 0.0)
|
|
29
|
+
|
|
30
|
+
# Post-HF correlation breakdown — energy_hartree already includes every
|
|
31
|
+
# contribution, so the HF reference is the total minus those.
|
|
32
|
+
_mp2 = get("mp2_correlation_hartree")
|
|
33
|
+
if _mp2 is not None:
|
|
34
|
+
rows += _num("HF reference", f"{energy - _mp2:.8f} Ha")
|
|
35
|
+
rows += _num("MP2 correlation", f"{_mp2:.8f} Ha")
|
|
36
|
+
_ccsd = get("ccsd_correlation_hartree")
|
|
37
|
+
_ccsd_t = get("ccsd_t_correction_hartree")
|
|
38
|
+
if _ccsd is not None:
|
|
39
|
+
rows += _num("HF reference", f"{energy - _ccsd - (_ccsd_t or 0.0):.8f} Ha")
|
|
40
|
+
rows += _num("CCSD correlation", f"{_ccsd:.8f} Ha")
|
|
41
|
+
if _ccsd_t is not None:
|
|
42
|
+
rows += _num("(T) triples correction", f"{_ccsd_t:.8f} Ha")
|
|
43
|
+
|
|
44
|
+
_solvent = get("solvent")
|
|
45
|
+
if _solvent is not None:
|
|
46
|
+
rows += _num("Solvent (PCM)", str(_solvent))
|
|
47
|
+
|
|
48
|
+
# Compute device — always shown; old saved results lack the
|
|
49
|
+
# field and safely read "CPU".
|
|
50
|
+
if bool(get("gpu_used", False)):
|
|
51
|
+
_name = get("gpu_name")
|
|
52
|
+
_device = (
|
|
53
|
+
f'<span style="color:#16a34a">🚀 GPU</span>'
|
|
54
|
+
f' — <span style="font-family:monospace">{_name}</span>'
|
|
55
|
+
if _name
|
|
56
|
+
else '<span style="color:#16a34a">🚀 GPU</span>'
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
_device = '<span style="color:#555">CPU</span>'
|
|
60
|
+
rows += (
|
|
61
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Compute device</td>'
|
|
62
|
+
f"<td>{_device}</td></tr>"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_dip = get("dipole_moment_debye")
|
|
66
|
+
if _dip is not None:
|
|
67
|
+
rows += _num("Dipole moment", f"{_dip:.4f} D")
|
|
68
|
+
|
|
69
|
+
_chg = get("mulliken_charges")
|
|
70
|
+
_syms = get("atom_symbols")
|
|
71
|
+
if _chg is not None and _syms is not None:
|
|
72
|
+
_charge_str = " ".join(f"{sym}:{c:+.3f}" for sym, c in zip(_syms, _chg))
|
|
73
|
+
rows += (
|
|
74
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444;vertical-align:top">'
|
|
75
|
+
f"Mulliken charges</td>"
|
|
76
|
+
f'<td style="color:#000;font-family:monospace;font-size:12px;'
|
|
77
|
+
f'word-break:break-all">{_charge_str}</td></tr>'
|
|
78
|
+
)
|
|
79
|
+
return rows
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def format_result(r: Any) -> str:
|
|
83
|
+
"""Format a single-point-style result card."""
|
|
84
|
+
_conv = "Yes" if r.converged else "No (treat results with caution)"
|
|
85
|
+
_cc = "green" if r.converged else "#c00"
|
|
86
|
+
_gap = f"{r.homo_lumo_gap_ev:.4f} eV" if r.homo_lumo_gap_ev is not None else "N/A"
|
|
87
|
+
_rows = "".join(
|
|
88
|
+
f"<tr>"
|
|
89
|
+
f'<td style="padding:3px 18px 3px 0;color:#444">{k}</td>'
|
|
90
|
+
f'<td style="color:{vc}">{v}</td>'
|
|
91
|
+
f"</tr>"
|
|
92
|
+
for k, v, vc in [
|
|
93
|
+
(
|
|
94
|
+
"Total energy",
|
|
95
|
+
f"{r.energy_hartree:.8f} Ha  ({r.energy_ev:.4f} eV)",
|
|
96
|
+
"#000",
|
|
97
|
+
),
|
|
98
|
+
("HOMO-LUMO gap", _gap, "#000"),
|
|
99
|
+
("SCF converged", _conv, _cc),
|
|
100
|
+
(
|
|
101
|
+
"SCF iterations",
|
|
102
|
+
(
|
|
103
|
+
"—"
|
|
104
|
+
if getattr(r, "n_iterations", None) in (None, -1)
|
|
105
|
+
else str(r.n_iterations)
|
|
106
|
+
),
|
|
107
|
+
"#000",
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
_extra = _result_extra_rows(lambda k, d=None: getattr(r, k, d))
|
|
112
|
+
return (
|
|
113
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
114
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
115
|
+
f"<b>{r.formula} — {r.method}/{r.basis}</b>"
|
|
116
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
117
|
+
f"{_rows}{_extra}</table></div>"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_opt_result(r: Any) -> str:
|
|
122
|
+
"""Format a geometry-optimization result card."""
|
|
123
|
+
_conv = "Yes" if r.converged else "No (max steps reached)"
|
|
124
|
+
_cc = "green" if r.converged else "#c00"
|
|
125
|
+
_rows = "".join(
|
|
126
|
+
f"<tr>"
|
|
127
|
+
f'<td style="padding:3px 18px 3px 0;color:#444">{k}</td>'
|
|
128
|
+
f'<td style="color:{vc}">{v}</td>'
|
|
129
|
+
f"</tr>"
|
|
130
|
+
for k, v, vc in [
|
|
131
|
+
("Final energy", f"{r.energy_hartree:.8f} Ha", "#000"),
|
|
132
|
+
("Energy change", f"{r.energy_change_hartree:+.6f} Ha", "#000"),
|
|
133
|
+
("Opt converged", _conv, _cc),
|
|
134
|
+
("Steps taken", str(r.n_steps), "#000"),
|
|
135
|
+
("Geometry RMSD", f"{r.rmsd_angstrom:.4f} Å", "#000"),
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
return (
|
|
139
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
140
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
141
|
+
f"<b>Geometry Optimisation — {r.formula} ({r.method}/{r.basis})</b>"
|
|
142
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
143
|
+
f"{_rows}</table></div>"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_freq_result(r: Any) -> str:
|
|
148
|
+
"""Format a frequency-analysis result card."""
|
|
149
|
+
_conv = "Yes" if r.converged else "No (treat with caution)"
|
|
150
|
+
_cc = "green" if r.converged else "#c00"
|
|
151
|
+
n_real = r.n_real_modes()
|
|
152
|
+
n_imag = r.n_imaginary_modes()
|
|
153
|
+
real_freqs = sorted(f for f in r.frequencies_cm1 if f > 0)[:6]
|
|
154
|
+
freq_str = " ".join(f"{f:.1f}" for f in real_freqs)
|
|
155
|
+
if len([f for f in r.frequencies_cm1 if f > 0]) > 6:
|
|
156
|
+
freq_str += " …"
|
|
157
|
+
imag_note = ""
|
|
158
|
+
if n_imag > 0:
|
|
159
|
+
imag_note = (
|
|
160
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Imaginary modes</td>'
|
|
161
|
+
f'<td style="color:#c00">{n_imag} — geometry may not be a minimum</td></tr>'
|
|
162
|
+
)
|
|
163
|
+
_rows = (
|
|
164
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">SCF energy</td>'
|
|
165
|
+
f'<td style="color:#000">{r.energy_hartree:.8f} Ha</td></tr>'
|
|
166
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">SCF converged</td>'
|
|
167
|
+
f'<td style="color:{_cc}">{_conv}</td></tr>'
|
|
168
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Real modes</td>'
|
|
169
|
+
f'<td style="color:#000">{n_real}</td></tr>'
|
|
170
|
+
+ imag_note
|
|
171
|
+
+ (
|
|
172
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Frequencies (cm⁻¹)</td>'
|
|
173
|
+
f'<td style="color:#000;font-family:monospace">{freq_str or "none"}</td></tr>'
|
|
174
|
+
if real_freqs
|
|
175
|
+
else ""
|
|
176
|
+
)
|
|
177
|
+
+ f'<tr><td style="padding:3px 18px 3px 0;color:#444">ZPVE</td>'
|
|
178
|
+
f'<td style="color:#000">{r.zpve_hartree:.6f} Ha '
|
|
179
|
+
f"({r.zpve_hartree * 27.211386245988:.4f} eV)</td></tr>"
|
|
180
|
+
)
|
|
181
|
+
_thermo_rows = ""
|
|
182
|
+
_thermo = getattr(r, "thermo", None)
|
|
183
|
+
if _thermo is not None:
|
|
184
|
+
_kj = 2625.5 # kJ/mol per Hartree
|
|
185
|
+
_thermo_rows = (
|
|
186
|
+
f'<tr><td colspan="2" style="padding:6px 0 2px 0;color:#666;'
|
|
187
|
+
f'font-size:12px;font-style:italic">'
|
|
188
|
+
f"— Thermochemistry at {_thermo.temperature_k:.0f} K / 1 atm —"
|
|
189
|
+
f"</td></tr>"
|
|
190
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">H (298 K)</td>'
|
|
191
|
+
f'<td style="color:#000">{_thermo.H_hartree:.6f} Ha</td></tr>'
|
|
192
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">S (298 K)</td>'
|
|
193
|
+
f'<td style="color:#000">{_thermo.S_jmol:.2f} J/(mol·K)</td></tr>'
|
|
194
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">G (298 K)</td>'
|
|
195
|
+
f'<td style="color:#000">{_thermo.G_hartree:.6f} Ha'
|
|
196
|
+
f" ({_thermo.G_hartree * _kj:.2f} kJ/mol)</td></tr>"
|
|
197
|
+
)
|
|
198
|
+
return (
|
|
199
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
200
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
201
|
+
f"<b>Frequency Analysis — {r.formula} ({r.method}/{r.basis})</b>"
|
|
202
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
203
|
+
f"{_rows}{_thermo_rows}</table></div>"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def format_tddft_result(r: Any) -> str:
|
|
208
|
+
"""Format a TD-DFT / UV-Vis result card."""
|
|
209
|
+
_conv = "Yes" if r.converged else "No (treat with caution)"
|
|
210
|
+
_cc = "green" if r.converged else "#c00"
|
|
211
|
+
header_rows = (
|
|
212
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Ground-state energy</td>'
|
|
213
|
+
f'<td style="color:#000">{r.energy_hartree:.8f} Ha</td></tr>'
|
|
214
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">SCF converged</td>'
|
|
215
|
+
f'<td style="color:{_cc}">{_conv}</td></tr>'
|
|
216
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">States computed</td>'
|
|
217
|
+
f'<td style="color:#000">{len(r.excitation_energies_ev)}</td></tr>'
|
|
218
|
+
)
|
|
219
|
+
exc_table = ""
|
|
220
|
+
if r.excitation_energies_ev:
|
|
221
|
+
wl = r.wavelengths_nm()
|
|
222
|
+
exc_rows = []
|
|
223
|
+
for i, (e_ev, f_osc) in enumerate(
|
|
224
|
+
zip(r.excitation_energies_ev[:8], r.oscillator_strengths[:8]), 1
|
|
225
|
+
):
|
|
226
|
+
bold = "font-weight:bold" if f_osc > 0.05 else ""
|
|
227
|
+
exc_rows.append(
|
|
228
|
+
f'<tr style="{bold}">'
|
|
229
|
+
f'<td style="padding:2px 12px 2px 0;color:#555">S{i}</td>'
|
|
230
|
+
f'<td style="padding:2px 12px 2px 0;color:#000">{e_ev:.3f} eV</td>'
|
|
231
|
+
f'<td style="padding:2px 12px 2px 0;color:#000">{wl[i - 1]:.1f} nm</td>'
|
|
232
|
+
f'<td style="padding:2px 4px 2px 0;color:#000">f = {f_osc:.4f}</td>'
|
|
233
|
+
f"</tr>"
|
|
234
|
+
)
|
|
235
|
+
if len(r.excitation_energies_ev) > 8:
|
|
236
|
+
exc_rows.append(
|
|
237
|
+
f'<tr><td colspan="4" style="color:#888;font-size:12px">… '
|
|
238
|
+
f"and {len(r.excitation_energies_ev) - 8} more states</td></tr>"
|
|
239
|
+
)
|
|
240
|
+
exc_table = (
|
|
241
|
+
'<tr><td colspan="2" style="padding:8px 0 2px;color:#444;font-weight:bold">'
|
|
242
|
+
"Vertical excitations:</td></tr>"
|
|
243
|
+
"<tr>"
|
|
244
|
+
'<th style="text-align:left;color:#555;font-size:12px;padding:2px 12px 2px 0">State</th>'
|
|
245
|
+
'<th style="text-align:left;color:#555;font-size:12px;padding:2px 12px 2px 0">Energy</th>'
|
|
246
|
+
'<th style="text-align:left;color:#555;font-size:12px;padding:2px 12px 2px 0">λ</th>'
|
|
247
|
+
'<th style="text-align:left;color:#555;font-size:12px">Osc. str.</th></tr>'
|
|
248
|
+
+ "".join(exc_rows)
|
|
249
|
+
)
|
|
250
|
+
return (
|
|
251
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
252
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
253
|
+
f"<b>TD-DFT / UV-Vis — {r.formula} ({r.method}/{r.basis})</b>"
|
|
254
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
255
|
+
f"{header_rows}{exc_table}</table></div>"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def format_nmr_result(r: Any) -> str:
|
|
260
|
+
"""Format an NMR shielding result card."""
|
|
261
|
+
_conv = "Yes" if r.converged else "No (treat with caution)"
|
|
262
|
+
_cc = "green" if r.converged else "#c00"
|
|
263
|
+
header_rows = (
|
|
264
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">SCF converged</td>'
|
|
265
|
+
f'<td style="color:{_cc}">{_conv}</td></tr>'
|
|
266
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Reference</td>'
|
|
267
|
+
f'<td style="color:#000">{r.reference_compound} ({r.method}/{r.basis})</td></tr>'
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _nmr_table(label: str, shifts: list, sym: str) -> str:
|
|
271
|
+
if not shifts:
|
|
272
|
+
return ""
|
|
273
|
+
rows = "".join(
|
|
274
|
+
f"<tr>"
|
|
275
|
+
f'<td style="padding:2px 14px 2px 0;color:#555">{sym}-{n}</td>'
|
|
276
|
+
f'<td style="color:#000">{d:.2f} ppm</td>'
|
|
277
|
+
f"</tr>"
|
|
278
|
+
for n, (_i, d) in enumerate(shifts, 1)
|
|
279
|
+
)
|
|
280
|
+
return (
|
|
281
|
+
f'<tr><td colspan="2" style="padding:8px 0 2px;color:#444;font-weight:bold">'
|
|
282
|
+
f"{label} shifts (vs. TMS):</td></tr>"
|
|
283
|
+
f"<tr>"
|
|
284
|
+
f'<th style="text-align:left;color:#555;font-size:12px;padding:2px 14px 2px 0">Atom</th>'
|
|
285
|
+
f'<th style="text-align:left;color:#555;font-size:12px">δ (ppm)</th></tr>'
|
|
286
|
+
+ rows
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
h_table = _nmr_table("¹H", r.h_shifts(), "H")
|
|
290
|
+
c_table = _nmr_table("¹³C", r.c_shifts(), "C")
|
|
291
|
+
|
|
292
|
+
_basis_warn = ""
|
|
293
|
+
if r.basis.upper() in ("STO-3G", "3-21G"):
|
|
294
|
+
_basis_warn = (
|
|
295
|
+
'<tr><td colspan="2" style="padding:6px 0 0">'
|
|
296
|
+
'<span style="color:#b45309;font-size:12px">'
|
|
297
|
+
f"⚠ {r.basis} gives qualitative NMR only — use 6-31G* or better.</span>"
|
|
298
|
+
"</td></tr>"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# M4 audit fix (2026-07-14): the reference shielding constants table only
|
|
302
|
+
# covers a handful of method/basis combinations; any other combination
|
|
303
|
+
# silently substitutes the B3LYP/6-31G* constants, which can shift the
|
|
304
|
+
# reported ppm values by several ppm relative to a properly calibrated
|
|
305
|
+
# reference. Surface that substitution rather than let it pass silently.
|
|
306
|
+
_ref_warn = ""
|
|
307
|
+
if getattr(r, "is_fallback_reference", False):
|
|
308
|
+
_ref_warn = (
|
|
309
|
+
'<tr><td colspan="2" style="padding:6px 0 0">'
|
|
310
|
+
'<span style="color:#b45309;font-size:12px">'
|
|
311
|
+
f"⚠ No calibrated TMS reference for {r.method}/{r.basis} — using "
|
|
312
|
+
f"{getattr(r, 'reference_key', 'B3LYP/6-31G*')} constants instead. "
|
|
313
|
+
"Shifts may be off by a few ppm.</span>"
|
|
314
|
+
"</td></tr>"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
_empty = ""
|
|
318
|
+
if not r.h_shifts() and not r.c_shifts():
|
|
319
|
+
_empty = (
|
|
320
|
+
'<tr><td colspan="2" style="color:#888;font-size:12px">'
|
|
321
|
+
"No ¹H or ¹³C atoms found in this molecule.</td></tr>"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
326
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
327
|
+
f"<b>NMR Shielding — {r.formula} ({r.method}/{r.basis})</b>"
|
|
328
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
329
|
+
f"{header_rows}{h_table}{c_table}{_empty}{_basis_warn}{_ref_warn}</table></div>"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def format_pes_scan_result(r: Any) -> str:
|
|
334
|
+
"""Format a PESScanResult as an HTML result card."""
|
|
335
|
+
_conv = "Yes" if r.converged_all else "No (some points did not converge)"
|
|
336
|
+
_cc = "green" if r.converged_all else "#c00"
|
|
337
|
+
if r.energies_hartree:
|
|
338
|
+
e_min = min(r.energies_hartree)
|
|
339
|
+
e_max = max(r.energies_hartree)
|
|
340
|
+
barrier_kcal = (e_max - e_min) * 627.509474
|
|
341
|
+
_e_row = (
|
|
342
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Min energy</td>'
|
|
343
|
+
f'<td style="color:#000">{e_min:.8f} Ha</td></tr>'
|
|
344
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Energy range</td>'
|
|
345
|
+
f'<td style="color:#000">{barrier_kcal:.2f} kcal/mol</td></tr>'
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
_e_row = ""
|
|
349
|
+
_idx_str = "–".join(str(i + 1) for i in r.atom_indices)
|
|
350
|
+
return (
|
|
351
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
352
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
353
|
+
f"<b>PES Scan — {r.formula} ({r.method}/{r.basis})</b>"
|
|
354
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
355
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Scan type</td>'
|
|
356
|
+
f'<td style="color:#000">{r.scan_type.capitalize()} ({_idx_str})</td></tr>'
|
|
357
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Range</td>'
|
|
358
|
+
f'<td style="color:#000">{r.scan_parameter_values[0]:.3f} → '
|
|
359
|
+
f"{r.scan_parameter_values[-1]:.3f} {r.scan_unit} "
|
|
360
|
+
f"({r.n_steps} points)</td></tr>"
|
|
361
|
+
f"{_e_row}"
|
|
362
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">All converged</td>'
|
|
363
|
+
f'<td style="color:{_cc}">{_conv}</td></tr>'
|
|
364
|
+
f"</table></div>"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def format_reorg_result(r: Any) -> str:
|
|
369
|
+
"""Format a reorganization-energy (Marcus 4-point) result card."""
|
|
370
|
+
_conv = "Yes" if r.converged else "No (some steps did not converge)"
|
|
371
|
+
_cc = "green" if r.converged else "#c00"
|
|
372
|
+
|
|
373
|
+
def _channel_block(ch: Any) -> str:
|
|
374
|
+
rows = "".join(
|
|
375
|
+
f'<tr><td style="padding:2px 18px 2px 0;color:#444">{k}</td>'
|
|
376
|
+
f'<td style="color:#000;font-family:monospace">{v}</td></tr>'
|
|
377
|
+
for k, v in [
|
|
378
|
+
("λ", f"{ch.lambda_ev:.4f} eV ({ch.lambda_kcal:.2f} kcal/mol)"),
|
|
379
|
+
("λ₁ ion relaxation", f"{ch.lambda1_hartree * 27.211386245988:.4f} eV"),
|
|
380
|
+
(
|
|
381
|
+
"λ₂ neutral relaxation",
|
|
382
|
+
f"{ch.lambda2_hartree * 27.211386245988:.4f} eV",
|
|
383
|
+
),
|
|
384
|
+
(
|
|
385
|
+
"Ion state",
|
|
386
|
+
f"charge {ch.ion_charge:+d}, mult {ch.ion_multiplicity}",
|
|
387
|
+
),
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
return (
|
|
391
|
+
f'<div style="margin-top:8px">'
|
|
392
|
+
f'<b style="font-size:13px;color:#166534">{ch.label}</b>'
|
|
393
|
+
f'<table style="margin-top:2px;font-size:13px;border-collapse:collapse">'
|
|
394
|
+
f"{rows}</table></div>"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
_channels_html = "".join(_channel_block(ch) for ch in r.channels)
|
|
398
|
+
return (
|
|
399
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
400
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0">'
|
|
401
|
+
f"<b>Reorganization Energy (Marcus 4-point) — "
|
|
402
|
+
f"{r.formula} ({r.method}/{r.basis})</b>"
|
|
403
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
404
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Neutral energy</td>'
|
|
405
|
+
f'<td style="color:#000">{r.neutral_energy_hartree:.8f} Ha</td></tr>'
|
|
406
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">Total opt steps</td>'
|
|
407
|
+
f'<td style="color:#000">{r.n_total_opt_steps}</td></tr>'
|
|
408
|
+
f'<tr><td style="padding:3px 18px 3px 0;color:#444">All converged</td>'
|
|
409
|
+
f'<td style="color:{_cc}">{_conv}</td></tr>'
|
|
410
|
+
f"</table>{_channels_html}</div>"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def format_past_result(data: dict[str, Any], result_dir: Optional[Path] = None) -> str:
|
|
415
|
+
"""Format a saved result.json payload as an HTML result card."""
|
|
416
|
+
import base64 as _b64
|
|
417
|
+
|
|
418
|
+
_ct_labels = {
|
|
419
|
+
"single_point": ("Single Point", "#2563eb", "#dbeafe"),
|
|
420
|
+
"geometry_opt": ("Geometry Optimization", "#7c3aed", "#ede9fe"),
|
|
421
|
+
"frequency": ("Frequency Analysis", "#15803d", "#dcfce7"),
|
|
422
|
+
"tddft": ("TD-DFT", "#b45309", "#fef3c7"),
|
|
423
|
+
"nmr": ("NMR", "#0d9488", "#ccfbf1"),
|
|
424
|
+
"pes_scan": ("PES Scan", "#c2410c", "#ffedd5"),
|
|
425
|
+
}
|
|
426
|
+
ct = data.get("calc_type", "")
|
|
427
|
+
_ct_label, _ct_fg, _ct_bg = _ct_labels.get(
|
|
428
|
+
ct, (ct.replace("_", " ").title(), "#555", "#f3f4f6")
|
|
429
|
+
)
|
|
430
|
+
_ct_badge = (
|
|
431
|
+
f'<span style="display:inline-block;padding:2px 10px;border-radius:12px;'
|
|
432
|
+
f"background:{_ct_bg};color:{_ct_fg};font-size:12px;font-weight:700;"
|
|
433
|
+
f'letter-spacing:0.03em;margin-bottom:6px">{_ct_label}</span>'
|
|
434
|
+
)
|
|
435
|
+
_conv = "Yes" if data.get("converged") else "No (treat results with caution)"
|
|
436
|
+
_cc = "green" if data.get("converged") else "#c00"
|
|
437
|
+
_gap = (
|
|
438
|
+
f"{data['homo_lumo_gap_ev']:.4f} eV"
|
|
439
|
+
if data.get("homo_lumo_gap_ev") is not None
|
|
440
|
+
else "N/A"
|
|
441
|
+
)
|
|
442
|
+
_rows = "".join(
|
|
443
|
+
f"<tr>"
|
|
444
|
+
f'<td style="padding:3px 18px 3px 0;color:#444">{k}</td>'
|
|
445
|
+
f'<td style="color:{vc}">{v}</td>'
|
|
446
|
+
f"</tr>"
|
|
447
|
+
for k, v, vc in [
|
|
448
|
+
(
|
|
449
|
+
"Total energy",
|
|
450
|
+
f"{data['energy_hartree']:.8f} Ha  ({data['energy_ev']:.4f} eV)",
|
|
451
|
+
"#000",
|
|
452
|
+
),
|
|
453
|
+
("HOMO-LUMO gap", _gap, "#000"),
|
|
454
|
+
("SCF converged", _conv, _cc),
|
|
455
|
+
(
|
|
456
|
+
"SCF iterations",
|
|
457
|
+
(
|
|
458
|
+
"—"
|
|
459
|
+
if data.get("n_iterations") in (None, -1)
|
|
460
|
+
else str(data.get("n_iterations"))
|
|
461
|
+
),
|
|
462
|
+
"#000",
|
|
463
|
+
),
|
|
464
|
+
]
|
|
465
|
+
)
|
|
466
|
+
ts = data.get("timestamp", "")
|
|
467
|
+
|
|
468
|
+
# Shared 'extra' rows (correlation breakdown / solvent / device / dipole /
|
|
469
|
+
# Mulliken) — same builder as the live card so the two never drift.
|
|
470
|
+
_extra = _result_extra_rows(lambda k, d=None: data.get(k, d))
|
|
471
|
+
|
|
472
|
+
# Embed thumbnail if saved
|
|
473
|
+
_thumb_html = ""
|
|
474
|
+
if result_dir is not None:
|
|
475
|
+
_thumb_path = Path(result_dir) / "thumbnail.png"
|
|
476
|
+
if _thumb_path.exists():
|
|
477
|
+
_img_b64 = _b64.b64encode(_thumb_path.read_bytes()).decode()
|
|
478
|
+
_thumb_html = (
|
|
479
|
+
f'<img src="data:image/png;base64,{_img_b64}" '
|
|
480
|
+
f'style="float:right;margin:0 0 6px 14px;border-radius:4px;'
|
|
481
|
+
f'border:1px solid #e2e8f0" width="173" height="108" />'
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
f'<div style="background:#f0fff0;border-left:4px solid #4CAF50;'
|
|
486
|
+
f'padding:10px 14px;border-radius:4px;margin:6px 0;overflow:hidden">'
|
|
487
|
+
f"{_thumb_html}"
|
|
488
|
+
f"{_ct_badge}<br>"
|
|
489
|
+
f'<b>{data["formula"]} — {data["method"]}/{data["basis"]}</b>'
|
|
490
|
+
f' <small style="color:#777">{ts}</small>'
|
|
491
|
+
f'<table style="margin-top:8px;font-size:14px;border-collapse:collapse">'
|
|
492
|
+
f"{_rows}{_extra}</table></div>"
|
|
493
|
+
)
|