quantui 0.6.0__tar.gz → 0.7.0__tar.gz
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-0.6.0 → quantui-0.7.0}/CHANGELOG.md +104 -1
- {quantui-0.6.0/quantui.egg-info → quantui-0.7.0}/PKG-INFO +1 -1
- {quantui-0.6.0 → quantui-0.7.0}/pyproject.toml +1 -1
- {quantui-0.6.0 → quantui-0.7.0}/quantui/__init__.py +1 -1
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app.py +311 -2
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_analysis.py +104 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_builders.py +120 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_formatters.py +260 -23
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_runflow.py +351 -8
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_visualization.py +194 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/benchmarks.py +20 -2
- {quantui-0.6.0 → quantui-0.7.0}/quantui/calc_log.py +132 -8
- quantui-0.7.0/quantui/checkpoint.py +650 -0
- quantui-0.7.0/quantui/estimator_eval.py +272 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/help_content.py +70 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/optimizer.py +130 -3
- {quantui-0.6.0 → quantui-0.7.0}/quantui/pes_scan.py +117 -1
- {quantui-0.6.0 → quantui-0.7.0}/quantui/reorganization_energy.py +93 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/results_storage.py +67 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/session_calc.py +107 -1
- {quantui-0.6.0 → quantui-0.7.0/quantui.egg-info}/PKG-INFO +1 -1
- {quantui-0.6.0 → quantui-0.7.0}/quantui.egg-info/SOURCES.txt +2 -0
- {quantui-0.6.0 → quantui-0.7.0}/LICENSE +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/MANIFEST.in +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/README.md +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/SECURITY.md +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/analytics.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_exports.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/app_history.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/ase_bridge.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/c_stderr.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/cactus.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/calculator.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/cancellation.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/cli.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/comparison.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/config.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/js/3Dmol-min.js +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/js/3Dmol-min.js.LICENSE.txt +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/library/library.sqlite +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/manifests/bulk_qm9.json +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/manifests/curated.json +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/data/manifests/presets.json +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/descriptor_cards.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/freq_calc.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/freq_ir_workers.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/gpu_offload.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/ir_plot.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/issue_tracker.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/live_log.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/log_utils.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/molecule.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/molecule_library.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/nmr_calc.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/orbital_visualization.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/preopt.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/progress.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/pubchem.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/security.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/structure_providers.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/tddft_calc.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/theme.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/user_settings.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/utils.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/vib_cache.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/visualization_py3dmol.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/viz_assets.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui/viz_backend_router.py +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui.egg-info/dependency_links.txt +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui.egg-info/entry_points.txt +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui.egg-info/requires.txt +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/quantui.egg-info/top_level.txt +0 -0
- {quantui-0.6.0 → quantui-0.7.0}/setup.cfg +0 -0
|
@@ -7,6 +7,108 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-08-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Running the test suite no longer corrupts your time estimates.** QuantUI's
|
|
15
|
+
own tests were writing their runs into `~/.quantui/logs/perf_log.jsonl` — the
|
|
16
|
+
file the runtime estimator learns from. Because those tests use a *simulated*
|
|
17
|
+
calculation, each one recorded a fabricated timing: 2 773 records were in the
|
|
18
|
+
log and roughly four fifths of them were test artifacts, including "water
|
|
19
|
+
frequency" runs whose recorded times ranged from 0.34 s to 143 s for identical
|
|
20
|
+
chemistry. This is the reason the pre-run estimate had been unreliable. Only
|
|
21
|
+
developers running the test suite were affected; the fabricated records are
|
|
22
|
+
now superseded automatically as real runs accumulate, so no manual cleanup is
|
|
23
|
+
needed.
|
|
24
|
+
- **Calibration now times the calculation, not the process.** Each calibration
|
|
25
|
+
step runs in a fresh subprocess and its stopwatch started before PySCF was
|
|
26
|
+
even imported, so every calibration record was inflated by startup cost that
|
|
27
|
+
a real run in an open session never pays. Import time is still recorded, just
|
|
28
|
+
separately.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Checkpoints — an interrupted calculation can pick up where it stopped.**
|
|
33
|
+
Geometry Optimization saves its trajectory and the optimizer's accumulated
|
|
34
|
+
curvature after every step; PES Scan banks each point as it finishes. If a
|
|
35
|
+
run is cancelled, crashes, or the machine goes to sleep, the Calculate tab
|
|
36
|
+
offers to resume it — and says how much is already done ("8 of 20 scan points
|
|
37
|
+
already computed") rather than just asking. The offer appears only when the
|
|
38
|
+
calculation you have configured is *exactly* the interrupted one, geometry
|
|
39
|
+
included, so a resume can never splice two different runs together.
|
|
40
|
+
- **An "Unfinished calculations" list on the History tab.** After restarting
|
|
41
|
+
QuantUI you no longer have to remember what you were running: every
|
|
42
|
+
interrupted calculation is listed with its molecule, type, level of theory,
|
|
43
|
+
how much finished and how long ago. **Load these settings** puts the molecule
|
|
44
|
+
and all its settings back on the Calculate tab, ready to resume; **Discard**
|
|
45
|
+
removes one you are done with. Hidden entirely when nothing is unfinished.
|
|
46
|
+
- **Checkpoint activity is recorded in the saved output log.** Opening a
|
|
47
|
+
checkpoint, every save, completion and discarding all appear as
|
|
48
|
+
`[checkpoint]` lines in `pyscf.log`. Resuming writes a banner stating plainly
|
|
49
|
+
that the log covers only the continuation and that the earlier output lives
|
|
50
|
+
in the interrupted run's result directory — without it the file would read
|
|
51
|
+
as a calculation that started from the geometry at the top. A warm start now
|
|
52
|
+
names the file its initial density came from, since the SCF iteration count
|
|
53
|
+
is only interpretable if you know what it started from.
|
|
54
|
+
- **Help topic: "Resuming an interrupted calculation."** Covers how to resume,
|
|
55
|
+
why the offer disappears if you change a setting, which calculation types can
|
|
56
|
+
be resumed, and where checkpoints are stored.
|
|
57
|
+
- **Warm-started SCF.** A converged density from an earlier run of the same
|
|
58
|
+
molecule, charge, method and basis is reused as the starting guess, which
|
|
59
|
+
usually cuts several SCF cycles. The geometry does not have to match — a
|
|
60
|
+
density from a nearby geometry is a good guess, which is what a geometry
|
|
61
|
+
optimization relies on internally.
|
|
62
|
+
- **Calculation records say how they were measured.** Runs launched from the app
|
|
63
|
+
and runs measured by the calibration tool are now labelled as such, and the
|
|
64
|
+
estimator keeps them apart instead of averaging two populations that measure
|
|
65
|
+
different things. Records also carry a per-stage time breakdown (SCF, Hessian,
|
|
66
|
+
excited-state solve, …), which is the groundwork for stage-aware estimates.
|
|
67
|
+
- **`python -m quantui.estimator_eval`** — replays your recorded history through
|
|
68
|
+
the estimator and reports how accurate it would have been, split by
|
|
69
|
+
calculation type. Reports coverage alongside accuracy, so a model that stays
|
|
70
|
+
silent can't look good by refusing to answer.
|
|
71
|
+
|
|
72
|
+
## [0.6.1] - 2026-08-05
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **Reorganization-energy results now survive History.** Reloading one from
|
|
77
|
+
History showed the calculation's headline numbers as missing — λ, the
|
|
78
|
+
four-point energies and the per-channel breakdown were never written to disk,
|
|
79
|
+
so there was nothing to redisplay. Both the live and History result cards now
|
|
80
|
+
render from the same saved data, so anything visible during a run is visible
|
|
81
|
+
after reloading it.
|
|
82
|
+
**Results saved before this cannot be recovered** — λ requires two geometry
|
|
83
|
+
optimizations and four SCF energies. Those results now say so on the card and
|
|
84
|
+
tell you to re-run, rather than appearing silently incomplete.
|
|
85
|
+
- **3-D backgrounds follow the theme** in the new geometry viewers, matching the
|
|
86
|
+
fix applied to the other viewers in 0.6.0.
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
|
|
90
|
+
- **A Geometries panel** for reorganization energy, on the Analysis tab — which
|
|
91
|
+
previously showed nothing at all for this calculation type. Step through the
|
|
92
|
+
distinct geometries behind the four energies, or overlay two of them with
|
|
93
|
+
arrows marking how each atom moved. The arrows can be scaled ×1–×10 for small
|
|
94
|
+
relaxations; the structures always show true computed positions.
|
|
95
|
+
- **Geometry relaxation on the result card** — RMSD and the largest single-atom
|
|
96
|
+
shift between the neutral and ion geometries, which is what λ physically
|
|
97
|
+
measures.
|
|
98
|
+
- **Compare λ across saved results.** Selecting several reorganization-energy
|
|
99
|
+
runs in the Compare tab now shows a λ table with a column per channel, for
|
|
100
|
+
screening candidates by how much they reorganize.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- The NCShare GPU diagnostic notebook now reports the **CPU affinity mask**
|
|
105
|
+
alongside the core count, and states plainly when CPU timings cannot be
|
|
106
|
+
trusted. A job can be granted 6 cores while still *seeing* 192; whether that
|
|
107
|
+
matters depends on how the scheduler constrains it, and the core count alone
|
|
108
|
+
cannot distinguish the two cases.
|
|
109
|
+
- `apptainer/README.md` records the first verified H200 run — driver, compute
|
|
110
|
+
capability, partition, and the measured CPU/GPU crossover.
|
|
111
|
+
|
|
10
112
|
## [0.6.0] - 2026-08-04
|
|
11
113
|
|
|
12
114
|
### Added
|
|
@@ -563,7 +665,8 @@ Initial public scaffolding of the QuantUI package: `quantui` package with
|
|
|
563
665
|
`calculator.py`, basic notebook launcher, Apptainer container definition,
|
|
564
666
|
MIT license, and project metadata.
|
|
565
667
|
|
|
566
|
-
[Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.
|
|
668
|
+
[Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.1...HEAD
|
|
669
|
+
[0.6.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.0...v0.6.1
|
|
567
670
|
[0.6.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.2...v0.6.0
|
|
568
671
|
[0.5.2]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.1...v0.5.2
|
|
569
672
|
[0.5.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.0...v0.5.1
|
|
@@ -7,7 +7,7 @@ Calculations run locally in the Jupyter session — no cluster or SLURM required
|
|
|
7
7
|
PySCF requires Linux/macOS/WSL. Windows users should use the Apptainer container.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.7.0"
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
from typing import Any
|
|
@@ -45,6 +45,9 @@ from quantui.app_analysis import (
|
|
|
45
45
|
from quantui.app_analysis import (
|
|
46
46
|
deactivate_all_ana_panels as _ana_deactivate_all_ana_panels,
|
|
47
47
|
)
|
|
48
|
+
from quantui.app_analysis import (
|
|
49
|
+
on_reorg_view_changed as _ana_on_reorg_view_changed,
|
|
50
|
+
)
|
|
48
51
|
from quantui.app_analysis import (
|
|
49
52
|
pop_energies as _ana_pop_energies,
|
|
50
53
|
)
|
|
@@ -69,6 +72,9 @@ from quantui.app_analysis import (
|
|
|
69
72
|
from quantui.app_analysis import (
|
|
70
73
|
pop_preopt_trajectory as _ana_pop_preopt_trajectory,
|
|
71
74
|
)
|
|
75
|
+
from quantui.app_analysis import (
|
|
76
|
+
pop_reorg_geometries as _ana_pop_reorg_geometries,
|
|
77
|
+
)
|
|
72
78
|
from quantui.app_analysis import (
|
|
73
79
|
pop_uv_vis as _ana_pop_uv_vis,
|
|
74
80
|
)
|
|
@@ -688,6 +694,9 @@ _RE_CYCLE = re.compile(
|
|
|
688
694
|
)
|
|
689
695
|
_RE_CONV = re.compile(r"converged SCF energy\s*=\s*([\-\d\.]+)")
|
|
690
696
|
_RE_Q_STATUS = re.compile(r"\[QuantUI_STATUS\]\s*(.+)")
|
|
697
|
+
# Step/point/state counters inside a status message. Removed before the
|
|
698
|
+
# message is used as a per-stage timing key — see _LogCapture._stage_key.
|
|
699
|
+
_RE_STAGE_NUMBERS = re.compile(r"\d+(?:[./]\d+)*")
|
|
691
700
|
|
|
692
701
|
# ── Silent-phase heartbeat (M-PROGRESS Phase D) ──────────────────────────────
|
|
693
702
|
#
|
|
@@ -751,6 +760,60 @@ class _LogCapture:
|
|
|
751
760
|
self._hb_started_t = self._last_write_t
|
|
752
761
|
self._hb_stop = threading.Event()
|
|
753
762
|
self._hb_thread: Optional[threading.Thread] = None
|
|
763
|
+
# Per-stage wall times (M-PROGRESS Phase C, deferred from B3).
|
|
764
|
+
# Every calc type already announces its phases through
|
|
765
|
+
# log_utils.emit_status, and every one of those announcements passes
|
|
766
|
+
# through this object — so stage boundaries can be timed here without
|
|
767
|
+
# threading a timer through optimizer/freq/tddft/nmr one by one.
|
|
768
|
+
self._stage_times: dict[str, float] = {}
|
|
769
|
+
self._stage_name: Optional[str] = None
|
|
770
|
+
self._stage_started_t = self._last_write_t
|
|
771
|
+
|
|
772
|
+
# ── Per-stage timing ────────────────────────────────────────────────────
|
|
773
|
+
|
|
774
|
+
@staticmethod
|
|
775
|
+
def _stage_key(message: str) -> str:
|
|
776
|
+
"""Collapse a live status message to a stable stage name.
|
|
777
|
+
|
|
778
|
+
Status messages carry per-step detail — "Opt step 7 — SCF…",
|
|
779
|
+
"Solving TD-DFT excited states (10)…" — which is exactly right for
|
|
780
|
+
the user watching the run and exactly wrong as a dictionary key: it
|
|
781
|
+
would turn one stage into one entry per step. Stripping the numbers
|
|
782
|
+
leaves the phase itself, which is the unit a cost model reasons in.
|
|
783
|
+
"""
|
|
784
|
+
text = _RE_STAGE_NUMBERS.sub(" ", message)
|
|
785
|
+
text = text.replace("…", " ").replace("(", " ").replace(")", " ")
|
|
786
|
+
text = " ".join(text.split())
|
|
787
|
+
return text.strip(" -—:·").lower()
|
|
788
|
+
|
|
789
|
+
def _enter_stage(self, name: str) -> None:
|
|
790
|
+
"""Close the stage in progress and start *name*.
|
|
791
|
+
|
|
792
|
+
Repeated announcements of the same stage (the optimizer re-announces
|
|
793
|
+
every step) are treated as one continuous stage, so the recorded
|
|
794
|
+
breakdown stays at the granularity a cost model can use rather than
|
|
795
|
+
exploding into one entry per step.
|
|
796
|
+
"""
|
|
797
|
+
now = time.monotonic()
|
|
798
|
+
if self._stage_name is not None and name != self._stage_name:
|
|
799
|
+
prev = self._stage_times.get(self._stage_name, 0.0)
|
|
800
|
+
self._stage_times[self._stage_name] = prev + (now - self._stage_started_t)
|
|
801
|
+
if name != self._stage_name:
|
|
802
|
+
self._stage_name = name
|
|
803
|
+
self._stage_started_t = now
|
|
804
|
+
|
|
805
|
+
def stage_timings(self) -> dict[str, float]:
|
|
806
|
+
"""Return ``{stage: seconds}``, including the stage still running.
|
|
807
|
+
|
|
808
|
+
Safe to call mid-run: the open stage is measured up to now rather
|
|
809
|
+
than omitted, so a caller that logs this at the end of a calc gets a
|
|
810
|
+
breakdown that actually sums to the run.
|
|
811
|
+
"""
|
|
812
|
+
out = dict(self._stage_times)
|
|
813
|
+
if self._stage_name is not None:
|
|
814
|
+
elapsed = time.monotonic() - self._stage_started_t
|
|
815
|
+
out[self._stage_name] = out.get(self._stage_name, 0.0) + elapsed
|
|
816
|
+
return out
|
|
754
817
|
|
|
755
818
|
# ── Silent-phase heartbeat ──────────────────────────────────────────────
|
|
756
819
|
|
|
@@ -824,8 +887,11 @@ class _LogCapture:
|
|
|
824
887
|
while "\n" in self._line_buf:
|
|
825
888
|
line, self._line_buf = self._line_buf.split("\n", 1)
|
|
826
889
|
m = _RE_Q_STATUS.search(line)
|
|
827
|
-
if m
|
|
828
|
-
|
|
890
|
+
if m:
|
|
891
|
+
message = m.group(1).strip()
|
|
892
|
+
self._enter_stage(self._stage_key(message))
|
|
893
|
+
if self._status is not None:
|
|
894
|
+
self._status.value = message
|
|
829
895
|
continue
|
|
830
896
|
m = _RE_CYCLE.search(line)
|
|
831
897
|
if m and self._status is not None:
|
|
@@ -854,6 +920,7 @@ class _LogCapture:
|
|
|
854
920
|
step k…") during silent (``verbose=0``) phases, without cluttering the
|
|
855
921
|
output log the way a ``[QuantUI_STATUS]`` stream line would.
|
|
856
922
|
"""
|
|
923
|
+
self._enter_stage(self._stage_key(message))
|
|
857
924
|
if self._status is not None:
|
|
858
925
|
try:
|
|
859
926
|
self._status.value = message
|
|
@@ -974,6 +1041,14 @@ class QuantUIApp:
|
|
|
974
1041
|
_reset_confirm_html: Any
|
|
975
1042
|
_reset_confirm_no: Any
|
|
976
1043
|
_reset_confirm_yes: Any
|
|
1044
|
+
_resume_cb: Any
|
|
1045
|
+
_resume_discard_btn: Any
|
|
1046
|
+
_resume_entries: Any
|
|
1047
|
+
_resume_list_box: Any
|
|
1048
|
+
_resume_list_dd: Any
|
|
1049
|
+
_resume_list_html: Any
|
|
1050
|
+
_resume_notice_html: Any
|
|
1051
|
+
_resume_restore_btn: Any
|
|
977
1052
|
_status_html: Any
|
|
978
1053
|
_status_tab_panel: Any
|
|
979
1054
|
_theme_style: Any
|
|
@@ -1230,6 +1305,12 @@ class QuantUIApp:
|
|
|
1230
1305
|
# The active run's _LogCapture, so the ticker can read the
|
|
1231
1306
|
# completion fraction calc modules report onto it. None between runs.
|
|
1232
1307
|
self._active_log: Optional[_LogCapture] = None
|
|
1308
|
+
# Calc types this session has already completed once. The first run
|
|
1309
|
+
# of a type pays import costs later ones don't (PySCF loads its
|
|
1310
|
+
# Hessian module on the first Frequency, for instance), so the
|
|
1311
|
+
# perf record carries a warm/cold flag rather than silently mixing
|
|
1312
|
+
# the two populations. See calc_log.log_calculation.
|
|
1313
|
+
self._warm_calc_types: set[str] = set()
|
|
1233
1314
|
# Relaxed molecule from a pending pre-opt preview, awaiting Keep/Revert.
|
|
1234
1315
|
self._preopt_relaxed_mol: Optional[Molecule] = None
|
|
1235
1316
|
# Cache kernel io_loop once on the main thread so worker threads can
|
|
@@ -1348,9 +1429,14 @@ class QuantUIApp:
|
|
|
1348
1429
|
if loop is not None:
|
|
1349
1430
|
loop.add_callback(self._refresh_results_browser)
|
|
1350
1431
|
loop.add_callback(self._populate_compare_list)
|
|
1432
|
+
# Startup is the moment that matters for CHK.6: after a restart
|
|
1433
|
+
# the targeted resume offer can't fire, because nothing is
|
|
1434
|
+
# configured yet.
|
|
1435
|
+
loop.add_callback(self._refresh_resume_list)
|
|
1351
1436
|
else:
|
|
1352
1437
|
self._refresh_results_browser()
|
|
1353
1438
|
self._populate_compare_list()
|
|
1439
|
+
self._refresh_resume_list()
|
|
1354
1440
|
|
|
1355
1441
|
def display(self) -> None:
|
|
1356
1442
|
"""Inject global CSS and render the application widget."""
|
|
@@ -1630,6 +1716,7 @@ class QuantUIApp:
|
|
|
1630
1716
|
("IR Spectrum", "_ir_accordion", "Frequency"),
|
|
1631
1717
|
("PES Scan", "_pes_scan_accordion", "PES Scan"),
|
|
1632
1718
|
("Isosurface", "_iso_accordion", "Single Point (Linux/WSL only)"),
|
|
1719
|
+
("Geometries", "_reorg_geom_accordion", "Reorganization Energy"),
|
|
1633
1720
|
("UV-Vis", "_tddft_accordion", "UV-Vis (TD-DFT)"),
|
|
1634
1721
|
("NMR", "_nmr_accordion", "NMR Shielding"),
|
|
1635
1722
|
]
|
|
@@ -1678,6 +1765,16 @@ class QuantUIApp:
|
|
|
1678
1765
|
"nmr": [
|
|
1679
1766
|
("NMR", "_pop_nmr_shielding", True),
|
|
1680
1767
|
],
|
|
1768
|
+
# reorganization_energy had NO entry at all until 2026-08-05, so the
|
|
1769
|
+
# Analysis tab populated nothing for these runs — not a missing panel,
|
|
1770
|
+
# no panels. Order matters twice over: the FIRST auto_select=True that
|
|
1771
|
+
# returns True wins, AND _pop_energies loads the orbital state
|
|
1772
|
+
# _pop_isosurface checks, so Energies must precede Isosurface.
|
|
1773
|
+
"reorganization_energy": [
|
|
1774
|
+
("Energies", "_pop_energies", False),
|
|
1775
|
+
("Geometries", "_pop_reorg_geometries", True),
|
|
1776
|
+
("Isosurface", "_pop_isosurface", False),
|
|
1777
|
+
],
|
|
1681
1778
|
"pes_scan": [
|
|
1682
1779
|
("PES Scan", "_pop_pes_plot", True),
|
|
1683
1780
|
("Trajectory", "_pop_pes_trajectory", False),
|
|
@@ -1693,6 +1790,9 @@ class QuantUIApp:
|
|
|
1693
1790
|
def _pop_energies(self, ctx: _AnalysisContext) -> bool:
|
|
1694
1791
|
return _ana_pop_energies(self, ctx)
|
|
1695
1792
|
|
|
1793
|
+
def _pop_reorg_geometries(self, ctx: _AnalysisContext) -> bool:
|
|
1794
|
+
return _ana_pop_reorg_geometries(self, ctx)
|
|
1795
|
+
|
|
1696
1796
|
def _pop_isosurface(self, ctx: _AnalysisContext) -> bool:
|
|
1697
1797
|
return _ana_pop_isosurface(self, ctx)
|
|
1698
1798
|
|
|
@@ -1881,6 +1981,12 @@ class QuantUIApp:
|
|
|
1881
1981
|
self.mult_si.observe(self._safe_cb(self._update_notes), names="value")
|
|
1882
1982
|
self.method_dd.observe(self._safe_cb(self._update_estimate), names="value")
|
|
1883
1983
|
self.basis_dd.observe(self._safe_cb(self._update_estimate), names="value")
|
|
1984
|
+
# Unfinished-calculations list (CHK.6)
|
|
1985
|
+
self._resume_list_dd.observe(
|
|
1986
|
+
self._safe_cb(self._on_resume_entry_changed), names="value"
|
|
1987
|
+
)
|
|
1988
|
+
self._resume_restore_btn.on_click(self._on_resume_restore)
|
|
1989
|
+
self._resume_discard_btn.on_click(self._on_resume_discard)
|
|
1884
1990
|
# Help buttons
|
|
1885
1991
|
self.method_help_btn.on_click(self._on_method_help)
|
|
1886
1992
|
self.basis_help_btn.on_click(self._on_basis_help)
|
|
@@ -2014,6 +2120,14 @@ class QuantUIApp:
|
|
|
2014
2120
|
# Cube + bundle exports
|
|
2015
2121
|
self._iso_export_cube_btn.on_click(self._on_iso_export_cube)
|
|
2016
2122
|
self._iso_cancel_btn.on_click(self._safe_cb(self._on_iso_cancel))
|
|
2123
|
+
# Reorg geometry views (REORG.3): both redraw from data already in
|
|
2124
|
+
# memory, so they respond directly rather than behind an Apply button.
|
|
2125
|
+
for _w in (
|
|
2126
|
+
self._reorg_view_toggle,
|
|
2127
|
+
self._reorg_overlay_pair,
|
|
2128
|
+
self._reorg_exaggerate,
|
|
2129
|
+
):
|
|
2130
|
+
_w.observe(self._safe_cb(self._on_reorg_view_changed), names="value")
|
|
2017
2131
|
# PNG capture arrives from the browser, so there is no button to bind
|
|
2018
2132
|
# here — the viewer's own Save-PNG button posts into this Textarea and
|
|
2019
2133
|
# ipywidgets syncs it back, firing this observer (ORBX.1).
|
|
@@ -3378,6 +3492,9 @@ class QuantUIApp:
|
|
|
3378
3492
|
def _on_export_pdb(self, btn) -> None:
|
|
3379
3493
|
_exp_on_export_pdb(self, btn)
|
|
3380
3494
|
|
|
3495
|
+
def _on_reorg_view_changed(self, change) -> None:
|
|
3496
|
+
_ana_on_reorg_view_changed(self, change)
|
|
3497
|
+
|
|
3381
3498
|
def _on_iso_cancel(self, btn) -> None:
|
|
3382
3499
|
_viz_on_iso_cancel(self, btn)
|
|
3383
3500
|
|
|
@@ -4387,6 +4504,7 @@ class QuantUIApp:
|
|
|
4387
4504
|
n_basis=_nb_for_est,
|
|
4388
4505
|
calc_type=_ct_for_est,
|
|
4389
4506
|
gpu_used=_predicted_gpu_used,
|
|
4507
|
+
source="app",
|
|
4390
4508
|
)
|
|
4391
4509
|
if _est is not None:
|
|
4392
4510
|
_predicted_run_s = float(_est["seconds"])
|
|
@@ -4460,6 +4578,28 @@ class QuantUIApp:
|
|
|
4460
4578
|
# and keep writing into a finished log.
|
|
4461
4579
|
log.start_heartbeat()
|
|
4462
4580
|
|
|
4581
|
+
# --- Checkpoint for this run (M-CHECKPOINT) ---
|
|
4582
|
+
# Opened before any calculation starts, because the runs worth
|
|
4583
|
+
# checkpointing are exactly the ones that never reach the end. Failure
|
|
4584
|
+
# to open one leaves ``_ckpt`` as None and the calc runs
|
|
4585
|
+
# uncheckpointed, which is the pre-M-CHECKPOINT behaviour.
|
|
4586
|
+
#
|
|
4587
|
+
# Deliberately after ``log`` exists: the checkpoint writes its own
|
|
4588
|
+
# provenance lines into the run log, and those lines are the only
|
|
4589
|
+
# record that a resumed run did not start from the geometry at the
|
|
4590
|
+
# top of the file.
|
|
4591
|
+
_ckpt = self._begin_run_checkpoint(log)
|
|
4592
|
+
# Resume only when there is genuinely something to continue. The
|
|
4593
|
+
# checkbox defaults to checked and is *hidden* when nothing is
|
|
4594
|
+
# resumable, so consulting it alone would ask every ordinary run to
|
|
4595
|
+
# resume — and the optimizer would answer with a "no usable
|
|
4596
|
+
# checkpoint" warning on a calculation the user started from scratch.
|
|
4597
|
+
_resume = bool(
|
|
4598
|
+
_ckpt is not None
|
|
4599
|
+
and self._resume_cb.value
|
|
4600
|
+
and getattr(self, "_checkpoint_resumable", False)
|
|
4601
|
+
)
|
|
4602
|
+
|
|
4463
4603
|
# The run header (structured banner) is written synchronously + atomically
|
|
4464
4604
|
# on the main thread by ``on_run_clicked`` → ``_write_run_header`` BEFORE
|
|
4465
4605
|
# this background thread starts. Writing it here (bg thread) instead was
|
|
@@ -4578,6 +4718,8 @@ class QuantUIApp:
|
|
|
4578
4718
|
expected_steps=(
|
|
4579
4719
|
int(round(_expected_steps)) if _expected_steps else None
|
|
4580
4720
|
),
|
|
4721
|
+
checkpoint=_ckpt,
|
|
4722
|
+
resume=_resume,
|
|
4581
4723
|
)
|
|
4582
4724
|
_sp_result = _run_required_final_single_point(
|
|
4583
4725
|
result.molecule,
|
|
@@ -4850,6 +4992,8 @@ class QuantUIApp:
|
|
|
4850
4992
|
stop=self._scan_stop.value,
|
|
4851
4993
|
steps=self._scan_steps.value,
|
|
4852
4994
|
progress_stream=log, # type: ignore[arg-type]
|
|
4995
|
+
checkpoint=_ckpt,
|
|
4996
|
+
resume=_resume,
|
|
4853
4997
|
)
|
|
4854
4998
|
result_html = self._format_pes_scan_result(result)
|
|
4855
4999
|
save_spectra = {
|
|
@@ -4905,6 +5049,7 @@ class QuantUIApp:
|
|
|
4905
5049
|
basis=self.basis_dd.value,
|
|
4906
5050
|
progress_stream=log, # type: ignore[arg-type]
|
|
4907
5051
|
solvent=_solvent,
|
|
5052
|
+
checkpoint=_ckpt,
|
|
4908
5053
|
)
|
|
4909
5054
|
result_html = self._format_result(result)
|
|
4910
5055
|
save_spectra, save_type = {}, "single_point"
|
|
@@ -5127,6 +5272,8 @@ class QuantUIApp:
|
|
|
5127
5272
|
_mark("perf_begin")
|
|
5128
5273
|
try:
|
|
5129
5274
|
_elapsed_for_est = time.perf_counter() - _run_wall_t
|
|
5275
|
+
_was_warm = save_type in self._warm_calc_types
|
|
5276
|
+
self._warm_calc_types.add(save_type)
|
|
5130
5277
|
_calc_log.log_calculation(
|
|
5131
5278
|
formula=result.formula,
|
|
5132
5279
|
n_atoms=len(calc_mol.atoms),
|
|
@@ -5144,6 +5291,9 @@ class QuantUIApp:
|
|
|
5144
5291
|
gpu_used=getattr(result, "gpu_used", None),
|
|
5145
5292
|
gpu_name=getattr(result, "gpu_name", None),
|
|
5146
5293
|
n_steps=getattr(result, "n_steps", None),
|
|
5294
|
+
source="app",
|
|
5295
|
+
warm=_was_warm,
|
|
5296
|
+
stages=log.stage_timings(),
|
|
5147
5297
|
)
|
|
5148
5298
|
_calc_log.log_event(
|
|
5149
5299
|
"calc_done",
|
|
@@ -5325,6 +5475,7 @@ class QuantUIApp:
|
|
|
5325
5475
|
"Tips: try a smaller basis set (STO-3G), use a geometry-optimized "
|
|
5326
5476
|
"structure first, or check for unusually long/short bonds in your "
|
|
5327
5477
|
"XYZ input. Full error details are in the <b>Output</b> tab.</small>"
|
|
5478
|
+
f"{self._resume_hint_html(_ckpt)}"
|
|
5328
5479
|
"</div>"
|
|
5329
5480
|
)
|
|
5330
5481
|
self.result_output.append_display_data(HTML(_err_html))
|
|
@@ -5347,6 +5498,7 @@ class QuantUIApp:
|
|
|
5347
5498
|
log.stop_heartbeat()
|
|
5348
5499
|
except Exception: # noqa: BLE001 — teardown must not mask a failure
|
|
5349
5500
|
pass
|
|
5501
|
+
self._finish_run_checkpoint(_ckpt)
|
|
5350
5502
|
self._activity_end(kind="compute")
|
|
5351
5503
|
|
|
5352
5504
|
# ── Live elapsed ticker ────────────────────────────────────────────────
|
|
@@ -5437,6 +5589,163 @@ class QuantUIApp:
|
|
|
5437
5589
|
def _update_notes(self, change=None) -> None:
|
|
5438
5590
|
_run_update_notes(self, change)
|
|
5439
5591
|
|
|
5592
|
+
def _begin_run_checkpoint(self, log_stream: Optional[Any] = None) -> Optional[Any]:
|
|
5593
|
+
"""Open a checkpoint for the run about to start, or return ``None``.
|
|
5594
|
+
|
|
5595
|
+
Returning ``None`` — no molecule, an unavailable checkpoint module, an
|
|
5596
|
+
unwritable directory — means the calculation runs without one. A
|
|
5597
|
+
checkpoint is an optimisation for the failure case; it must never be
|
|
5598
|
+
the reason a calculation doesn't start.
|
|
5599
|
+
"""
|
|
5600
|
+
try:
|
|
5601
|
+
from quantui.app_runflow import checkpoint_identity
|
|
5602
|
+
from quantui.checkpoint import Checkpoint
|
|
5603
|
+
|
|
5604
|
+
identity = checkpoint_identity(self)
|
|
5605
|
+
if identity is None:
|
|
5606
|
+
self._checkpoint_resumable = False
|
|
5607
|
+
return None
|
|
5608
|
+
ckpt = Checkpoint(identity, log_stream=log_stream)
|
|
5609
|
+
# Read resumability BEFORE begin() — begin() rewrites the metadata
|
|
5610
|
+
# with a fresh "running" status, so asking afterwards would
|
|
5611
|
+
# describe the run about to start rather than the one that stopped.
|
|
5612
|
+
self._checkpoint_resumable = ckpt.resumable_state() is not None
|
|
5613
|
+
extra: dict = {}
|
|
5614
|
+
if self.calc_type_dd.value == "PES Scan":
|
|
5615
|
+
# Lets the resume offer say "8 of 20" rather than just "8".
|
|
5616
|
+
extra["total_points"] = int(self._scan_steps.value)
|
|
5617
|
+
# Scan geometry isn't part of the checkpoint identity, so
|
|
5618
|
+
# without these a restore would reinstate the molecule and
|
|
5619
|
+
# method but silently leave a different scan range — and the
|
|
5620
|
+
# stored points, matched by coordinate value, would all miss.
|
|
5621
|
+
extra["settings"] = {
|
|
5622
|
+
"scan_type": self._scan_type_dd.value,
|
|
5623
|
+
"scan_atom1": int(self._scan_atom1.value),
|
|
5624
|
+
"scan_atom2": int(self._scan_atom2.value),
|
|
5625
|
+
"scan_atom3": int(self._scan_atom3.value),
|
|
5626
|
+
"scan_atom4": int(self._scan_atom4.value),
|
|
5627
|
+
"scan_start": float(self._scan_start.value),
|
|
5628
|
+
"scan_stop": float(self._scan_stop.value),
|
|
5629
|
+
"scan_steps": int(self._scan_steps.value),
|
|
5630
|
+
}
|
|
5631
|
+
if not ckpt.begin(**extra):
|
|
5632
|
+
return None
|
|
5633
|
+
return ckpt
|
|
5634
|
+
except Exception as exc: # noqa: BLE001 — never block a run
|
|
5635
|
+
self._checkpoint_resumable = False
|
|
5636
|
+
try:
|
|
5637
|
+
_calc_log.log_event(
|
|
5638
|
+
"checkpoint_unavailable", f"{type(exc).__name__}: {exc}"[:200]
|
|
5639
|
+
)
|
|
5640
|
+
except Exception: # noqa: BLE001 — telemetry self-guard
|
|
5641
|
+
pass
|
|
5642
|
+
return None
|
|
5643
|
+
|
|
5644
|
+
def _refresh_resume_list(self) -> None:
|
|
5645
|
+
"""Rebuild the History tab's unfinished-calculations list."""
|
|
5646
|
+
try:
|
|
5647
|
+
from quantui.app_runflow import refresh_resume_list
|
|
5648
|
+
|
|
5649
|
+
refresh_resume_list(self)
|
|
5650
|
+
except Exception: # noqa: BLE001 — never break the History tab
|
|
5651
|
+
pass
|
|
5652
|
+
|
|
5653
|
+
def _on_resume_entry_changed(self, _change=None) -> None:
|
|
5654
|
+
from quantui.app_runflow import describe_resume_entry
|
|
5655
|
+
|
|
5656
|
+
describe_resume_entry(self, _change)
|
|
5657
|
+
|
|
5658
|
+
def _on_resume_restore(self, _btn=None) -> None:
|
|
5659
|
+
"""Load the selected checkpoint's settings and go to Calculate."""
|
|
5660
|
+
from quantui.app_runflow import restore_resume_entry
|
|
5661
|
+
|
|
5662
|
+
if not restore_resume_entry(self):
|
|
5663
|
+
return
|
|
5664
|
+
# Send the user where the settings just landed. Restoring without
|
|
5665
|
+
# moving them leaves the effect invisible on a tab they aren't
|
|
5666
|
+
# looking at, which reads as the button having done nothing.
|
|
5667
|
+
try:
|
|
5668
|
+
self.root_tab.selected_index = 1
|
|
5669
|
+
except Exception: # noqa: BLE001 — tab index is cosmetic
|
|
5670
|
+
pass
|
|
5671
|
+
|
|
5672
|
+
def _on_resume_discard(self, _btn=None) -> None:
|
|
5673
|
+
"""Delete the selected checkpoint and refresh the list."""
|
|
5674
|
+
try:
|
|
5675
|
+
import shutil
|
|
5676
|
+
|
|
5677
|
+
selected = self._resume_list_dd.value
|
|
5678
|
+
if selected and selected in (getattr(self, "_resume_entries", None) or {}):
|
|
5679
|
+
shutil.rmtree(selected, ignore_errors=True)
|
|
5680
|
+
except Exception: # noqa: BLE001 — discarding is best-effort
|
|
5681
|
+
pass
|
|
5682
|
+
self._refresh_resume_list()
|
|
5683
|
+
try:
|
|
5684
|
+
from quantui.app_runflow import refresh_resume_notice
|
|
5685
|
+
|
|
5686
|
+
refresh_resume_notice(self)
|
|
5687
|
+
except Exception: # noqa: BLE001 — a stale notice is not fatal
|
|
5688
|
+
pass
|
|
5689
|
+
|
|
5690
|
+
def _resume_hint_html(self, ckpt: Optional[Any]) -> str:
|
|
5691
|
+
"""Return a "you can resume this" line for the failure card, or ``""``.
|
|
5692
|
+
|
|
5693
|
+
The resume offer itself lives up by the Run button, which is not where
|
|
5694
|
+
anyone is looking after a calculation fails. Saying it here, next to
|
|
5695
|
+
the error, is the difference between the feature being discovered and
|
|
5696
|
+
it quietly never being used.
|
|
5697
|
+
"""
|
|
5698
|
+
try:
|
|
5699
|
+
if ckpt is None or ckpt.resumable_state() is None:
|
|
5700
|
+
return ""
|
|
5701
|
+
points = len(ckpt.completed_points())
|
|
5702
|
+
done = f"{points} completed scan point{'s' if points != 1 else ''}"
|
|
5703
|
+
if not points:
|
|
5704
|
+
done = "the steps completed so far"
|
|
5705
|
+
return (
|
|
5706
|
+
'<br><br><small style="color:#991b1b">'
|
|
5707
|
+
f"♻ <b>This run can be resumed.</b> {done} "
|
|
5708
|
+
"were saved. Leave the settings as they are and press "
|
|
5709
|
+
"<b>Run</b> again — the <b>Resume from checkpoint</b> box "
|
|
5710
|
+
"above the Run button is already ticked.</small>"
|
|
5711
|
+
)
|
|
5712
|
+
except Exception: # noqa: BLE001 — a hint must never mask the error
|
|
5713
|
+
return ""
|
|
5714
|
+
|
|
5715
|
+
def _finish_run_checkpoint(self, ckpt: Optional[Any]) -> None:
|
|
5716
|
+
"""Close out *ckpt* after a run ends, however it ended.
|
|
5717
|
+
|
|
5718
|
+
Runs from the ``finally`` of ``_do_run``, so it is reached on success,
|
|
5719
|
+
on failure and on cancel. It deliberately does **not** decide whether
|
|
5720
|
+
the run succeeded — the calc modules mark completion themselves, since
|
|
5721
|
+
only they know whether "finished" means converged, and a run that
|
|
5722
|
+
stopped early must keep its resumable state.
|
|
5723
|
+
|
|
5724
|
+
What happens here is bookkeeping: refresh the resume offer so it
|
|
5725
|
+
reflects reality, and prune old checkpoints so the directory doesn't
|
|
5726
|
+
grow without bound.
|
|
5727
|
+
"""
|
|
5728
|
+
try:
|
|
5729
|
+
if ckpt is not None and self._cancel_event.is_set():
|
|
5730
|
+
ckpt.mark_interrupted()
|
|
5731
|
+
except Exception: # noqa: BLE001 — teardown must not mask a failure
|
|
5732
|
+
pass
|
|
5733
|
+
try:
|
|
5734
|
+
from quantui.checkpoint import prune
|
|
5735
|
+
|
|
5736
|
+
prune()
|
|
5737
|
+
except Exception: # noqa: BLE001 — retention is best-effort
|
|
5738
|
+
pass
|
|
5739
|
+
try:
|
|
5740
|
+
from quantui.app_runflow import refresh_resume_notice
|
|
5741
|
+
|
|
5742
|
+
refresh_resume_notice(self)
|
|
5743
|
+
except Exception: # noqa: BLE001 — a stale notice is not fatal
|
|
5744
|
+
pass
|
|
5745
|
+
# A run that just failed becomes a new listing entry; one that
|
|
5746
|
+
# succeeded removes itself. Either way the list is now stale.
|
|
5747
|
+
self._refresh_resume_list()
|
|
5748
|
+
|
|
5440
5749
|
def _update_estimate(self, change=None) -> None:
|
|
5441
5750
|
_run_update_estimate(self, calc_log_mod=_calc_log, change=change)
|
|
5442
5751
|
|