Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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= <excitation energy, Ha> 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
Expand All @@ -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+\-]+)"
)
Expand All @@ -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.
#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) ---
Expand Down
9 changes: 4 additions & 5 deletions quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)", "")],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion quantui/app_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
6 changes: 3 additions & 3 deletions quantui/app_runflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions quantui/calc_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion quantui/engines/pyfock_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
5 changes: 2 additions & 3 deletions quantui/estimator_eval.py
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
4 changes: 2 additions & 2 deletions quantui/freq_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 39 additions & 1 deletion quantui/help_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,44 @@
"<code>apptainer run quantui.sif</code></p>"
"<p>Each dropdown in the Calculate tab has a <b>?</b> button for "
"context-sensitive help on that specific option.</p>"
"<p>While a calculation runs, see <i>Live run progress</i> in the "
"Help browser for what the status line and log messages mean.</p>"
),
},
"run_progress": {
"title": "Live run progress — what the status line means",
"body": (
"<p>Long quantum-chemistry steps can run for minutes with little or "
"no new text in the output log. QuantUI keeps you oriented with a "
"<b>live status line</b> next to the Run button and, when needed, "
"occasional <i>still working</i> lines in the log.</p>"
"<p><b>Status chip</b> — shows the current stage, elapsed time, and "
"(when history allows) an approximate time remaining. Examples you "
"may see:</p>"
"<ul>"
"<li><b>SCF cycle <i>N</i></b> — the self-consistent field iteration "
"is still converging the electron density.</li>"
"<li><b>TD-DFT root <i>k</i> converged</b> — excited-state "
"calculations report each converged root as it lands.</li>"
"<li><b>NMR GIAO · atom <i>k</i>/<i>N</i></b> — shielding tensors "
"are computed one atom at a time.</li>"
"<li><b>Hessian build · atom <i>k</i></b> — frequency calculations "
"building the force-constant matrix.</li>"
"<li><b>CCSD cycle <i>N</i></b> or <b>MP2 · …</b> — post-HF "
"correlation steps after SCF finishes.</li>"
"<li><b>Opt step <i>k</i></b> / <b>Scan point <i>k</i></b> — "
"geometry optimizations and PES scans advancing through their "
"outer loop.</li>"
"</ul>"
"<p><b>Log heartbeat</b> — if PySCF prints nothing for ~25 seconds, "
"the live log may show a grey <i>… still working — &lt;stage&gt; · "
"&lt;elapsed&gt;</i> line. This proves the run is alive; those lines "
"are <i>not</i> copied into the saved <code>pyscf.log</code> in your "
"results folder.</p>"
"<p><b>When in doubt:</b> as long as the status chip or heartbeat is "
"advancing, the calculation is progressing. Use <b>Cancel</b> if you "
"need to stop — cancellation is checked between SCF cycles and "
"optimizer steps.</p>"
),
},
"calc_type": {
Expand Down Expand Up @@ -182,7 +220,7 @@
"ferrocene) or paste your own coordinates in the <b>XYZ Input</b> "
"tab rather than relying on an online name search, which often "
"returns a disconnected salt form for coordination compounds.</p>"
# 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.
"<h4 style='margin:14px 0 4px'>Reading the names: two notations, "
Expand Down
1 change: 0 additions & 1 deletion quantui/log_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion quantui/nmr_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion quantui/session_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions quantui/tddft_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion quantui/theme.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion quantui/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 8 additions & 0 deletions tests/test_help_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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