diff --git a/quantui/app.py b/quantui/app.py
index cc3b772..4683a21 100644
--- a/quantui/app.py
+++ b/quantui/app.py
@@ -829,7 +829,7 @@ def _layout(**kwargs: Any) -> widgets.Layout:
)
_RE_CONV = re.compile(r"converged SCF energy\s*=\s*([\-\d\.]+)")
_RE_Q_STATUS = re.compile(r"\[QuantUI_STATUS\]\s*(.+)")
-# TD-DFT root convergence (M-PROGRESS D2). PySCF's Davidson solver prints
+# TD-DFT root convergence. PySCF's Davidson solver prints
# "root %d converged |r|= ... e= max|de|= ..." at
# verbose=5 (DEBUG) — see tddft_calc.py's td.verbose. This is the only
# per-root progress signal the solve emits; without it the heartbeat's
@@ -838,7 +838,7 @@ def _layout(**kwargs: Any) -> widgets.Layout:
_RE_TD_ROOT = re.compile(
r"root\s+(\d+)\s+converged\s+\|r\|=\s*[\d.eE+\-]+\s+e=\s*([\d.eE+\-]+)"
)
-# M-PROGRESS D3 — per-phase progress for long silent kernels (mirrors D2).
+# Per-phase progress for long silent kernels (NMR, Hessian, post-HF, TD-DFT).
_RE_CCSD_CYCLE = re.compile(
r"cycle\s*=\s*(\d+)\s+E_corr\(CCSD\)\s*=\s*[\-\d\.Ee+\-]+\s+dE\s*=\s*([\-\d\.Ee+\-]+)"
)
@@ -852,7 +852,7 @@ def _layout(**kwargs: Any) -> widgets.Layout:
# message is used as a per-stage timing key — see _LogCapture._stage_key.
_RE_STAGE_NUMBERS = re.compile(r"\d+(?:[./]\d+)*")
-# ── Silent-phase heartbeat (M-PROGRESS Phase D) ──────────────────────────────
+# ── Silent-phase heartbeat ───────────────────────────────────────────────────
#
# Seconds of stream silence before the log says it is still alive.
#
@@ -906,19 +906,18 @@ def __init__(
# Completion fraction (0..1) reported by calc modules via
# log_utils.emit_progress; read by the elapsed ticker. None = unknown.
self._fraction: Optional[float] = None
- # Silent-phase heartbeat (M-PROGRESS Phase D). Long kernels — the TD-DFT
- # excited-state solve most of all — print nothing for minutes, so the
- # log looks hung even though the status label is advancing. A watchdog
- # appends a "still working" line when the stream has gone quiet.
+ # Silent-phase heartbeat. Long kernels — the TD-DFT excited-state solve
+ # most of all — print nothing for minutes, so the log looks hung even
+ # though the status label is advancing. A watchdog appends a "still
+ # working" line when the stream has gone quiet.
self._last_write_t = time.monotonic()
self._hb_started_t = self._last_write_t
self._hb_stop = threading.Event()
self._hb_thread: Optional[threading.Thread] = None
- # Per-stage wall times (M-PROGRESS Phase C, deferred from B3).
- # Every calc type already announces its phases through
- # log_utils.emit_status, and every one of those announcements passes
- # through this object — so stage boundaries can be timed here without
- # threading a timer through optimizer/freq/tddft/nmr one by one.
+ # Per-stage wall times. Every calc type announces its phases through
+ # log_utils.emit_status, and every announcement passes through this
+ # object — stage boundaries can be timed here without threading a timer
+ # through optimizer/freq/tddft/nmr one by one.
self._stage_times: dict[str, float] = {}
self._stage_name: Optional[str] = None
self._stage_started_t = self._last_write_t
@@ -2410,9 +2409,9 @@ def _wire_callbacks(self) -> None:
self._safe_cb(self._on_calc_type_changed), names="value"
)
# Geometry Opt / Frequency / UV-Vis (TD-DFT) share one seed-geometry
- # dropdown + refresh button (UXP2.5, M-UX2) — only one observer/click
- # binding is needed, not three, since `_geo_seed_dd`, `_freq_seed_dd`
- # and `_tddft_seed_dd` are the same underlying widget.
+ # dropdown + refresh button — only one observer/click binding is needed,
+ # not three, since `_geo_seed_dd`, `_freq_seed_dd` and `_tddft_seed_dd`
+ # are the same underlying widget.
self._seed_dd.observe(self._safe_cb(self._on_seed_changed), names="value")
self._seed_refresh_btn.on_click(lambda _btn: self._refresh_seed_options())
self._scan_type_dd.observe(
@@ -4036,7 +4035,7 @@ def _on_seed_changed(self, change) -> None:
# Geometry Opt / Frequency / UV-Vis (TD-DFT) used to each have their own
# seed-refresh + seed-changed methods; the widget they operate on is now
- # one shared dropdown (UXP2.5, M-UX2), so these are aliases of the two
+ # one shared dropdown, so these are aliases of the two methods above rather
# methods above rather than separate implementations. Kept under their
# original names because app_runflow.py's per-calc-type branches and the
# existing per-calc-type tests still call them by these names.
@@ -4931,7 +4930,7 @@ def _set_molecule(
#
# This used to refresh only the Frequency/UV-Vis dropdowns, because
# Geometry Opt's seed dropdown didn't exist yet when this was written.
- # Now that all three calc types share one dropdown (UXP2.5), a single
+ # Now that all three calc types share one dropdown, a single call here
# call here also fixes a real gap: switching molecules while already
# on the Geometry Opt panel used to leave its seed list showing the
# PREVIOUS molecule's matches until the user switched calc types away
@@ -5378,9 +5377,9 @@ def _run_required_final_single_point(target_mol, reason: str):
# Expose this run's log to the elapsed ticker so it can read the
# completion fraction calc modules report via emit_progress.
self._active_log = log
- # Watch for silent stretches (M-PROGRESS Phase D). Stopped in the
- # `finally` alongside the elapsed ticker, so it cannot outlive the run
- # and keep writing into a finished log.
+ # Watch for silent stretches in the output log. Stopped in the `finally`
+ # alongside the elapsed ticker, so it cannot outlive the run and keep
+ # writing into a finished log.
log.start_heartbeat()
# --- Checkpoint for this run (M-CHECKPOINT) ---
diff --git a/quantui/app_builders.py b/quantui/app_builders.py
index 47ad96c..0ca2d22 100644
--- a/quantui/app_builders.py
+++ b/quantui/app_builders.py
@@ -1131,9 +1131,8 @@ def build_shared_widgets(
# molecule (e.g. optimise cheaply, then refine at a higher level of
# theory). Only one of these three calc types is ever visible at a time,
# so ONE widget group is built here and referenced under all three
- # historical per-calc-type attribute names below (UXP2.5, M-UX2,
- # 2026-07-31) — this used to be three near-identical widget groups.
- # Real object count is one; the aliases exist so app.py, app_runflow.py,
+ # historical per-calc-type attribute names below — this used to be three
+ # near-identical widget groups. Real object count is one; the aliases exist so app.py, app_runflow.py,
# and the existing per-calc-type tests keep working unchanged.
app._seed_dd = widgets.Dropdown(
options=[("(use current molecule)", "")],
@@ -2651,7 +2650,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox:
app._nmr_output = app._nmr_summary
# Mulliken Populations — table + Plotly bar chart + 3D overlay toggles
- # (always-mounted; content swapped when activated, DEC-009).
+ # (always-mounted; content swapped when activated).
app._mulliken_summary = widgets.HTML(value="", layout=layout_fn(width="100%"))
app._mulliken_table = widgets.HTML(value="", layout=layout_fn(width="100%"))
# min_height matches plot_mulliken_charges height so the Output does not
@@ -2889,7 +2888,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox:
),
]
)
- # DEC-009: the panel stays visible regardless of backend — only its
+ # The panel stays visible regardless of backend — only its
# content swaps (MEAS.6). Shown when the resolved backend can't support
# native clicking (plotlymol, or py3Dmol simply unavailable).
app._measure_fallback_msg = widgets.HTML(
diff --git a/quantui/app_measurement.py b/quantui/app_measurement.py
index c22d427..58baed6 100644
--- a/quantui/app_measurement.py
+++ b/quantui/app_measurement.py
@@ -384,7 +384,7 @@ def reset_picks(app: Any) -> None:
def update_panel_for_backend(app: Any, backend: Any) -> None:
"""Swap the measurement panel between controls and an explanation.
- DEC-009: the panel itself stays visible either way — only its *content*
+ The panel itself stays visible either way — only its *content*
switches, never a silent no-op when the resolved backend can't support
clicking.
"""
diff --git a/quantui/app_runflow.py b/quantui/app_runflow.py
index 35185ff..55e0aca 100644
--- a/quantui/app_runflow.py
+++ b/quantui/app_runflow.py
@@ -404,7 +404,7 @@ def on_calc_type_changed(app: Any, change: Any, *, layout_fn: Any) -> None:
elif ct in ("Frequency", "UV-Vis (TD-DFT)"):
app._freq_preopt_cb.layout.display = ""
# The seed dropdown is shared across all three seed-consuming calc
- # types (UXP2.5), so it can carry a value in from whichever of these
+ # types, so it can carry a value in from whichever of these
# two was active before. Re-evaluate .disabled here rather than trust
# whatever it was left at — otherwise switching Frequency (seeded) ->
# UV-Vis carries a stale disabled=True even if UV-Vis's own seed slot
@@ -930,7 +930,7 @@ def refresh_seed_options(app: Any) -> None:
Used by Geometry Opt, Frequency, UV-Vis (TD-DFT), and NMR Shielding —
only one of which is ever visible at a time, so there is exactly one
- dropdown to refresh (UXP2.5, M-UX2). Superseded the three near-identical
+ dropdown to refresh. Superseded the three near-identical
``refresh_{geo,freq,tddft}_seed_options`` wrappers that used to exist here.
"""
include_freq = getattr(app, "calc_type_dd", None)
@@ -943,7 +943,7 @@ def on_seed_changed(app: Any, change: Any) -> None:
redundant.
Superseded the three near-identical ``on_{geo,freq,tddft}_seed_changed``
- handlers (UXP2.5, M-UX2) — the dropdown is now one shared widget, so one
+ handlers — the dropdown is now one shared widget, so one
handler suffices, made calc-type-aware where the three used to differ:
- **Frequency / UV-Vis (TD-DFT):** a selected seed is already an optimised
diff --git a/quantui/calc_log.py b/quantui/calc_log.py
index e1a1238..13b850e 100644
--- a/quantui/calc_log.py
+++ b/quantui/calc_log.py
@@ -477,9 +477,9 @@ def log_calculation(
``quantui.analytics.build_dashboard`` compute GPU-vs-CPU speedups
across runs of the same (method, basis, formula) tuple.
- ``source`` / ``warm`` / ``import_s`` / ``stages`` (added 2026-08-05,
- M-PROGRESS Phase C) describe **how the timing was measured**, which
- turned out to matter more than the cost model itself:
+ ``source`` / ``warm`` / ``import_s`` / ``stages`` (added 2026-08-05)
+ describe **how the timing was measured**, which turned out to matter more
+ than the cost model itself:
* ``source`` — ``"app"`` for a run the user launched in the UI,
``"calibration"`` for one the benchmark harness measured in a fresh
@@ -554,8 +554,8 @@ def log_calculation(
_HESSIAN_MULTIPLIER_HF_DFT: float = 2.0
_HESSIAN_MULTIPLIER_POST_HF: float = 6.0
_POST_HF_METHODS: frozenset = frozenset({"MP2", "CCSD", "CCSD(T)"})
-# Minimum tagged frequency records carrying ``stages`` before the learned
-# ratios replace the fixed Hessian / IR multipliers (PROG.C6).
+# Minimum tagged frequency records carrying ``stages`` before learned stage
+# ratios replace the fixed Hessian / IR multipliers.
_MIN_FREQ_STAGE_SAMPLES: int = 2
@@ -708,8 +708,8 @@ def _estimate_frequency_cost(
return None
scf_anchor_s = float(sp_est["seconds"])
- # PROG.C6 — replace fixed multipliers with medians from tagged stage
- # records when enough ``source="app"`` frequency runs have accumulated.
+ # Replace fixed multipliers with medians from tagged stage records when
+ # enough ``source="app"`` frequency runs have accumulated.
learned = _learned_frequency_stage_params(
records, method=method, basis=basis, source=source
)
@@ -868,7 +868,7 @@ def estimate_time_from_records(
label downgraded one notch — better an approximate estimate from
cross-device data than no estimate at all.
- **Provenance filtering** (2026-08-05, M-PROGRESS Phase C): ``source``
+ **Provenance filtering** (2026-08-05): ``source`` partitions the pool the
partitions the pool the same way ``gpu_used`` does, and for the same
reason — the two populations measure different things. A calibration
record times a fresh subprocess, so it includes PySCF's import cost;
diff --git a/quantui/engines/pyfock_engine.py b/quantui/engines/pyfock_engine.py
index 03eb240..13495a2 100644
--- a/quantui/engines/pyfock_engine.py
+++ b/quantui/engines/pyfock_engine.py
@@ -15,7 +15,7 @@
_PYFOCK_SPEC = importlib.util.find_spec("pyfock")
_AVAILABLE = _PYFOCK_SPEC is not None
-# Phase-1 subset — see QUANTUM-ENGINE-CONTRACT.md and DEC-020.
+# Phase-1 subset — see QUANTUM-ENGINE-CONTRACT.md.
_PYFOCK_METHODS = ("PBE", "B3LYP", "PBE0")
_PYFOCK_BASES = ("def2-SVP", "def2-TZVP", "LANL2DZ")
diff --git a/quantui/estimator_eval.py b/quantui/estimator_eval.py
index 20be53d..a1c28fc 100644
--- a/quantui/estimator_eval.py
+++ b/quantui/estimator_eval.py
@@ -1,9 +1,8 @@
"""
Offline evaluation of QuantUI's runtime estimator.
-M-PROGRESS Phase C asked for the pre-run time estimate to be "overhauled"
-until it is trustworthy. That is only actionable if a change to the model
-can be *scored*, so this module replays the recorded performance history
+The pre-run time estimate is only actionable if a change to the model can be
+*scored*, so this module replays the recorded performance history
through :func:`quantui.calc_log.estimate_time_from_records` and reports how
well the predictor would have done.
diff --git a/quantui/freq_calc.py b/quantui/freq_calc.py
index 28fd6a3..5a59140 100644
--- a/quantui/freq_calc.py
+++ b/quantui/freq_calc.py
@@ -496,8 +496,8 @@ def _status(msg: str) -> None:
try:
hess_obj = mf.Hessian()
# verbose=6 surfaces per-atom integral contractions — the only
- # in-kernel progress signal during an analytical Hessian build
- # (M-PROGRESS D3). _LogCapture.write greps for them.
+ # in-kernel progress signal during an analytical Hessian build.
+ # _LogCapture.write greps for per-atom contraction lines.
hess_obj.verbose = 6
hess_obj.stdout = stream
diff --git a/quantui/help_content.py b/quantui/help_content.py
index cd33f5f..f0aa26f 100644
--- a/quantui/help_content.py
+++ b/quantui/help_content.py
@@ -48,6 +48,44 @@
"apptainer run quantui.sif
"
"Each dropdown in the Calculate tab has a ? button for "
"context-sensitive help on that specific option.
"
+ "While a calculation runs, see Live run progress in the "
+ "Help browser for what the status line and log messages mean.
"
+ ),
+ },
+ "run_progress": {
+ "title": "Live run progress — what the status line means",
+ "body": (
+ "Long quantum-chemistry steps can run for minutes with little or "
+ "no new text in the output log. QuantUI keeps you oriented with a "
+ "live status line next to the Run button and, when needed, "
+ "occasional still working lines in the log.
"
+ "Status chip — shows the current stage, elapsed time, and "
+ "(when history allows) an approximate time remaining. Examples you "
+ "may see:
"
+ ""
+ "- SCF cycle N — the self-consistent field iteration "
+ "is still converging the electron density.
"
+ "- TD-DFT root k converged — excited-state "
+ "calculations report each converged root as it lands.
"
+ "- NMR GIAO · atom k/N — shielding tensors "
+ "are computed one atom at a time.
"
+ "- Hessian build · atom k — frequency calculations "
+ "building the force-constant matrix.
"
+ "- CCSD cycle N or MP2 · … — post-HF "
+ "correlation steps after SCF finishes.
"
+ "- Opt step k / Scan point k — "
+ "geometry optimizations and PES scans advancing through their "
+ "outer loop.
"
+ "
"
+ "Log heartbeat — if PySCF prints nothing for ~25 seconds, "
+ "the live log may show a grey … still working — <stage> · "
+ "<elapsed> line. This proves the run is alive; those lines "
+ "are not copied into the saved pyscf.log in your "
+ "results folder.
"
+ "When in doubt: as long as the status chip or heartbeat is "
+ "advancing, the calculation is progressing. Use Cancel if you "
+ "need to stop — cancellation is checked between SCF cycles and "
+ "optimizer steps.
"
),
},
"calc_type": {
@@ -182,7 +220,7 @@
"ferrocene) or paste your own coordinates in the XYZ Input "
"tab rather than relying on an online name search, which often "
"returns a disconnected salt form for coordination compounds."
- # UXP2.1: the two Pople notations are a recurring source of
+ # The two Pople notations are a recurring source of
# confusion — a reader who only knows 6-31G(d) can conclude the
# 6-31G* in the dropdown is a different set they can't select.
"Reading the names: two notations, "
diff --git a/quantui/log_utils.py b/quantui/log_utils.py
index 8c316b9..2399db9 100644
--- a/quantui/log_utils.py
+++ b/quantui/log_utils.py
@@ -406,7 +406,6 @@ def _extract_warnings(
(> ``_DEGENERACY_GAP_THRESHOLD_EV``), such lines are dropped as transient:
they describe a pre-convergence density, not the result. A genuinely
(near-)degenerate converged state has a small gap and keeps its warning.
- (M-UX2 UXP2.6.)
"""
drop_degeneracy = (
converged_gap_ev is not None and converged_gap_ev > _DEGENERACY_GAP_THRESHOLD_EV
diff --git a/quantui/nmr_calc.py b/quantui/nmr_calc.py
index ff9c8a6..68444e3 100644
--- a/quantui/nmr_calc.py
+++ b/quantui/nmr_calc.py
@@ -411,7 +411,7 @@ def _run_nmr_calc_body(
else:
nmr_obj = _pyscf_nmr.RKS(mf) if mol.spin == 0 else _pyscf_nmr.UKS(mf)
# verbose=4 prints per-atom shielding blocks — parsed by _LogCapture
- # for live GIAO progress (M-PROGRESS D3).
+ # for live GIAO progress. _LogCapture.write greps for per-atom lines.
nmr_obj.verbose = 4
nmr_obj.stdout = stream
tensors = nmr_obj.kernel()
diff --git a/quantui/session_calc.py b/quantui/session_calc.py
index f6b1d6b..143ff26 100644
--- a/quantui/session_calc.py
+++ b/quantui/session_calc.py
@@ -580,7 +580,7 @@ def _run_session_calc_body(
emit_status(stream, "Running MP2 correlation…")
_mp2 = _mp.MP2(mf)
# verbose=5 surfaces integral-transform / kernel milestones for
- # the live status label (M-PROGRESS D3).
+ # the live status label during the correlation step.
_mp2.verbose = 5
_mp2.stdout = stream
_e_corr, _ = _mp2.kernel()
diff --git a/quantui/tddft_calc.py b/quantui/tddft_calc.py
index 13363a3..57c940b 100644
--- a/quantui/tddft_calc.py
+++ b/quantui/tddft_calc.py
@@ -290,8 +290,8 @@ def _run_tddft_calc_body(
td.nstates = nstates
# verbose=5 (DEBUG) is what surfaces PySCF's per-root "root %d
# converged" lines during the Davidson solve — the only progress
- # signal available while it runs (M-PROGRESS D2). _LogCapture.write
- # in app.py greps for them to update the live status label.
+ # signal available while it runs. _LogCapture.write in app.py greps
+ # for per-root convergence lines to update the live status label.
td.verbose = 5
td.stdout = stream
td.kernel()
diff --git a/quantui/theme.py b/quantui/theme.py
index e0e6076..00a5558 100644
--- a/quantui/theme.py
+++ b/quantui/theme.py
@@ -1,4 +1,4 @@
-"""Theme colour tokens and preset palettes (M-THEME).
+"""Theme colour tokens and preset palettes.
QuantUI themes are **preset palettes** backed by CSS custom properties. Each
palette defines independent light/dark (or tinted) values — the whole-page
diff --git a/quantui/user_settings.py b/quantui/user_settings.py
index 46bc32b..09d7cdc 100644
--- a/quantui/user_settings.py
+++ b/quantui/user_settings.py
@@ -80,7 +80,7 @@ class VizSettings:
@dataclass
class ThemeSettings:
- """Visual theme preferences (M-THEME THEME.6)."""
+ """Visual theme preferences (preset palette selection)."""
palette: str = "Dark"
diff --git a/tests/test_help_content.py b/tests/test_help_content.py
index 82ae210..63b9633 100644
--- a/tests/test_help_content.py
+++ b/tests/test_help_content.py
@@ -82,3 +82,11 @@ def test_method_panel_mentions_rhf_uhf(self):
def test_basis_set_panel_mentions_sto3g(self):
panel = help_panel("basis_set")
assert "STO-3G" in panel.value
+
+ def test_run_progress_panel_covers_status_and_heartbeat(self):
+ panel = help_panel("run_progress")
+ assert "status" in panel.value.lower()
+ assert (
+ "heartbeat" in panel.value.lower() or "still working" in panel.value.lower()
+ )
+ assert "TD-DFT" in panel.value