From 847755c3324aea04e260fe970abae6a396828ef3 Mon Sep 17 00:00:00 2001 From: NCCU-Schultz-Lab Date: Sat, 18 Jul 2026 19:04:50 -0400 Subject: [PATCH 1/3] Use fraction-based ETA in live progress chip Add B2 progress plumbing so live remaining-time estimates can use real completion fractions when available. `_LogCapture` now stores a clamped progress fraction, `log_utils.emit_progress()` duck-types to streams that support it, and the elapsed chip prefers a self-correcting fraction-based ETA before falling back to the prior static estimate. Wire fraction reporting into PES scans (exact point completion) and geometry optimization (fmax convergence trend), and disable optimizer fraction reporting for reorganization-energy sub-optimizations to avoid oscillating ETA resets. Extend app tests to cover fraction emission/clamping, no-op behavior on plain streams, and ETA selection/fallback behavior. --- quantui/app.py | 41 ++++++++++++++++++++++++++ quantui/log_utils.py | 18 ++++++++++++ quantui/optimizer.py | 30 +++++++++++++++++++ quantui/pes_scan.py | 6 ++-- quantui/reorganization_energy.py | 2 ++ tests/test_app.py | 49 ++++++++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/quantui/app.py b/quantui/app.py index 882dbe9..225b4f2 100644 --- a/quantui/app.py +++ b/quantui/app.py @@ -702,6 +702,9 @@ def __init__( # Public alias so calc modules can duck-type the predicate off the # progress stream (see quantui.cancellation.cancel_check_from_stream). self.cancel_check = cancel_check + # B2: 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 def write(self, text: str) -> None: if not text: @@ -752,6 +755,21 @@ def set_status(self, message: str) -> None: except Exception: pass + def set_progress_fraction(self, fraction: float) -> None: + """Record a completion fraction (0..1) for the live remaining-time chip. + + M-PROGRESS B2: calc modules call this via ``log_utils.emit_progress`` + when they know a real completion fraction (PES points, optimizer fmax + trend). The elapsed ticker reads it off the active log and prefers a + self-correcting ``elapsed·(1−f)/f`` estimate over the B1 static total. + """ + try: + f = float(fraction) + except (TypeError, ValueError): + return + # Clamp below 1.0 so the chip never claims "0s left" while work remains. + self._fraction = max(0.0, min(f, 0.999)) + def flush(self) -> None: pass @@ -1091,6 +1109,9 @@ def __init__(self) -> None: # remaining"; set by _do_run once estimate_time() has run. self._run_estimate_s: Optional[float] = None self._run_estimate_conf: str = "unknown" + # M-PROGRESS B2: the active run's _LogCapture, so the ticker can read the + # completion fraction calc modules report onto it. None between runs. + self._active_log: Optional[_LogCapture] = None # Relaxed molecule from a pending pre-opt preview, awaiting Keep/Revert. self._preopt_relaxed_mol: Optional[Molecule] = None # Cache kernel io_loop once on the main thread so worker threads can @@ -4181,6 +4202,9 @@ def _run_required_final_single_point(target_mol, reason: str): on_scf_converged=_on_scf_converged, cancel_check=self._cancel_event.is_set, ) + # B2: 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 # The run header (structured banner) is written synchronously + atomically # on the main thread by ``on_run_clicked`` → ``_write_run_header`` BEFORE @@ -5054,6 +5078,9 @@ def _start_elapsed_ticker(self, start_t: float) -> None: # B1: reset the runtime estimate; _do_run fills it in once computed. self._run_estimate_s = None self._run_estimate_conf = "unknown" + # B2: drop any prior run's log so a stale fraction can't leak into the + # new run's chip before this run's _LogCapture is created. + self._active_log = None stop_event = threading.Event() self._elapsed_stop_event = stop_event @@ -5083,6 +5110,20 @@ def _format_elapsed_chip(self, elapsed: float) -> str: from quantui.log_utils import format_elapsed base = f"⏱ {format_elapsed(elapsed)}" + + # B2: prefer a self-correcting fraction-based estimate when a calc module + # reports real progress (PES points, optimizer fmax trend). Only trust it + # once past a small floor so early-run noise doesn't spike the estimate. + log = getattr(self, "_active_log", None) + frac = getattr(log, "_fraction", None) if log is not None else None + if frac is not None and frac >= 0.03 and elapsed > 0: + remaining = elapsed * (1.0 - frac) / frac + return ( + '' + f"{base} · ~{format_elapsed(remaining)} left" + ) + + # B1 fallback: static total estimate minus elapsed. est = getattr(self, "_run_estimate_s", None) if est and est > 0: remaining = est - elapsed diff --git a/quantui/log_utils.py b/quantui/log_utils.py index a74e35b..51faa07 100644 --- a/quantui/log_utils.py +++ b/quantui/log_utils.py @@ -198,6 +198,24 @@ def emit_status(stream: Any, message: str) -> None: pass +def emit_progress(stream: Any, fraction: float) -> None: + """Report a completion fraction (0..1) via a progress stream, if supported. + + M-PROGRESS B2: calc modules that know a real completion fraction (PES scan + points, optimizer fmax-convergence trend) call this so the live ticker can + show a *self-correcting* ``elapsed·(1−f)/f`` remaining-time estimate instead + of the static B1 total. Duck-typed on ``stream.set_progress_fraction`` — a + no-op for plain streams, keeping calc modules decoupled from the widget layer. + """ + setter = getattr(stream, "set_progress_fraction", None) + if setter is None: + return + try: + setter(fraction) + except Exception: # noqa: BLE001 — progress is best-effort + pass + + def format_elapsed(seconds: float) -> str: """Compact elapsed-time label for the live ticker: ``0:42`` / ``1:03:22``.""" if seconds < 0: diff --git a/quantui/optimizer.py b/quantui/optimizer.py index 47582cc..53e32cb 100644 --- a/quantui/optimizer.py +++ b/quantui/optimizer.py @@ -341,6 +341,7 @@ def optimize_geometry( steps: int = DEFAULT_OPT_STEPS, progress_stream: Optional[IO[str]] = None, status_label: str = "Optimizing geometry", + report_fraction: bool = True, ) -> OptimizationResult: """ Optimize a molecular geometry at the QM level using ASE-BFGS + PySCF. @@ -469,6 +470,35 @@ def optimize_geometry( if _cancel_check is not None: dyn.attach(lambda: raise_if_cancelled(_cancel_check), interval=1) + # M-PROGRESS B2: estimate completion from the fmax-convergence trend + # (log-scale between the first step's fmax and the target). Data-free + # and self-correcting. Skipped when report_fraction is False (e.g. + # reorg drives several sub-optimizations, whose 0→1 resets would make + # an overall remaining-time estimate oscillate). + if report_fraction: + from .log_utils import emit_progress + + _fmax0: list = [] # first-step fmax, captured on first callback + + def _report_opt_fraction() -> None: + try: + forces = atoms.get_forces() + fmax_now = float(math.sqrt((forces**2).sum(axis=1).max())) + except Exception: # noqa: BLE001 — progress is best-effort + return + if fmax_now <= 0: + return + if not _fmax0: + _fmax0.append(fmax_now) + return + denom = math.log(_fmax0[0] / fmax) if fmax > 0 else 0.0 + if denom <= 0: + return + frac = math.log(_fmax0[0] / fmax_now) / denom + emit_progress(_stream, max(0.0, min(frac, 0.99))) + + dyn.attach(_report_opt_fraction, interval=1) + with capture_c_stderr(_stream), contextlib.redirect_stdout(_null): converged = bool(dyn.run(fmax=fmax, steps=steps)) diff --git a/quantui/pes_scan.py b/quantui/pes_scan.py index 7e54fce..12a93a7 100644 --- a/quantui/pes_scan.py +++ b/quantui/pes_scan.py @@ -318,13 +318,15 @@ def run_pes_scan( for step_num, val in enumerate(scan_values, start=1): raise_if_cancelled(_cancel_check) - # M-PROGRESS A2: live per-point status (the per-point SCF is silent). - from .log_utils import emit_status + # M-PROGRESS A2/B2: live per-point status + exact completion fraction + # (points already done / total) for the self-correcting time estimate. + from .log_utils import emit_progress, emit_status emit_status( _stream, f"Scan point {step_num}/{steps} — relaxing (SCF + gradient)…", ) + emit_progress(_stream, (step_num - 1) / steps) _stream.write( f"\nScan point {step_num}/{steps}: " f"{scan_type} = {val:.4f} {('Å' if scan_type == 'bond' else '°')}\n" diff --git a/quantui/reorganization_energy.py b/quantui/reorganization_energy.py index 06d8e30..55a9823 100644 --- a/quantui/reorganization_energy.py +++ b/quantui/reorganization_energy.py @@ -320,6 +320,7 @@ def _single_point(mol: Molecule, mth: str, tag: str) -> float: steps=steps, progress_stream=stream, # type: ignore[arg-type] status_label="Reorg: optimizing neutral geometry", + report_fraction=False, # B2: don't let sub-opt 0→1 resets oscillate ETA ) neutral_mol = neutral_opt.molecule n_total_steps = neutral_opt.n_steps @@ -363,6 +364,7 @@ def _single_point(mol: Molecule, mth: str, tag: str) -> float: steps=steps, progress_stream=stream, # type: ignore[arg-type] status_label=f"Reorg: optimizing {kind} ion geometry", + report_fraction=False, # B2: see neutral-opt note above ) ion_mol = ion_opt.molecule n_total_steps += ion_opt.n_steps diff --git a/tests/test_app.py b/tests/test_app.py index a6fc0db..ca741c4 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -807,6 +807,55 @@ def test_overdue_estimate_switches_message(self): assert "left" not in chip +class TestFractionProgress: + """M-PROGRESS B2: fraction-complete drives a self-correcting remaining time.""" + + def test_emit_progress_sets_and_clamps_fraction(self): + from quantui.app import _LogCapture + from quantui.log_utils import emit_progress + + cap = _LogCapture(widgets.Output()) + emit_progress(cap, 0.4) + assert cap._fraction == 0.4 + emit_progress(cap, 1.5) # clamped below 1.0 + assert cap._fraction == 0.999 + emit_progress(cap, -0.2) # clamped at 0 + assert cap._fraction == 0.0 + + def test_emit_progress_noop_on_plain_stream(self): + import io + + from quantui.log_utils import emit_progress + + emit_progress(io.StringIO(), 0.5) # must not raise + + def test_chip_prefers_fraction_over_total(self): + from quantui.app import _LogCapture + + app = QuantUIApp() + # A total estimate is present, but a real fraction should win. + app._run_estimate_s = 9999.0 + cap = _LogCapture(widgets.Output()) + cap.set_progress_fraction(0.5) + app._active_log = cap + # 50% done at 60 s elapsed → ~60 s remaining (elapsed·(1−f)/f). + chip = app._format_elapsed_chip(60) + assert "~1:00 left" in chip + assert "(rough)" not in chip # fraction path is not "rough" + + def test_chip_ignores_tiny_fraction_and_falls_back(self): + from quantui.app import _LogCapture + + app = QuantUIApp() + app._run_estimate_s = 120.0 + cap = _LogCapture(widgets.Output()) + cap.set_progress_fraction(0.01) # below the 0.03 floor + app._active_log = cap + chip = app._format_elapsed_chip(30) + # Falls back to B1 total: 120 − 30 = 90 s. + assert "~1:30 left" in chip + + class TestStatusHeartbeat: """M-PROGRESS A2: emit_status drives run_status without touching the log.""" From c84b9bcaab445e1cb892fa9899563b65d6545b5f Mon Sep 17 00:00:00 2001 From: NCCU-Schultz-Lab Date: Sat, 18 Jul 2026 21:21:24 -0400 Subject: [PATCH 2/3] Add geom-opt step telemetry and progress prior This adds B3 geometry-optimization progress improvements by recording `n_steps` in performance logs and introducing `calc_log.estimate_opt_steps()` to derive a median historical step prior from converged runs. `QuantUIApp` now passes that estimate into `optimize_geometry()`, and optimizer status/progress uses it to show `step k/~N` and floor early progress when fmax-based fractions are noisy. Tests were added for `n_steps` persistence and `estimate_opt_steps()` behavior (median, empty history, and filtering of unconverged/other calc types). --- quantui/app.py | 9 ++++++ quantui/calc_log.py | 46 +++++++++++++++++++++++++++++ quantui/optimizer.py | 20 ++++++++++--- tests/test_calc_log.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/quantui/app.py b/quantui/app.py index 225b4f2..3a73239 100644 --- a/quantui/app.py +++ b/quantui/app.py @@ -4291,6 +4291,11 @@ def _run_required_final_single_point(target_mol, reason: str): self.run_status.value = "Optimizing geometry..." from quantui import optimize_geometry + # B3: history-based expected step count → "step k/~N" + a floor + # for the live progress fraction. None on cold history. + _expected_steps = _calc_log.estimate_opt_steps( + self.method_dd.value, self.basis_dd.value + ) result = optimize_geometry( molecule=calc_mol, method=self.method_dd.value, @@ -4298,6 +4303,9 @@ def _run_required_final_single_point(target_mol, reason: str): fmax=self.fmax_fi.value, steps=self.max_steps_si.value, progress_stream=log, # type: ignore[arg-type] + expected_steps=( + int(round(_expected_steps)) if _expected_steps else None + ), ) _sp_result = _run_required_final_single_point( result.molecule, @@ -4863,6 +4871,7 @@ def _run_required_final_single_point(target_mol, reason: str): calc_type=save_type, gpu_used=getattr(result, "gpu_used", None), gpu_name=getattr(result, "gpu_name", None), + n_steps=getattr(result, "n_steps", None), ) _calc_log.log_event( "calc_done", diff --git a/quantui/calc_log.py b/quantui/calc_log.py index d4f9607..cebc673 100644 --- a/quantui/calc_log.py +++ b/quantui/calc_log.py @@ -456,6 +456,7 @@ def log_calculation( calc_type: Optional[str] = None, gpu_used: Optional[bool] = None, gpu_name: Optional[str] = None, + n_steps: Optional[int] = None, ) -> None: """Append one performance record to ``perf_log.jsonl``. @@ -485,6 +486,10 @@ def log_calculation( record["gpu_used"] = bool(gpu_used) if gpu_name is not None: record["gpu_name"] = gpu_name + # B3: outer-loop step count (geom-opt BFGS steps / PES scan points). Enables + # a history-based "step k / ~N" prior for the live progress fraction. + if n_steps is not None: + record["n_steps"] = n_steps _append(_perf_path(), record) @@ -840,6 +845,47 @@ def _eff(r: dict) -> Optional[float]: return None +def estimate_opt_steps( + method: str, basis: str, calc_type: str = "geometry_opt" +) -> Optional[float]: + """Median historical outer-step count for *calc_type* (B3 progress prior). + + Reads ``perf_log`` for converged records of *calc_type* that recorded + ``n_steps`` (added B3). Prefers exact method+basis (>= 2 records), falls back + to same-basis (>= 2), then to all matching-calc_type records. Returns the + median or ``None`` when there is no usable history — a rough prior used only + to seed / floor the live progress fraction, not a hard prediction. + """ + try: + records = _read_all(_perf_path()) + except Exception: + return None + + def _usable(r: dict) -> bool: + return ( + r.get("calc_type") == calc_type + and bool(r.get("converged")) + and isinstance(r.get("n_steps"), (int, float)) + and r["n_steps"] > 0 + ) + + pool = [r for r in records if _usable(r)] + if not pool: + return None + exact = [r for r in pool if r.get("method") == method and r.get("basis") == basis] + same_basis = [r for r in pool if r.get("basis") == basis] + if len(exact) >= 2: + chosen = exact + elif len(same_basis) >= 2: + chosen = same_basis + else: + chosen = pool + try: + return float(statistics.median(r["n_steps"] for r in chosen)) + except Exception: + return None + + def format_estimate(est: Optional[dict]) -> str: """ Return an HTML string summarising *est* for display in the notebook. diff --git a/quantui/optimizer.py b/quantui/optimizer.py index 53e32cb..2c7eeb7 100644 --- a/quantui/optimizer.py +++ b/quantui/optimizer.py @@ -96,6 +96,7 @@ def __init__( cancel_check=None, progress_stream=None, status_label: str = "Optimizing geometry", + expected_steps=None, **kwargs, ) -> None: super().__init__(**kwargs) @@ -112,6 +113,7 @@ def __init__( # progress during a step). ``_eval_count`` counts force evaluations. self.progress_stream = progress_stream self.status_label = status_label + self.expected_steps = expected_steps # B3: history-based ~N prior self._eval_count = 0 def calculate( @@ -136,9 +138,14 @@ def calculate( self._eval_count += 1 from .log_utils import emit_status + _step_of = ( + f"{self._eval_count}/~{int(self.expected_steps)}" + if self.expected_steps + else f"{self._eval_count}" + ) emit_status( self.progress_stream, - f"{self.status_label} — SCF + gradient (step {self._eval_count})…", + f"{self.status_label} — SCF + gradient (step {_step_of})…", ) import numpy as np @@ -342,6 +349,7 @@ def optimize_geometry( progress_stream: Optional[IO[str]] = None, status_label: str = "Optimizing geometry", report_fraction: bool = True, + expected_steps: Optional[int] = None, ) -> OptimizationResult: """ Optimize a molecular geometry at the QM level using ASE-BFGS + PySCF. @@ -446,6 +454,7 @@ def optimize_geometry( cancel_check=_cancel_check, progress_stream=_stream, status_label=status_label, + expected_steps=expected_steps, ) # M-STDERR / STDERR.1: PySCF gradients (called by ASE-BFGS at every @@ -492,9 +501,12 @@ def _report_opt_fraction() -> None: _fmax0.append(fmax_now) return denom = math.log(_fmax0[0] / fmax) if fmax > 0 else 0.0 - if denom <= 0: - return - frac = math.log(_fmax0[0] / fmax_now) / denom + frac = math.log(_fmax0[0] / fmax_now) / denom if denom > 0 else 0.0 + # B3: floor with the history-based step prior so early steps + # (where the fmax trend is noisy / near 0) still advance. + if expected_steps: + step = getattr(atoms.calc, "_eval_count", 0) + frac = max(frac, min(step / float(expected_steps), 0.9)) emit_progress(_stream, max(0.0, min(frac, 0.99))) dyn.attach(_report_opt_fraction, interval=1) diff --git a/tests/test_calc_log.py b/tests/test_calc_log.py index d7b38a3..c7dd0c7 100644 --- a/tests/test_calc_log.py +++ b/tests/test_calc_log.py @@ -340,3 +340,69 @@ def test_basis_function_table_internally_consistent(isolated_log_dir): assert ( table["H"] == table["He"] ), f"{basis}: H={table['H']} but He={table['He']}, expected equal" + + +# ── B3: outer-step telemetry + geom-opt step prior ────────────────────────── + + +def _log_geom_opt(clog, *, n_steps, method="B3LYP", basis="6-31G", converged=True): + clog.log_calculation( + formula="CH2O", + n_atoms=4, + n_electrons=16, + method=method, + basis=basis, + n_iterations=10, + elapsed_s=30.0, + converged=converged, + n_basis=44, + n_cores=1, + calc_type="geometry_opt", + n_steps=n_steps, + ) + + +def test_log_calculation_records_n_steps(isolated_log_dir): + import quantui.calc_log as clog + + _log_geom_opt(clog, n_steps=7) + records = clog._read_all(clog._perf_path()) + assert records and records[-1]["n_steps"] == 7 + + +def test_estimate_opt_steps_returns_median(isolated_log_dir): + import quantui.calc_log as clog + + for n in (5, 7, 9): + _log_geom_opt(clog, n_steps=n) + assert clog.estimate_opt_steps("B3LYP", "6-31G") == 7.0 + + +def test_estimate_opt_steps_none_without_history(isolated_log_dir): + import quantui.calc_log as clog + + assert clog.estimate_opt_steps("B3LYP", "6-31G") is None + + +def test_estimate_opt_steps_excludes_unconverged_and_other_types(isolated_log_dir): + import quantui.calc_log as clog + + # Unconverged geom-opt + a single-point with n_steps must be ignored. + _log_geom_opt(clog, n_steps=99, converged=False) + clog.log_calculation( + formula="CH2O", + n_atoms=4, + n_electrons=16, + method="B3LYP", + basis="6-31G", + n_iterations=10, + elapsed_s=12.0, + converged=True, + n_basis=44, + calc_type="single_point", + n_steps=42, + ) + assert clog.estimate_opt_steps("B3LYP", "6-31G") is None + # One valid converged geom-opt → still None (needs the record to exist). + _log_geom_opt(clog, n_steps=6) + assert clog.estimate_opt_steps("B3LYP", "6-31G") == 6.0 From 43ff3989e410fbdb29524c1a8e2e9eddf6f6d6e9 Mon Sep 17 00:00:00 2001 From: NCCU-Schultz-Lab Date: Sat, 18 Jul 2026 21:57:27 -0400 Subject: [PATCH 3/3] Clean milestone tags from inline docs Removes roadmap/issue shorthand (e.g., M-*, EST.*, UXP.*, BUG.*) from comments and docstrings across the QuantUI modules, replacing them with neutral, date/context-based wording. This is a documentation-only readability pass to reduce internal jargon and keep implementation notes understandable without roadmap cross-references; runtime behavior is unchanged. --- quantui/analytics.py | 8 +- quantui/app.py | 147 +++++++++++++++---------------- quantui/app_analysis.py | 2 +- quantui/app_builders.py | 58 ++++++------ quantui/app_exports.py | 4 +- quantui/app_formatters.py | 6 +- quantui/app_history.py | 16 ++-- quantui/app_runflow.py | 80 ++++++++--------- quantui/app_visualization.py | 28 +++--- quantui/benchmarks.py | 52 +++++------ quantui/c_stderr.py | 2 +- quantui/calc_log.py | 46 +++++----- quantui/cancellation.py | 8 +- quantui/descriptor_cards.py | 2 +- quantui/freq_calc.py | 18 ++-- quantui/gpu_offload.py | 6 +- quantui/log_utils.py | 6 +- quantui/nmr_calc.py | 16 ++-- quantui/optimizer.py | 28 +++--- quantui/pes_scan.py | 12 +-- quantui/preopt.py | 6 +- quantui/reorganization_energy.py | 4 +- quantui/results_storage.py | 18 ++-- quantui/session_calc.py | 30 +++---- quantui/tddft_calc.py | 12 +-- quantui/vib_cache.py | 2 +- quantui/visualization_py3dmol.py | 5 +- quantui/viz_backend_router.py | 3 +- 28 files changed, 309 insertions(+), 316 deletions(-) diff --git a/quantui/analytics.py b/quantui/analytics.py index 99318eb..9c18b8c 100644 --- a/quantui/analytics.py +++ b/quantui/analytics.py @@ -19,7 +19,7 @@ compute device (CPU grey, GPU green), so a user can spot regressions or speedups visually as they run more calcs. -Older perf-log records that pre-date the M-GPU follow-up don't have +Older perf-log records that pre-date GPU support don't have ``gpu_used`` set — those are treated as "device unknown" and counted in their own bucket rather than guessed CPU. @@ -46,7 +46,7 @@ def _classify_device(record: dict) -> str: """Return ``"GPU"``, ``"CPU"``, or ``"Unknown"`` for one perf record. - Records written before the M-GPU follow-up (2026-05-25) don't have + Records written before GPU support (2026-05-25) don't have ``gpu_used`` at all — we don't backfill those as CPU because they pre-date GPU support entirely, so calling them "CPU" would muddy any speedup comparison. ``"Unknown"`` is the honest bucket. @@ -353,7 +353,7 @@ def _timeline_html(records: list[dict], *, include_plotlyjs: bool) -> Optional[s # --------------------------------------------------------------------------- -# Prediction-accuracy section (M-EST / EST.6, 2026-05-25) +# Prediction-accuracy section (2026-05-25) # --------------------------------------------------------------------------- @@ -555,7 +555,7 @@ def build_dashboard(out_path: Optional[Path] = None) -> Optional[Path]: method_counts = _counts_by(records, "method") calc_type_counts = _counts_by(records, "calc_type") - # M-EST / EST.6: prediction-accuracy data lives in its own log file. + # Prediction-accuracy data lives in its own log file. # Best-effort read — older installs without the file produce an # empty list and the section degrades to an empty-state message. try: diff --git a/quantui/app.py b/quantui/app.py index 3a73239..9427f92 100644 --- a/quantui/app.py +++ b/quantui/app.py @@ -511,7 +511,7 @@ # Provider key → short, accurate label for the loaded-molecule card. Replaces # the old hard-coded "PubChem: " which mislabeled offline/library/SMILES -# hits (manual finding #1b, 2026-06-15). +# hits (2026-06-15). _STRUCT_SOURCE_PREFIX = { "pubchem": "PubChem", "cactus": "NCI CACTUS", @@ -614,7 +614,7 @@ def _load_last_calibration_label() -> str: background: transparent !important; } -/* 3D molecule-viewer frames (UXP.2) — a subtle bounding box so the viewer +/* 3D molecule-viewer frames — a subtle bounding box so the viewer extent reads clearly. The light border inverts to dark automatically under the global dark-mode invert filter. */ .quantui-viewer-frame { @@ -629,7 +629,7 @@ def _load_last_calibration_label() -> str: border-color: transparent !important; } -/* Inline "calculating" spinner (UXP.3) — shown next to slow on-demand +/* Inline "calculating" spinner — shown next to slow on-demand controls (e.g. orbital-isosurface generation) while work is in flight. */ @keyframes quantui-spin { to { transform: rotate(360deg); } } .quantui-spinner { @@ -677,7 +677,7 @@ def _layout(**kwargs: Any) -> widgets.Layout: # ``_CalcCancelled`` is defined in quantui.cancellation (imported at the top of # this module) so the calc modules (session_calc / optimizer / freq / tddft / # nmr / pes) can raise the SAME class from their SCF callbacks + optimizer -# observers (UXP.5) without importing the app layer. ``_do_run``'s +# observers without importing the app layer. ``_do_run``'s # ``except _CalcCancelled`` catches it whether it was raised by # ``_LogCapture.write`` or by one of those hooks. @@ -702,7 +702,7 @@ def __init__( # Public alias so calc modules can duck-type the predicate off the # progress stream (see quantui.cancellation.cancel_check_from_stream). self.cancel_check = cancel_check - # B2: completion fraction (0..1) reported by calc modules via + # 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 @@ -744,7 +744,7 @@ def write(self, text: str) -> None: def set_status(self, message: str) -> None: """Update the live status label WITHOUT appending to the log. - M-PROGRESS A2: calc modules (optimizer / pes / reorg) call this via + Calc modules (optimizer / pes / reorg) call this via ``log_utils.emit_status`` to surface a stage label ("Optimizing — step k…") during silent (``verbose=0``) phases, without cluttering the output log the way a ``[QuantUI_STATUS]`` stream line would. @@ -758,10 +758,10 @@ def set_status(self, message: str) -> None: def set_progress_fraction(self, fraction: float) -> None: """Record a completion fraction (0..1) for the live remaining-time chip. - M-PROGRESS B2: calc modules call this via ``log_utils.emit_progress`` + Calc modules call this via ``log_utils.emit_progress`` when they know a real completion fraction (PES points, optimizer fmax trend). The elapsed ticker reads it off the active log and prefers a - self-correcting ``elapsed·(1−f)/f`` estimate over the B1 static total. + self-correcting ``elapsed·(1−f)/f`` estimate over the static total. """ try: f = float(fraction) @@ -785,7 +785,7 @@ def close(self) -> None: ase==3.26.0 (the newest version pip resolves for Python 3.9) hits the stricter ``openfile()`` and raises ``TypeError: expected str, bytes or os.PathLike object`` — a real - Python-3.9-specific compatibility gap the L6 audit fix's CI matrix + Python-3.9-specific compatibility gap the CI matrix expansion caught. """ @@ -1084,7 +1084,7 @@ def __init__(self) -> None: self._last_orb_mo_occ: Any = None self._last_orb_mol_atom: Any = None self._last_orb_mol_basis: Any = None - # Last-generated cube file path + orbital label (M-EXPORT / EXPORT.5). + # Last-generated cube file path + orbital label. # Set by the isosurface render path; consumed by the Export cube # button. Initialized here so the button handler reads ``None`` # cleanly when no isosurface has been generated yet. @@ -1103,13 +1103,13 @@ def __init__(self) -> None: # Clear button from wiping output mid-run. self._cancel_event = threading.Event() self._calc_running: bool = False - # M-PROGRESS A1: stop signal for the live elapsed-time ticker thread. + # Stop signal for the live elapsed-time ticker thread. self._elapsed_stop_event: Optional[threading.Event] = None - # M-PROGRESS B1: total run estimate the ticker turns into "time + # Total run estimate the ticker turns into "time # remaining"; set by _do_run once estimate_time() has run. self._run_estimate_s: Optional[float] = None self._run_estimate_conf: str = "unknown" - # M-PROGRESS B2: the active run's _LogCapture, so the ticker can read the + # The active run's _LogCapture, so the ticker can read the # completion fraction calc modules report onto it. None between runs. self._active_log: Optional[_LogCapture] = None # Relaxed molecule from a pending pre-opt preview, awaiting Keep/Revert. @@ -1128,7 +1128,7 @@ def __init__(self) -> None: # User settings (persisted in ~/.quantui/settings.json) + viz # backend availability snapshot. The router consumes these; render - # call sites will be migrated to the router in VIZBACK.4 ff. + # call sites will be migrated to the router. self._user_settings: UserSettings = UserSettings.load() self._viz_availability: BackendAvailability = ( BackendAvailability.from_environment() @@ -1157,7 +1157,7 @@ def __init__(self) -> None: self._assemble_tabs() # Log startup, but never let optional logging I/O break app startup. - # Include the loaded viz backend preference so a "it reset" report (#4a) + # Include the loaded viz backend preference so a "it reset" report # can be confirmed against what was actually persisted. try: _calc_log.log_event( @@ -1189,7 +1189,7 @@ def _start_deferred_startup_tasks(self) -> None: def _detect_gpu() -> None: # Warm the run-header's system-info cache (lru_cache; may shell out # to nvidia-smi) off the main thread so the synchronous header write - # in on_run_clicked stays instant on the first calc (UXP.6). + # in on_run_clicked stays instant on the first calc. try: from quantui.log_utils import get_system_info @@ -1635,7 +1635,7 @@ def _assemble_tabs(self) -> None: _rtp.insert(_rtp.index(self._to_analysis_btn), self.advanced_accordion) self.results_tab_panel.children = tuple(_rtp) - # POLISH.8 (M-POLISH, 2026-05-25): Log moved to be an + # Log moved to be an # Accordion inside the History tab — see build_output_tab for # the wrap. Tab indices renumbered: Files 6→5, System Settings # 7→6. Update any caller that depended on tab-index 5 being @@ -1658,7 +1658,7 @@ def _assemble_tabs(self) -> None: self.root_tab.set_title(3, "History") self.root_tab.set_title(4, "Compare") self.root_tab.set_title(5, "Files") - # POLISH.4 (M-POLISH, 2026-05-25): "Status" was ambiguous — + # "Status" was ambiguous — # status of what? "System Settings" is what the tab actually # holds (env info + calibration + GPU status + UI prefs). self.root_tab.set_title(6, "System Settings") @@ -1765,7 +1765,7 @@ def _wire_callbacks(self) -> None: self._orb_export_btn.on_click(self._on_orb_export_plot) self._pes_export_btn.on_click(self._on_pes_export_plot) self._vib_export_btn.on_click(self._on_vib_export_animation) - # M-EXPORT / EXPORT.4: per-panel CSV-to-clipboard / file buttons. + # Per-panel CSV-to-clipboard / file buttons. self._ir_copy_data_btn.on_click(self._on_ir_copy_data) self._uv_copy_data_btn.on_click(self._on_uv_copy_data) self._orb_copy_data_btn.on_click(self._on_orb_copy_data) @@ -1847,11 +1847,11 @@ def _wire_callbacks(self) -> None: ) # Orbital isosurface generate button self._iso_generate_btn.on_click(self._on_iso_generate) - # UXP.4: reveal the free-entry MO-index input only in "By index" mode. + # Reveal the free-entry MO-index input only in "By index" mode. self._orb_toggle.observe( self._safe_cb(self._on_orb_toggle_changed), names="value" ) - # M-EXPORT / EXPORT.5: cube + bundle exports + # Cube + bundle exports self._iso_export_cube_btn.on_click(self._on_iso_export_cube) self._export_bundle_btn.on_click(self._on_export_bundle) @@ -1864,7 +1864,7 @@ def _files_allowed_roots(self) -> list[Path]: _last_dir = getattr(self, "_last_result_dir", None) if isinstance(_last_dir, Path): candidates.append(_last_dir) - # UXP.1: expose the app's own log dir (~/.quantui/logs) so the event + # Expose the app's own log dir (~/.quantui/logs) so the event # log is reachable in-app. Resolves inside the runtime process, so it # correctly points at the WSL home when the app runs under WSL. try: @@ -2123,7 +2123,7 @@ def _preview_file_path(self, path: Path) -> None: self._set_files_status(f"Previewing SVG: {path.name}") return - # POLISH.5 (M-POLISH, 2026-05-25): specialized previews for + # Specialized previews for # extensions where the generic text dump is unhelpful. Each # handler caps file reads at 256 KB. On any exception inside a # handler, fall through to the generic text dispatch below so @@ -2187,7 +2187,7 @@ def _preview_file_path(self, path: Path) -> None: pass if suffix == ".jsonl": - # UXP.1: JSONL logs (event_log.jsonl) grow append-only, so the + # JSONL logs (event_log.jsonl) grow append-only, so the # newest — and most useful — records are at the END. The generic # text dispatch keeps the FIRST 200 KB (oldest events), so tail # the file here instead: show the last N lines, newest last. @@ -2511,8 +2511,8 @@ def _set_html_output(self, out: widgets.Output, html: str) -> None: The assignment is a single ``out.outputs = (display_data,)`` rather than ``clear_output() + append_display_data()`` so the browser never observes an intermediate empty state. This eliminates the flicker - users were seeing on IR Stick/Broadened toggle and FWHM slider drag - (BUG.9) and matches the atomic-swap pattern already used by + users were seeing on IR Stick/Broadened toggle and FWHM slider drag, + and matches the atomic-swap pattern already used by ``_swap_frame_out`` (trajectory) and ``_swap_vib_output`` (vib). """ if threading.current_thread() is not threading.main_thread(): @@ -2532,20 +2532,20 @@ def _refresh_calc_mol_viewer(self, *, backend: Optional[str] = None) -> None: """Re-render the Calculate-tab molecule viewer via an atomic HTML swap. Replaces the ``with self.viz_output: display_molecule(...)`` pattern - that surfaced BUG B1/B2/B3 (2026-05-25 user report): + that surfaced three viewer issues (2026-05-25): - - **B1** "viewer doesn't update on PubChem load until I toggle the + - "viewer doesn't update on PubChem load until I toggle the backend" — the Output-context render path was racing the kernel's comms flush, so the initial display was sometimes never emitted. Atomic ``outputs = (display_data,)`` is a single synchronous assignment that the front-end always picks up. - - **B3** "red log lines around the viewer on the Calculate tab" — + - "red log lines around the viewer on the Calculate tab" — ``with self.viz_output:`` captured every ``logger.info`` / ``logger.error`` line that ``display_molecule`` emitted while it ran. ``render_molecule_html`` returns the HTML string OUTSIDE any Output context, so the only thing that lands in the widget is the viewer itself. - - **B2** "PlotlyMol valence error spills as red text" — the same + - "PlotlyMol valence error spills as red text" — the same helper wraps render failures into an inline error
, so plotlymol's RDKit-bond-perception failure on aromatic systems shows up as a friendly inline message instead of a logger.error @@ -2667,7 +2667,7 @@ def _set_viz_preference(self, new_pref: str, *, persist: bool) -> None: ``new_pref`` must be one of "auto" | "py3dmol" | "plotlymol". All three widgets (Settings dropdown + Calculate/Analysis toggles) edit the same single global preference, so every user-initiated change passes - ``persist=True`` (#4a — a backend choice must survive the session). + ``persist=True`` (a backend choice must survive the session). ``persist=False`` remains available for programmatic syncs that must not write settings. @@ -2776,7 +2776,7 @@ def _on_viz_backend_changed(self, change) -> None: Previously ``persist=False`` (session-only). That surprised users: the toggle visibly updated every view + the Settings dropdown, but the - choice silently reverted next session (manual finding #4a, 2026-06-15). + choice silently reverted next session (2026-06-15). All three widgets edit the one global preference, so any user-initiated change now persists. """ @@ -2785,7 +2785,7 @@ def _on_viz_backend_changed(self, change) -> None: self._set_viz_preference(change["new"], persist=True) def _on_viz_backend_changed_ana(self, change) -> None: - """Analysis-tab toggle observer — persists the chosen backend (see #4a).""" + """Analysis-tab toggle observer — persists the chosen backend.""" if self._viz_sync_in_progress: return self._set_viz_preference(change["new"], persist=True) @@ -2894,14 +2894,14 @@ def _apply_pubchem_search_result( source: Optional[str] = None, ) -> None: # Runs on the main loop. Terminal point of a search → end the activity - # indicator started in the search handler (#2). Best-effort + idempotent + # indicator started in the search handler. Best-effort + idempotent # via the counter, so an unbalanced begin can't pin the light "busy". try: self._activity_end(kind="ui") except Exception: pass if error is None and mol is not None: - # Label by where the structure ACTUALLY came from (#1b) — not always + # Label by where the structure ACTUALLY came from — not always # "PubChem". Offline FALLBACK means the network was tried + failed, # so surface a no-network note; a plain library hit is not an error. prefix = _STRUCT_SOURCE_PREFIX.get(source or "", "Structure") @@ -2973,7 +2973,7 @@ def _show_pubchem_candidates(self, query: str, candidates: list) -> None: ) self.pubchem_btn.disabled = False # Terminal point of the search phase (awaiting the user's pick) → end - # the activity indicator started in _on_search_pubchem (#2). + # the activity indicator started in _on_search_pubchem. try: self._activity_end(kind="ui") except Exception: @@ -3009,7 +3009,7 @@ def _on_search_pubchem(self, btn) -> None: self.pubchem_btn.disabled = True # Light the toolbar activity indicator so the resolver chain # (PubChem → CACTUS → library, up to ~8 s on a CACTUS timeout) doesn't - # look like a hang (#2). Ended at every terminal point below. + # look like a hang. Ended at every terminal point below. try: self._activity_begin(f'Searching structures for "{query}"…', kind="ui") except Exception: @@ -3099,7 +3099,7 @@ def _on_cancel(self, btn=None) -> None: self.cancel_btn.disabled = True self.cancel_btn.description = "Cancelling…" self.run_status.value = "Cancelling — stopping at the next cycle/step…" - # UXP.5: write an immediate marker to the live log so the click reads as + # Write an immediate marker to the live log so the click reads as # acknowledged even if the next cooperative checkpoint is a moment away. try: self.run_output.append_stdout( @@ -3344,7 +3344,6 @@ def _fig_to_csv(fig: Any, *, title: str = "") -> str: see (e.g.) Stick + Broadened spectra in one file. Returns the empty string if the figure has no extractable data — caller treats that as "nothing to copy" rather than writing an empty file. - (M-EXPORT / EXPORT.4) """ if fig is None: return "" @@ -3384,7 +3383,6 @@ def _copy_plot_data( a secure context + user-gesture in some browsers; failures are invisible by design). Status widget surfaces the saved path so the user can find the file even when clipboard is unavailable. - (M-EXPORT / EXPORT.4) """ if fig is None: status_widget.value = ( @@ -3543,7 +3541,7 @@ def _history_load_results( self, data: dict, result_dir: Path, *, source_btns: tuple = () ) -> None: # Activity indicator + button-disable feedback are handled inside the - # inner ``history_load_results`` helper now (HIST.1). The wrapper just + # inner ``history_load_results`` helper now. The wrapper just # forwards source_btns and refreshes the Files tab after the load. try: _hist_history_load_results(self, data, result_dir, source_btns=source_btns) @@ -3730,10 +3728,10 @@ def _set_molecule(self, mol: Molecule, label: str = "") -> None: if mol.multiplicity > 1 and self.method_dd.value == "RHF": self.method_dd.value = "UHF" - # BUG B1/B2/B3 (2026-05-25): route through ``_refresh_calc_mol_viewer`` + # Route through ``_refresh_calc_mol_viewer`` (2026-05-25) # so the viewer renders via an atomic outputs swap rather than the - # ``with self.viz_output: display(...)`` pattern that the BUG.7 fix - # already replaced for the Analysis tab. The molecule attribute on + # ``with self.viz_output: display(...)`` pattern already replaced + # for the Analysis tab. The molecule attribute on # the app was set just above; the helper reads it. self._refresh_calc_mol_viewer() @@ -4083,12 +4081,12 @@ def _do_run(self) -> None: ) _run_wall_t = time.perf_counter() _run_cpu_t = time.process_time() - # M-PROGRESS A1: start the live elapsed-time ticker. + # Start the live elapsed-time ticker. self._start_elapsed_ticker(_run_wall_t) _scf_converged_t: Optional[float] = None _tail_marks: dict[str, float] = {} - # M-EST / EST.6 (2026-05-25): capture the estimator's pre-run + # Capture the estimator's pre-run (2026-05-25) # prediction so we can write a (predicted, actual) record to # ``prediction_log.jsonl`` after the calc completes. The # estimator may return None (insufficient history); we record @@ -4141,7 +4139,7 @@ def _do_run(self) -> None: if _est is not None: _predicted_run_s = float(_est["seconds"]) _predicted_run_confidence = str(_est.get("confidence", "unknown")) - # B1: hand the estimate to the live ticker so it can show a + # Hand the estimate to the live ticker so it can show a # "time remaining" readout alongside elapsed. self._run_estimate_s = _predicted_run_s self._run_estimate_conf = _predicted_run_confidence @@ -4202,7 +4200,7 @@ def _run_required_final_single_point(target_mol, reason: str): on_scf_converged=_on_scf_converged, cancel_check=self._cancel_event.is_set, ) - # B2: expose this run's log to the elapsed ticker so it can read the + # 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 @@ -4240,7 +4238,7 @@ def _run_required_final_single_point(target_mol, reason: str): ): from quantui import optimize_geometry - # POLISH.9 (M-POLISH, 2026-05-25): rename user-facing + # Rename user-facing # "Pre-optimisation" → "Geometry optimization". The # wrapped operation is the full DFT geom-opt at the # user's selected method/basis — same code path as the @@ -4251,10 +4249,9 @@ def _run_required_final_single_point(target_mol, reason: str): f"\n── Geometry optimization (before {ct}) " f"────────────────────────────\n" ) - # BUG C (2026-05-25): catch numerical failures (e.g. - # singular matrix in cho_solve on tight rings) and fall - # back to the user's input geometry rather than killing - # the whole calc. + # Catch numerical failures (e.g. singular matrix in + # cho_solve on tight rings) and fall back to the user's + # input geometry rather than killing the whole calc. try: _pre_opt = optimize_geometry( molecule=calc_mol, @@ -4291,7 +4288,7 @@ def _run_required_final_single_point(target_mol, reason: str): self.run_status.value = "Optimizing geometry..." from quantui import optimize_geometry - # B3: history-based expected step count → "step k/~N" + a floor + # History-based expected step count → "step k/~N" + a floor # for the live progress fraction. None on cold history. _expected_steps = _calc_log.estimate_opt_steps( self.method_dd.value, self.basis_dd.value @@ -4359,13 +4356,13 @@ def _run_required_final_single_point(target_mol, reason: str): # ── Step 2: optional geometry optimization ──────────────────── # - # POLISH.9 (M-POLISH, 2026-05-25): renamed from + # Renamed from # "pre-optimisation" — the wrapped operation is a full # DFT geometry optimization at the user's selected # method/basis. The LJ-classical pre-opt is in # quantui/preopt.py and keeps its "pre-opt" name. # - # BUG C (2026-05-25): geom-opt can hit a singular matrix + # Geom-opt can hit a singular matrix # in PySCF's ``cho_solve`` on tight rings (e.g. aromatic # benzene with B3LYP/6-31G). That raises out of the # optimizer and used to kill the whole calc. Wrap it: on @@ -4467,7 +4464,7 @@ def _run_required_final_single_point(target_mol, reason: str): ) # ── Step 2: optional geometry optimization ──────────────────── - # POLISH.9 (M-POLISH, 2026-05-25): renamed from + # Renamed from # "pre-optimisation" — DFT geom-opt is just geom-opt. if self._freq_preopt_cb.value: from quantui import optimize_geometry @@ -4479,9 +4476,9 @@ def _run_required_final_single_point(target_mol, reason: str): "\n── Geometry optimization (before UV-Vis (TD-DFT)) " "─────────────\n" ) - # BUG C (2026-05-25): catch numerical failures and - # fall back to the user's seed geometry rather than - # killing the whole TD-DFT calc. + # Catch numerical failures and fall back to the + # user's seed geometry rather than killing the whole + # TD-DFT calc. try: _pre_opt = optimize_geometry( molecule=calc_mol, @@ -4730,7 +4727,7 @@ def _run_required_final_single_point(target_mol, reason: str): spectra=save_spectra, ) self._last_result_dir = _saved_dir - # M-EXPORT / EXPORT.5: result folder is now on disk — + # Result folder is now on disk — # the "Export bundle (.zip)" button has something to zip. try: self._export_bundle_btn.disabled = False @@ -4750,7 +4747,7 @@ def _run_required_final_single_point(target_mol, reason: str): _e_list = getattr(result, "energies_hartree", []) if _traj: save_trajectory(_saved_dir, _traj, _e_list or []) - # M-EXPORT / EXPORT.3 + EXPORT.7: also write + # Also write # external-tool-friendly trajectory formats. # Multi-frame XYZ (any viewer) and ASE .traj # (ASE-GUI + ASE Python post-processing). Both @@ -4776,7 +4773,7 @@ def _run_required_final_single_point(target_mol, reason: str): ) except Exception: pass - # Persist pre-opt geometry trajectory for Frequency runs (DEC-007). + # Persist pre-opt geometry trajectory for Frequency runs. if ct == "Frequency" and _pre_opt is not None: _pre_traj = getattr(_pre_opt, "trajectory", None) _pre_e = list(getattr(_pre_opt, "energies_hartree", [])) @@ -4790,7 +4787,7 @@ def _run_required_final_single_point(target_mol, reason: str): # Persist MO data for orbital diagram + isosurface replay. if ct in ("Single Point", "Geometry Opt", "Frequency"): save_orbitals(_saved_dir, result) - # M-EXPORT / EXPORT.1+2: write a Molden-format companion + # Write a Molden-format companion # file so users can open results in Avogadro / IQmol / # Jmol. Best-effort — failures are swallowed by the # outer try block above and the calc still completes. @@ -4881,7 +4878,7 @@ def _run_required_final_single_point(target_mol, reason: str): gpu_used=bool(getattr(result, "gpu_used", False)), gpu_name=getattr(result, "gpu_name", None), ) - # M-EST / EST.6: persist the (predicted, actual) pair to + # Persist the (predicted, actual) pair to # ``prediction_log.jsonl``. ``_predicted_run_s`` was # captured at the top of _do_run via the same # estimate_time(...) call that drives the UI estimate; @@ -5073,21 +5070,21 @@ def _run_required_final_single_point(target_mol, reason: str): self._stop_elapsed_ticker() self._activity_end(kind="compute") - # ── Live elapsed ticker (M-PROGRESS A1) ─────────────────────────────── + # ── Live elapsed ticker ──────────────────────────────────────────────── def _start_elapsed_ticker(self, start_t: float) -> None: """Spin up a daemon thread that updates the elapsed chip every ~1 s. Writes only to ``_run_elapsed_lbl`` (never ``run_status``), so it never fights the stage labels set by ``_LogCapture`` / the calc modules. - ``Label``/``HTML`` ``.value`` writes are thread-safe (reflection 02 - Rule 1), so no io_loop marshaling is needed. + ``Label``/``HTML`` ``.value`` writes are thread-safe, so no io_loop + marshaling is needed. """ self._stop_elapsed_ticker() # ensure no prior ticker is still running - # B1: reset the runtime estimate; _do_run fills it in once computed. + # Reset the runtime estimate; _do_run fills it in once computed. self._run_estimate_s = None self._run_estimate_conf = "unknown" - # B2: drop any prior run's log so a stale fraction can't leak into the + # Drop any prior run's log so a stale fraction can't leak into the # new run's chip before this run's _LogCapture is created. self._active_log = None stop_event = threading.Event() @@ -5109,18 +5106,18 @@ def _tick() -> None: def _format_elapsed_chip(self, elapsed: float) -> str: """Compose the live chip: ``⏱ `` + ``· ~ left``. - B1 (M-PROGRESS): folds the pre-run total estimate (``_run_estimate_s``, + Folds the pre-run total estimate (``_run_estimate_s``, set by ``_do_run``) into a remaining-time readout. Degrades to elapsed-only when there's no estimate (cold history) and switches to "longer than estimated" once elapsed passes the estimate — never shows a negative or false-precise number. Low-confidence estimates are marked - "(rough)" so the readout stays honest (the estimator is Phase C's job). + "(rough)" so the readout stays honest. """ from quantui.log_utils import format_elapsed base = f"⏱ {format_elapsed(elapsed)}" - # B2: prefer a self-correcting fraction-based estimate when a calc module + # Prefer a self-correcting fraction-based estimate when a calc module # reports real progress (PES points, optimizer fmax trend). Only trust it # once past a small floor so early-run noise doesn't spike the estimate. log = getattr(self, "_active_log", None) @@ -5132,7 +5129,7 @@ def _format_elapsed_chip(self, elapsed: float) -> str: f"{base} · ~{format_elapsed(remaining)} left" ) - # B1 fallback: static total estimate minus elapsed. + # Fallback: static total estimate minus elapsed. est = getattr(self, "_run_estimate_s", None) if est and est > 0: remaining = est - elapsed @@ -5210,7 +5207,7 @@ def _wrapper(change): return _wrapper def _goto_output_tab(self) -> None: - # POLISH.8 (M-POLISH, 2026-05-25): the standalone Log tab is + # The standalone Log tab is # gone; the PySCF output log now lives in an Accordion inside # the History tab (index 3). Switch tabs + expand the log # accordion so the user lands directly on the log content. diff --git a/quantui/app_analysis.py b/quantui/app_analysis.py index ce7016c..e1033b6 100644 --- a/quantui/app_analysis.py +++ b/quantui/app_analysis.py @@ -328,7 +328,7 @@ def pop_preopt_trajectory(app: Any, ctx: Any) -> bool: """Populate Trajectory panel for the frequency-time DFT geometry optimization trajectory. - POLISH.9 (2026-05-25): the wrapped operation is a full DFT geom-opt + The wrapped operation is a full DFT geom-opt at the user's method/basis, not the classical LJ pre-opt that lives in ``quantui/preopt.py``. The function name + ``preopt_trajectory.json`` filename stay (renaming the saved file would break history replay of diff --git a/quantui/app_builders.py b/quantui/app_builders.py index 3fdc1f4..28ac254 100644 --- a/quantui/app_builders.py +++ b/quantui/app_builders.py @@ -96,7 +96,7 @@ def _ok(flag: bool, extra: str = "") -> str: "Timing calibration: not yet run — use the Calibrate panel in History
" ) - # GPU offload indicator (M-GPU / GPU.2). Detecting GPUs imports gpu4pyscf + + # GPU offload indicator. Detecting GPUs imports gpu4pyscf + # cupy and queries CUDA, which costs ~7 s — far too slow to block startup. # So the status HTML is rendered via this closure with the GPU row as a # "checking…" placeholder; the app re-renders it (via app._render_status_html) @@ -259,7 +259,7 @@ def build_history_section( tooltip="Open the full PySCF output log in the Output tab", ) - # M-EST / EST.4: 4-tier calibration selector. ToggleButtons works for + # 4-tier calibration selector. ToggleButtons works for # 4 options; switch to a Dropdown if a 5th tier is ever added. Tier 3 # / tier 4 require PySCF (the geom-opt + freq dispatch); tier 1 / 2 # are SP-only and gated separately by the run button. @@ -295,10 +295,9 @@ def build_history_section( tooltip="Abandon the rest of the calibration (current step is also killed).", layout=layout_fn(width="90px", display="none"), ) - # session 55 user request: replaced the hard 1800 s per-step timeout - # with a Skip button so the user can abandon ONE step that's running - # too long without losing the whole run. Distinct from Stop (which - # abandons everything remaining). + # Replaced the hard 1800 s per-step timeout with a Skip button so the + # user can abandon ONE step that's running too long without losing the + # whole run. Distinct from Stop (which abandons everything remaining). app._cal_skip_btn = widgets.Button( description="Skip step", button_style="info", @@ -391,7 +390,7 @@ def build_history_section( if cal_last else "" ) - # M-EST / EST.4: import tier sizes lazily so we can refer to all four + # Import tier sizes lazily so we can refer to all four # in the panel blurb. ``benchmark_suite`` / ``benchmark_suite_long`` # are kept as positional args for back-compat but new code prefers # the four named tiers. @@ -431,10 +430,9 @@ def build_history_section( app._cal_accordion = widgets.Accordion(children=[cal_panel], selected_index=None) app._cal_accordion.set_title(0, "Calibrate time estimates") - # POLISH.3 (M-POLISH, 2026-05-25): the History tab is now purely - # the result-browser. Performance stats + Calibrate accordions - # moved to the System Settings tab — see below — so the user finds - # benchmarking + system state in one logical place. + # The History tab is now purely the result-browser. Performance stats + # + Calibrate accordions moved to the System Settings tab — see below — + # so the user finds benchmarking + system state in one logical place. app.history_panel = widgets.VBox( [ widgets.HTML( @@ -455,7 +453,7 @@ def build_history_section( ] ) - # POLISH.3: now that the calibration + performance accordions exist + # Now that the calibration + performance accordions exist # (created above in this function), append them to the System # Settings tab. ``_status_tab_panel`` was built earlier in # ``build_status_panel`` without these — extend its children tuple. @@ -503,7 +501,7 @@ def build_shared_widgets( app.mol_summary_compact = widgets.HTML(value="") # Fixed heights reserve space so swapping content (backend/palette toggle) # or streaming output never resizes the container — which would reflow the - # page and jump the scrollbar (BUG-SCROLL). The molecule viewer renders at + # page and jump the scrollbar. The molecule viewer renders at # render_molecule_html's default 500px; the run log scrolls internally. # overflow hidden (not auto): the 3D viewer is a fixed-size canvas, so it # needs no scrollbar — clipping a few px of margin avoids an internal @@ -570,7 +568,7 @@ def build_shared_widgets( [app.viz_style_dd, app.viz_lighting_dd], layout=layout_fn(gap="8px", margin="2px 0 0 0", align_items="center"), ) - # UXP.7: method / basis descriptor cards replace the old inline + # Method / basis descriptor cards replace the old inline # ``notes_output`` educational-notes block. Two compact HTML cards # (icon + one-line) sit to the right of the dropdowns; populated here with # the defaults and refreshed by ``update_notes`` on every dropdown change. @@ -644,7 +642,7 @@ def build_shared_widgets( ' — fast MMFF/UFF ' "cleanup of a rough structure" ) - # Interactive pre-opt (M-PREOPT PREOPT.2/.3): run the bonded-FF pre-opt on + # Interactive pre-opt: run the bonded-FF pre-opt on # demand, watch it relax in-place, then keep or revert. app.preopt_preview_btn = widgets.Button( description="Preview", @@ -696,7 +694,7 @@ def build_shared_widgets( from quantui.config import SOLVENT_OPTIONS as _SOLVENT_OPTS - # POLISH.10: drop the default Checkbox description gutter + explicit width + # Drop the default Checkbox description gutter + explicit width # (style description_width "initial" + indent=False) that produced the # indent + horizontal scrollbar. app.solvent_cb = widgets.Checkbox( @@ -921,7 +919,7 @@ def build_shared_widgets( layout=layout_fn(width="200px", height="36px"), ) app.run_status = widgets.Label() - # M-PROGRESS A1: live elapsed-time chip next to the status text — a bg-thread + # Live elapsed-time chip next to the status text — a bg-thread # ticker updates it every ~1 s while a calc runs, so even a fully silent # native phase (first gradient / Hessian) visibly advances. app._run_elapsed_lbl = widgets.HTML(value="") @@ -990,7 +988,7 @@ def build_shared_widgets( layout=layout_fn(width="130px"), ) app.struct_export_status = widgets.Label() - # M-EXPORT / EXPORT.5: zip the entire result folder for emailing / + # Zip the entire result folder for emailing / # attaching to a writeup. Disabled until ``_last_result_dir`` is set. app._export_bundle_btn = widgets.Button( description="Export bundle (.zip)", @@ -1031,7 +1029,7 @@ def build_theme_selector(app: Any, *, layout_fn: Any) -> None: def build_welcome_header(app: Any, *, layout_fn: Any = None) -> None: """Build the QuantUI welcome banner. - POLISH.1 third iteration (M-POLISH, 2026-05-25): the + Third iteration (2026-05-25): the ```` approach failed too — Voilà's HTML sanitizer (stricter than JupyterLab's) strips ``data:`` URIs from ```` attributes. The third iteration @@ -1317,7 +1315,7 @@ def build_calc_setup(app: Any, *, layout_fn: Any) -> None: widgets.HTML("  "), widgets.VBox([app.charge_si, app.mult_si]), widgets.HTML(" "), - # UXP.7 descriptor cards — to the right of the inputs so + # Descriptor cards — to the right of the inputs so # they don't displace charge/multiplicity. app._descriptor_cards_box, ], @@ -1404,7 +1402,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox: layout=layout_fn(width="130px"), tooltip="Export the current plot as HTML or PNG", ) - # M-EXPORT / EXPORT.4: per-panel "Copy data" button that exports + # Per-panel "Copy data" button that exports # the underlying numerical data to CSV (saved to result_dir) and # also attempts to copy to the system clipboard via the browser # API. Status widget below is shared with the Save Plot path — @@ -1447,7 +1445,6 @@ def _plot_export_row(prefix: str) -> widgets.HBox: # Using `widgets.Output` here previously caused widget references inside # `with output: display(widget)` to be deferred/asynchronous, leaving the # accordion visibly empty even after _show_opt_trajectory logged success. - # See BUG-FRESH-TRAJ root-cause analysis in session 48. app.traj_output = widgets.VBox(layout=layout_fn(margin="0")) app.traj_accordion = widgets.Accordion( children=[app.traj_output], @@ -1551,7 +1548,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox: style={"description_width": "120px"}, layout=layout_fn(width="300px", display="none"), # continuous_update=False so dragging the slider only fires on - # release, not 30-60 times per second during the drag (BUG.9 fix). + # release, not 30-60 times per second during the drag. # Combined with the atomic outputs swap in _set_html_output this # eliminates the IR re-render storm that caused visible flicker. continuous_update=False, @@ -1640,7 +1637,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox: orb_diagram_content, layout=layout_fn(width="100%"), ) - # UXP.4: widen the preset buttons and add a "By index" mode that reveals a + # Widen the preset buttons and add a "By index" mode that reveals a # free-entry 0-based MO index input, so any orbital (not just the HOMO/LUMO # neighbourhood) can be rendered as an isosurface. app._orb_toggle = widgets.ToggleButtons( @@ -1694,7 +1691,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox: ), layout=layout_fn(width="200px", margin="8px 0 4px 0"), ) - # M-EXPORT / EXPORT.5: copy the last-generated cube to the top-level + # Copy the last-generated cube to the top-level # result dir under a friendly name (HOMO.cube / LUMO.cube / etc.). # Disabled until the first isosurface generation populates # ``app._last_cube_path``. @@ -1711,7 +1708,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox: app._iso_export_status = widgets.HTML( value="", layout=layout_fn(margin="0 0 0 8px") ) - # UXP.3: inline "calculating" spinner shown while a cube is being computed. + # Inline "calculating" spinner shown while a cube is being computed. # Hidden until on_iso_generate reveals it; hidden again on completion. app._iso_spinner = widgets.HTML( value='', @@ -2083,8 +2080,7 @@ def build_output_tab(app: Any, *, layout_fn: Any) -> None: button_style="danger", layout=layout_fn(width="140px", display="none"), ) - # POLISH.8 (M-POLISH, 2026-05-25): the Log tab moved to be an - # Accordion inside the History tab — rationale in the roadmap. The + # The Log tab moved to be an Accordion inside the History tab. The # explanatory text no longer needs to say "Use View log in the # History tab" since the user IS in the History tab now. app.log_tab_panel = widgets.VBox( @@ -2117,7 +2113,7 @@ def build_output_tab(app: Any, *, layout_fn: Any) -> None: layout=layout_fn(padding="8px 0"), ) - # POLISH.8: wrap the log panel in an Accordion + append to the + # Wrap the log panel in an Accordion + append to the # History tab. ``history_panel`` was built in # ``build_history_section`` earlier in the app-init sequence # (see app.py: _build_history_section runs BEFORE _build_output_tab). @@ -2222,8 +2218,8 @@ def build_help_section(app: Any, *, layout_fn: Any) -> None: app.help_content_html = widgets.HTML() app._render_help_topic() - # POLISH.2 (M-POLISH, 2026-05-25): the single-character "?" was - # visually noisy and hard to recognise as the global help toggle. + # The single-character "?" was visually noisy and hard to recognise + # as the global help toggle. # Field-level "?" buttons (method_help_btn / basis_help_btn earlier # in this file) keep the symbol — for inline-with-input help it's # universally understood. diff --git a/quantui/app_exports.py b/quantui/app_exports.py index 13b81ad..4c5b945 100644 --- a/quantui/app_exports.py +++ b/quantui/app_exports.py @@ -127,7 +127,7 @@ def export_molecule_and_label(app: Any) -> tuple[Any, str, str]: def on_iso_export_cube(app: Any, btn: Any) -> None: - """Copy the last-generated cube file to the result folder (EXPORT.5). + """Copy the last-generated cube file to the result folder. Reads ``app._last_cube_path`` (set by the isosurface render path in ``app_visualization.py``) and copies it to @@ -160,7 +160,7 @@ def on_iso_export_cube(app: Any, btn: Any) -> None: def on_export_bundle(app: Any, btn: Any) -> None: - """Zip the entire result folder for sharing (EXPORT.5).""" + """Zip the entire result folder for sharing.""" from quantui.results_storage import export_result_bundle result_dir = getattr(app, "_last_result_dir", None) diff --git a/quantui/app_formatters.py b/quantui/app_formatters.py index 24224ab..459da17 100644 --- a/quantui/app_formatters.py +++ b/quantui/app_formatters.py @@ -12,7 +12,7 @@ def _result_extra_rows(get: Any) -> str: ``get(key, default=None)`` reads a field from either a result object (``getattr``) or a saved ``result.json`` dict (``dict.get``). Used by BOTH :func:`format_result` (live) and :func:`format_past_result` (history) so the - two cards can never drift again — the M-CLEAN regression where the compute + two cards can never drift again — a past regression had the compute device / dipole / Mulliken rows existed only on the live card. Rows: post-HF correlation breakdown (MP2 / CCSD / (T)), solvent, compute device (always shown), dipole moment, Mulliken charges. @@ -45,7 +45,7 @@ def _num(label: str, value: str) -> str: if _solvent is not None: rows += _num("Solvent (PCM)", str(_solvent)) - # Compute device (M-GPU / GPU.2) — always shown; old saved results lack the + # Compute device — always shown; old saved results lack the # field and safely read "CPU". if bool(get("gpu_used", False)): _name = get("gpu_name") @@ -466,7 +466,7 @@ def format_past_result(data: dict[str, Any], result_dir: Optional[Path] = None) ts = data.get("timestamp", "") # Shared 'extra' rows (correlation breakdown / solvent / device / dipole / - # Mulliken) — same builder as the live card so the two never drift (M-CLEAN). + # Mulliken) — same builder as the live card so the two never drift. _extra = _result_extra_rows(lambda k, d=None: data.get(k, d)) # Embed thumbnail if saved diff --git a/quantui/app_history.py b/quantui/app_history.py index 426a6f9..a2f510b 100644 --- a/quantui/app_history.py +++ b/quantui/app_history.py @@ -13,14 +13,14 @@ class _LoadTimer: - """Per-stage timing collector for a history-load operation (HIST.2). + """Per-stage timing collector for a history-load operation. Used as: open one ``_LoadTimer`` at the top of each loader, wrap each interesting sub-stage in ``with timer.stage("name"):``, then call ``timer.emit(status=...)`` exactly once (from the loader's ``finally`` block). One ``history_load_timing`` event is appended to ``event_log.jsonl`` per load with the total elapsed time and a per-stage - breakdown. The data drives the HIST.2 latency-optimization pass — until + breakdown. The data drives the latency-optimization pass — until we know which stage dominates, we don't know which to optimize. Failures inside ``calc_log.log_event`` are swallowed: telemetry must @@ -223,7 +223,7 @@ def mol_from_result_dir(result_dir: Path, data: dict[str, Any]) -> Any: def _begin_history_load(app: Any, message: str, source_btns: tuple) -> None: - """Show immediate feedback when a history-load action starts (HIST.1). + """Show immediate feedback when a history-load action starts. Lights the toolbar activity indicator and disables the source buttons so a second click can't fire a parallel load. Both actions are best-effort — @@ -242,7 +242,7 @@ def _begin_history_load(app: Any, message: str, source_btns: tuple) -> None: def _end_history_load(app: Any, source_btns: tuple) -> None: - """Restore UI state after a history-load action finishes (HIST.1). + """Restore UI state after a history-load action finishes. Mirrors :func:`_begin_history_load`. Called from the loader's ``finally`` block so the activity indicator + buttons are always restored, even when @@ -269,11 +269,11 @@ def history_load_results( """Display a history result card in the Results tab and navigate there. ``source_btns`` is an optional tuple of button widgets to disable while - the load is in flight (HIST.1 immediate-loading-feedback contract). Tests + the load is in flight (immediate-loading-feedback contract). Tests and callers that don't have a button reference can omit it. Stage timings are emitted as a single ``history_load_timing`` event on - completion (HIST.2 — drives latency-optimization decisions). + completion (drives latency-optimization decisions). """ _begin_history_load(app, "Loading history result…", source_btns) timer = _LoadTimer("history_load_results", result_dir) @@ -330,11 +330,11 @@ def history_load_analysis( """Load analysis panels for a history result and navigate to Analysis tab. ``source_btns`` is an optional tuple of button widgets to disable while - the load is in flight (HIST.1 immediate-loading-feedback contract). Tests + the load is in flight (immediate-loading-feedback contract). Tests and callers that don't have a button reference can omit it. Stage timings are emitted as a single ``history_load_timing`` event on - completion (HIST.2 — drives latency-optimization decisions). Stages cover + completion (drives latency-optimization decisions). Stages cover the four expected hotspots: pyscf.log read, context build, molecule reconstruction, 3D viewer render, and the analysis-context registry walk. """ diff --git a/quantui/app_runflow.py b/quantui/app_runflow.py index 2829f31..7dd4333 100644 --- a/quantui/app_runflow.py +++ b/quantui/app_runflow.py @@ -36,7 +36,7 @@ def _calc_type_badge(calc_type: str) -> str: def _write_run_header(app: Any) -> None: """Write the full run header to the live log — synchronously, atomically. - UXP.6 + bug fix (2026-07-18): the header used to be written two ways, both + Bug fix (2026-07-18): the header used to be written two ways, both unreliable in Voilà — a provisional line via ``clear_output()`` + ``append_stdout()`` (the non-atomic combo ``_set_html_output`` exists to avoid) and the structured banner via ``append_stdout`` from the *background* @@ -136,8 +136,8 @@ def on_calc_type_changed(app: Any, change: Any, *, layout_fn: Any) -> None: # The "geometry optimization before this calc" checkbox is meaningful # for all workflows except Geometry Opt itself (which IS the geom-opt - # workflow). POLISH.9: this was called "pre-optimisation" pre-2026-05-25; - # the underlying operation is a full DFT geom-opt — distinct from the + # workflow). This was previously called "pre-optimisation"; the + # underlying operation is a full DFT geom-opt — distinct from the # LJ classical pre-opt in quantui/preopt.py. # Reorganization Energy runs its own neutral + ion optimizations, so the # standalone "geometry optimization before this calc" checkbox is @@ -237,7 +237,7 @@ def update_scan_widgets(app: Any, _change: Any = None) -> None: app._scan_unit_lbl.value = '°' -# Default RMSD tolerance for the seed-geometry "same molecule" check (HIST.6). +# Default RMSD tolerance for the seed-geometry "same molecule" check. # 0.1 Å is generous enough to admit slight conformational differences (e.g. # re-importing the same SMILES, which can produce ~0.05 Å float-precision # drift in RDKit's embedding) but tight enough to reject distinct isomers, @@ -302,7 +302,7 @@ def _geometries_match( *, rmsd_tol: float = _SEED_GEOMETRY_RMSD_TOLERANCE, ) -> bool: - """Strict atom-order + RMSD-based geometry comparison (HIST.6). + """Strict atom-order + RMSD-based geometry comparison. Returns ``True`` iff the atom symbol lists are equal in order AND the structures' RMSD (no rigid alignment) is at or below ``rmsd_tol`` Å. @@ -340,7 +340,7 @@ def _refresh_seed_options(app: Any, dropdown: Any) -> None: """Populate a geo-opt seed dropdown filtered by strict atom+coord match. Shared helper used by both Frequency and UV-Vis (TD-DFT) seed dropdowns. - Filter cascade (HIST.6): + Filter cascade: 1. No active molecule → list every geo-opt result (no filter; lets the user browse history before loading anything). @@ -483,10 +483,10 @@ def _preopt_small(text: str, color: str = "#555") -> str: def on_preopt_preview(app: Any, btn: Any = None) -> None: """Run the classical pre-opt on demand and animate the relaxation in-place. - M-PREOPT PREOPT.2: instead of the pre-opt being a silent step inside the - run, the user previews it here — watches the bonded-FF relaxation animate - in the Calculate tab — then Keeps or Reverts it (PREOPT.3). The pre-opt runs - on a background thread; UI updates are marshalled back to the main thread. + Instead of the pre-opt being a silent step inside the run, the user + previews it here — watches the bonded-FF relaxation animate in the + Calculate tab — then Keeps or Reverts it. The pre-opt runs on a + background thread; UI updates are marshalled back to the main thread. """ mol = getattr(app, "_molecule", None) if mol is None: @@ -567,7 +567,7 @@ def _preopt_preview_failed(app: Any, msg: str) -> None: def on_preopt_accept(app: Any, btn: Any = None) -> None: - """Make the previewed relaxed geometry the active molecule (PREOPT.3).""" + """Make the previewed relaxed geometry the active molecule.""" relaxed = getattr(app, "_preopt_relaxed_mol", None) if relaxed is None: return @@ -844,9 +844,9 @@ def on_exit_clicked(app: Any, _unused: Any = None) -> None: app._exit_btn.description = "Exiting…" app._exit_btn.disabled = True - # POLISH.1 retry-2 (2026-05-25): the welcome logo now lives in its - # own ``widgets.Image`` next to the text. At shutdown hide the logo - # so the centered "QuantUI has shut down" message isn't off-center. + # The welcome logo now lives in its own ``widgets.Image`` next to the + # text. At shutdown hide the logo so the centered "QuantUI has shut + # down" message isn't off-center. if hasattr(app, "_welcome_logo"): try: app._welcome_logo.layout.display = "none" @@ -883,8 +883,8 @@ def on_cal_run( """Start async calibration run and initialize calibration UI state.""" _ = btn mode = app._cal_mode_toggle.value - # session 55 hotfix: the old ``"short" else "long"`` two-tier dispatch - # silently routed tier 3 / tier 4 (and tier 1!) to the tier-2 suite, + # The old ``"short" else "long"`` two-tier dispatch silently routed + # tier 3 / tier 4 (and tier 1!) to the tier-2 suite, # which set ``progress_bar.max = 20`` while tier 1 only ran 8 steps # — the bar froze at 40% on completion. Use the 4-tier lookup so # ``max`` matches the actual step count. @@ -892,8 +892,8 @@ def on_cal_run( suite = _MODE_TO_SUITE.get(mode, benchmark_suite) app._cal_stop_event = threading.Event() - # session 55 user request: skip-current-step event, separate from - # the whole-run stop event. Replaces the hard per-step timeout. + # Skip-current-step event, separate from the whole-run stop event. + # Replaces the hard per-step timeout. app._cal_skip_event = threading.Event() app._cal_run_btn.disabled = True app._cal_mode_toggle.disabled = True @@ -906,7 +906,7 @@ def on_cal_run( app._cal_step_label.value = ( 'Starting…' # Reserve a second invisible line so the live-message ticker - # doesn't jump the accordion height (session 55 user report). + # doesn't jump the accordion height. '
.' ) app._cal_results_html.value = "" @@ -924,9 +924,9 @@ def on_cal_stop(app: Any, btn: Any) -> None: def on_cal_skip(app: Any, btn: Any) -> None: """Signal the active calibration to skip the CURRENT step + continue. - Replaces the per-step timeout (session 55 user request after a - near-finishing benzene B3LYP/6-31G* freq calc got cut off at the - 1800 s tier-4 cap). The worker is killed, the step is marked + Replaces the per-step timeout (added after a near-finishing benzene + B3LYP/6-31G* freq calc got cut off at the 1800 s tier-4 cap). The + worker is killed, the step is marked ``skipped``, the event is cleared inside ``run_calibration``, and the loop moves on to the next step. """ @@ -952,7 +952,7 @@ def _cal_table_html(steps_so_far, total: int, *, in_flight_step=None) -> str: Called incrementally — after every completed step — so the user sees rows accumulate in real time instead of waiting for the whole tier - to finish (session 55 user request). ``steps_so_far`` is the list of + to finish. ``steps_so_far`` is the list of ``BenchmarkStep`` objects completed; ``in_flight_step`` (optional) is a dict ``{label, n_electrons, n_basis, status, elapsed_s}`` that appends a "running" row at the bottom while a step is mid-execution. @@ -960,8 +960,8 @@ def _cal_table_html(steps_so_far, total: int, *, in_flight_step=None) -> str: For failed steps (error / timeout / skipped) we render an inline italic line below the status cell with a truncated ``error_msg``, so the user can see WHY a step failed without having to open - ``calibration.json`` (session 55 user request after MP2/CCSD on - H₂O/cc-pVDZ silently 'errored' with no on-screen explanation). + ``calibration.json`` (added after MP2/CCSD on H₂O/cc-pVDZ silently + 'errored' with no on-screen explanation). """ import html as _html_mod @@ -1029,7 +1029,7 @@ def _err_detail(s) -> str: def do_calibration(app: Any, *, pyscf_available: bool) -> None: """Run calibration suite and render calibration summary table. - Fixes shipped 2026-05-25 (session 55 user reports): + Fixes shipped 2026-05-25: - Wraps the whole run in ``_activity_begin/_end`` so the toolbar activity badge stops reading "Idle" while calibration is busy. @@ -1049,15 +1049,15 @@ def do_calibration(app: Any, *, pyscf_available: bool) -> None: # callback; no need to compute it locally. (The earlier draft pulled # it from ``_MODE_TO_SUITE`` but never used it — ruff F841.) - # session 55 user request (after a near-finishing benzene - # B3LYP/6-31G* freq got cut off at the old 1800 s tier-4 cap): - # no automatic timeout — the user controls long-running steps via - # the Skip button. If they walk away from a runaway calc, the - # Stop button is still available. Headless callers that genuinely - # want a wall-clock cap can pass timeout_per_step explicitly. + # No automatic timeout — the user controls long-running steps via + # the Skip button (added after a near-finishing benzene B3LYP/6-31G* + # freq got cut off at the old 1800 s tier-4 cap). If they walk away + # from a runaway calc, the Stop button is still available. Headless + # callers that genuinely want a wall-clock cap can pass + # timeout_per_step explicitly. timeout_per_step: Optional[float] = None - # M-EST follow-up: keep the toolbar activity badge red for the + # Keep the toolbar activity badge red for the # duration of the calibration so the user knows the kernel is busy. app._activity_begin(f"Calibrating ({mode})…", kind="compute") @@ -1162,7 +1162,7 @@ def _progress( def update_notes(app: Any, change: Any = None) -> None: """Refresh the method / basis descriptor cards + open-shell hint. - Replaces the old inline educational-notes text block (UXP.7). The cards + Replaces the old inline educational-notes text block. The cards describe the *method* and *basis* themselves, so — unlike the old notes — they refresh independently of whether a molecule is loaded. The open-shell hint (restored from the old notes) appears only when multiplicity > 1. @@ -1224,7 +1224,7 @@ def update_estimate(app: Any, *, calc_log_mod: Any, change: Any = None) -> None: n_basis = calc_log_mod.count_basis_functions( app._molecule.atoms, app.basis_dd.value ) - # M-EST / EST.1: predict the device the upcoming run will use so + # Predict the device the upcoming run will use so # the estimator can partition history by GPU vs CPU. The method # also matters — gpu4pyscf doesn't support CCSD(T), so even on a # GPU machine that calc will run CPU-side. @@ -1262,7 +1262,7 @@ def update_estimate(app: Any, *, calc_log_mod: Any, change: Any = None) -> None: def refresh_results_browser(app: Any) -> None: """Refresh the History dropdown with saved result directories. - POLISH.6 (M-POLISH, 2026-05-25): prepends a + Prepends a ``"(select a calculation to view)"`` placeholder so the dropdown opens in an explicit "no calc loaded yet" state. Without the placeholder, ipywidgets auto-selected the most-recent entry as the @@ -1297,10 +1297,10 @@ def refresh_results_browser(app: Any) -> None: data = load_result(d) ts = data.get("timestamp", d.name) calc_badge = _calc_type_badge(data.get("calc_type", "")) - # M-EST follow-up (2026-05-25): calibration-produced results - # get a 🔧 marker so the user can tell them apart from - # user-initiated calcs. The marker comes from result.json's - # ``calibration_run_id`` extras field written by the worker. + # Calibration-produced results get a 🔧 marker so the user + # can tell them apart from user-initiated calcs. The marker + # comes from result.json's ``calibration_run_id`` extras field + # written by the worker. calib_marker = "🔧 " if data.get("calibration_run_id") else "" label = ( f"{ts} · [{calc_badge}] " diff --git a/quantui/app_visualization.py b/quantui/app_visualization.py index f8cd497..2f0b3a4 100644 --- a/quantui/app_visualization.py +++ b/quantui/app_visualization.py @@ -79,8 +79,8 @@ def show_result_3d( ``visualization_py3dmol.render_molecule_html``); the HTML is routed through ``app._set_html_output`` so the viewer is replaced as a single atomic ``Output.outputs`` swap. This avoids the nested-Output + ``display(viz)`` - pattern that caused BUG.6 (trajectory regression) and BUG.7 (Analysis-tab - top viewer rendering blank with 🙁 on history replay). + pattern that caused a trajectory regression and an Analysis-tab top + viewer rendering blank with 🙁 on history replay. """ if render_html_fn is None or molecule is None: return @@ -595,7 +595,7 @@ def show_vib_animation(app: Any, freq_result: Any, molecule: Any) -> bool: panel populates regardless of plotlymol3d availability. The plotlymol3d `VibrationalData` wrapper is built optionally — required only when the plotlymol render path is selected; the py3Dmol render path reads - displacements directly from ``freq_result`` (VIZBACK.8). + displacements directly from ``freq_result``. """ freqs = freq_result.frequencies_cm1 if not freqs: @@ -1040,7 +1040,7 @@ def show_orbital_diagram(app: Any, result: Any) -> bool: def on_iso_generate(app: Any, btn: Any) -> None: """Generate orbital isosurface for currently selected orbital.""" orbital_label = app._orb_toggle.value - # UXP.4: "By index" mode renders an arbitrary 0-based MO index. Encode it + # "By index" mode renders an arbitrary 0-based MO index. Encode it # into the label as "MO "; render_orbital_isosurface parses it back. if orbital_label == "By index": orbital_label = f"MO {int(app._orb_index_input.value)}" @@ -1048,7 +1048,7 @@ def on_iso_generate(app: Any, btn: Any) -> None: render_token = app._iso_render_token btn.disabled = True btn.description = "Generating…" - # UXP.3: reveal the inline spinner + light the toolbar activity indicator + # Reveal the inline spinner + light the toolbar activity indicator # so the (slow) cube generation reads as busy, not hung. _spinner = getattr(app, "_iso_spinner", None) if _spinner is not None: @@ -1074,7 +1074,7 @@ def on_iso_generate(app: Any, btn: Any) -> None: ) done = threading.Event() - # UXP.3: balance the single _activity_begin above exactly once, across + # Balance the single _activity_begin above exactly once, across # both the normal-completion and timeout paths (idempotent). _finished = threading.Event() @@ -1199,7 +1199,7 @@ def _is_stale() -> bool: "LUMO+1": n_occ + 1, } orb_idx = idx_map.get(orbital_label) - # UXP.4: "MO " labels carry an explicit 0-based index from By-index mode. + # "MO " labels carry an explicit 0-based index from By-index mode. if orb_idx is None: _m = _re.match(r"MO\s+(\d+)$", orbital_label) if _m: @@ -1264,7 +1264,7 @@ def _show_range_err() -> None: # Charge/spin aren't carried on the app's orbital-state attributes — # infer them from the MO occupations so charged/open-shell molecules - # (H3O+, OH-, radicals, ...) don't fail to build in PySCF (H1 fix). + # (H3O+, OH-, radicals, ...) don't fail to build in PySCF. _charge, _spin = infer_charge_and_spin(mol_atom, mo_occ_for_charge) generate_cube_from_arrays( mol_atom, @@ -1340,7 +1340,7 @@ def _show_err(msg: str = err_msg) -> None: return if _is_stale(): return - # M-EXPORT / EXPORT.5: track the last-generated cube + its orbital + # Track the last-generated cube + its orbital # label so the "Export cube" button can copy it to the top-level # result dir with a friendly name without re-deriving the path. app._last_cube_path = cube_path @@ -1365,7 +1365,7 @@ def _show_err(msg: str = err_msg) -> None: html_str, ) - # M-EXPORT / EXPORT.5: now that ``_last_cube_path`` is populated, the + # Now that ``_last_cube_path`` is populated, the # "Export cube" button has something to copy. Enable it on the main # thread alongside the iso render swap. def _enable_cube_btn() -> None: @@ -1571,7 +1571,7 @@ def _render_vib_mode_py3dmol( ) -> None: """Render vibrational animation via py3Dmol multi-frame XYZ. - Per VIZBACK.8 spec: pure-numpy frame generation (no plotlymol3d + Pure-numpy frame generation (no plotlymol3d dependency); 24 sinusoidal-phase frames over one full oscillation; py3Dmol view with ``addModelsAsFrames`` + ``animate``; serialized to HTML and atomically swapped into ``app.vib_output``. @@ -1609,7 +1609,7 @@ def _render_vib_mode_py3dmol( _vib_err(app, "No frequency result cached for vibrational animation.") return - # Cache hit short-circuit (VIZBACK.9). The cache key now includes ``fps`` + # Cache hit short-circuit. The cache key now includes ``fps`` # so a user who changes the framerate will rebuild rather than play back # a mismatched-interval HTML blob. result_dir = getattr(app, "_last_result_dir", None) @@ -1714,7 +1714,7 @@ def _render_vib_mode_py3dmol( _swap_vib_output(app, html_str) # Persist to disk cache so future visits and history replay can hit - # this mode instantly (VIZBACK.9). Non-fatal on failure — render still + # this mode instantly. Non-fatal on failure — render still # succeeded, cache is purely an optimization. if result_dir is not None: try: @@ -2084,7 +2084,7 @@ def build_preopt_preview_html( ``viewer.setFrame`` so the user can compare geometries without re-rendering. A single-frame trajectory (no relaxation / FF fallback) renders as a static structure with no controls. Used by the interactive "Preview - pre-optimization" flow (M-PREOPT PREOPT.2). + pre-optimization" flow. """ import re diff --git a/quantui/benchmarks.py b/quantui/benchmarks.py index b1dd05f..9f19ec0 100644 --- a/quantui/benchmarks.py +++ b/quantui/benchmarks.py @@ -7,8 +7,8 @@ :func:`~quantui.calc_log.estimate_time` immediately becomes useful on a fresh install. -Four tiers (M-EST / EST.4, 2026-05-25) --------------------------------------- +Four tiers (2026-05-25) +----------------------- The calibration suite is now a **four-tier cascade** rather than the original short/long pair. Users pick the depth that matches their setup- @@ -320,7 +320,7 @@ "RHF", "STO-3G", ), - # ── M-EST / EST.4 expansion (2026-05-25) ────────────────────────────── + # ── Expansion (2026-05-25) ──────────────────────────────────────────── # Additional SP entries that broaden the method × basis grid coverage, # extending tier 2's expected wall-clock to the 3-5 min target. ( @@ -641,7 +641,7 @@ def _normalize_entry(entry: tuple) -> dict: # --------------------------------------------------------------------------- -# Cross-device probe (M-EST / EST.5, 2026-05-25) +# Cross-device probe (2026-05-25) # --------------------------------------------------------------------------- # # When GPU offload is available, tier 3 and tier 4 calibrations should run @@ -727,10 +727,10 @@ class BenchmarkStep: elapsed_s: float = 0.0 error_msg: str = "" n_basis: Optional[int] = None - # M-EST / EST.4: track which calc-type this step ran so tier 3+4 + # Track which calc-type this step ran so tier 3+4 # entries can be distinguished in summaries. calc_type: str = "single_point" - # M-EST follow-up (2026-05-25 user request): the calibration worker + # Follow-up (2026-05-25): the calibration worker # now saves each step as a real result directory (via save_result) # so users can re-open them from the History tab like any other # calc. ``None`` when save_result failed (best-effort) or the step @@ -746,7 +746,7 @@ class CalibrationResult: steps: List[BenchmarkStep] = field(default_factory=list) stopped_early: bool = False mode: str = "tier1" - # EST.5 cross-device probe expands the execution plan beyond + # The cross-device probe expands the execution plan beyond # ``len(_MODE_TO_SUITE[mode])`` for tier 3/4 on GPU hosts. Store # the plan length explicitly so progress denominators stay correct; # 0 (default) means "fall back to suite size" for back-compat with @@ -780,12 +780,12 @@ def _count_electrons(atoms: list[str], charge: int) -> int: # --------------------------------------------------------------------------- -# Subprocess worker (M-EST follow-up, 2026-05-25) +# Subprocess worker (2026-05-25) # --------------------------------------------------------------------------- # # Originally calibration ran each step in a ThreadPoolExecutor with a # ``future.result(timeout=...)`` block. That had three blockers exposed by -# the user's tier-4 attempt (session 55): +# a tier-4 attempt: # # 1. The Stop button only checked between steps, so an in-flight 5-minute # freq calc could not be killed mid-run. @@ -984,7 +984,7 @@ def _calibration_worker( ``force_cpu=True`` sets ``QUANTUI_DISABLE_GPU=1`` in the worker's environment BEFORE any quantui / gpu4pyscf import so the cached ``is_gpu_available()`` probe sees the override and the calc actually - runs on CPU. Used by the EST.5 cross-device probe — tier 3/4 on a + runs on CPU. Used by the cross-device probe — tier 3/4 on a GPU host runs selected entries twice (once forced-CPU, once GPU) so the analytics speedup table is populated from one calibration run. @@ -1002,7 +1002,7 @@ def _calibration_worker( from datetime import datetime as _dt from pathlib import Path as _P - # EST.5: must run BEFORE any quantui / pyscf / gpu4pyscf import so + # Must run BEFORE any quantui / pyscf / gpu4pyscf import so # the ``is_gpu_available()`` cache sees the override on first probe. if force_cpu: _os.environ["QUANTUI_DISABLE_GPU"] = "1" @@ -1057,7 +1057,7 @@ def _calibration_worker( # verbose=3 gives per-iteration SCF energies in the log — # enough signal to confirm the worker hasn't frozen on a - # slow tier-4 entry. (Was verbose=0 pre-session-55.) + # slow tier-4 entry. (Was verbose=0 previously.) res = _sp( mol, method=method, @@ -1072,9 +1072,9 @@ def _calibration_worker( elapsed = _t.perf_counter() - t0 log_fh.write(f"\n[QuantUI_STATUS] COMPLETED in {elapsed:.2f} s\n") - # Save as a regular result directory (M-EST follow-up, - # 2026-05-25 user request — tier 4's MP2 + CCSD + benzene - # freq are scientifically valuable; don't discard them). + # Save as a regular result directory (2026-05-25 — tier 4's + # MP2 + CCSD + benzene freq are scientifically valuable; + # don't discard them). saved_dir = _save_calibration_step( res, calc_type=calc_type, @@ -1230,10 +1230,10 @@ def run_calibration( steps are abandoned (no further work). timeout_per_step: Wall-clock seconds allowed per step. ``None`` (default) means no timeout — the user controls - stoppage via the Stop / Skip buttons. The session-55 tier-4 + stoppage via the Stop / Skip buttons. A tier-4 run had a benzene B3LYP/6-31G* freq calc finish at ~1500 s but be cut off at the old 1800 s hard cap, losing - the data; the no-timeout default removes that footgun. + the data; the no-timeout default removes that hazard. Pass a numeric value only when running headlessly (e.g. CI) where you genuinely want a wall-clock cap. mode: One of ``"tier1"`` / ``"tier2"`` / ``"tier3"`` / ``"tier4"``. @@ -1270,7 +1270,7 @@ def run_calibration( mode = "tier1" suite = _MODE_TO_SUITE[mode] - # EST.5: probe GPU availability once in the parent so we know whether + # Probe GPU availability once in the parent so we know whether # to duplicate cross-device entries. Failure (e.g. gpu_offload import # error on a misconfigured install) defaults to "no GPU" — the # calibration still runs, it just doesn't collect speedup pairs. @@ -1309,7 +1309,7 @@ def run_calibration( # the per-step progress trail. pass - # Use ``spawn`` everywhere (session 55 follow-up): ``fork`` from a + # Use ``spawn`` everywhere: ``fork`` from a # background thread (run_calibration runs inside ``_do_calibration`` # which is itself a daemon thread) collides hard with CUDA contexts # that the parent process may have initialized via the GPU-detection @@ -1394,7 +1394,7 @@ def _emit_progress(*args, live_message=None, step=None) -> None: str(log_path), result_queue, timestamp, # calibration_run_id — the parent's run timestamp - force_cpu, # EST.5 cross-device probe flag + force_cpu, # cross-device probe flag ), daemon=True, ) @@ -1413,7 +1413,7 @@ def _emit_progress(*args, live_message=None, step=None) -> None: break # Timeout is now opt-in (was a hard 1800 s for tier 4 which - # cut off a near-finishing benzene freq in session 55). + # cut off a near-finishing benzene freq). # ``None`` means "user controls; never auto-kill". if timeout_per_step is not None and elapsed > timeout_per_step: worker.terminate() @@ -1435,8 +1435,8 @@ def _emit_progress(*args, live_message=None, step=None) -> None: # Skip = "abandon THIS step, continue to the next." Distinct # from Stop. Clear the event after consuming so the next # step starts fresh — the UI re-sets it if the user clicks - # Skip again. (session 55 user request — replaces the - # hard timeout that was cutting off near-finishing calcs.) + # Skip again. (Replaces the hard timeout that was cutting + # off near-finishing calcs.) if skip_event is not None and skip_event.is_set(): worker.terminate() worker.join(timeout=5) @@ -1462,8 +1462,8 @@ def _emit_progress(*args, live_message=None, step=None) -> None: # the queue. Capture the exit code + the tail of the # calibration log so the user can see what actually # happened — "worker exited without result" alone is - # useless for diagnosis (the original session-55 - # symptom of every step failing at 0.04 s). + # useless for diagnosis (the original symptom of every + # step failing at 0.04 s). _exitcode = getattr(worker, "exitcode", None) _tail = _tail_last_status_line(log_path) or "(no log output)" _hint = "" @@ -1512,7 +1512,7 @@ def _emit_progress(*args, live_message=None, step=None) -> None: ) result.steps.append(step) - # Fix 2: persist after EVERY step so an interrupt at step N + # Persist after EVERY step so an interrupt at step N # still leaves a partial-state record on disk. _save_calibration_json(result, log_path) diff --git a/quantui/c_stderr.py b/quantui/c_stderr.py index a714518..ea5a2de 100644 --- a/quantui/c_stderr.py +++ b/quantui/c_stderr.py @@ -1,4 +1,4 @@ -"""POSIX file-descriptor stderr capture (M-STDERR / STDERR.1). +"""POSIX file-descriptor stderr capture. PySCF and its C-extension dependencies (libcint, BLAS/LAPACK, dftd3) write diagnostic messages directly to file-descriptor 2 (the OS-level stderr), diff --git a/quantui/calc_log.py b/quantui/calc_log.py index cebc673..80be74f 100644 --- a/quantui/calc_log.py +++ b/quantui/calc_log.py @@ -270,9 +270,9 @@ def _event_path() -> Path: def _prediction_log_path() -> Path: - """Path to ``prediction_log.jsonl`` — the M-EST / EST.6 file - capturing one record per ``_do_run`` invocation with the - estimator's pre-run prediction and the actual wall-clock outcome. + """Path to ``prediction_log.jsonl`` — the file capturing one record + per ``_do_run`` invocation with the estimator's pre-run prediction + and the actual wall-clock outcome. Kept indefinitely (like ``perf_log.jsonl``) so the analytics dashboard can plot prediction accuracy over time without manual @@ -359,7 +359,7 @@ def count_basis_functions(atoms: list[str], basis: str) -> Optional[int]: # --------------------------------------------------------------------------- -# Statistical helpers (M-EST / EST.3, 2026-05-25) +# Statistical helpers (2026-05-25) # --------------------------------------------------------------------------- @@ -407,7 +407,7 @@ def _coefficient_of_variation(values: list[float]) -> float: def _confidence_label(values: list[float], n_samples: int) -> str: - """Variance-aware confidence label (M-EST / EST.3). + """Variance-aware confidence label. Combines coefficient of variation (CV) with sample count: @@ -460,8 +460,8 @@ def log_calculation( ) -> None: """Append one performance record to ``perf_log.jsonl``. - ``gpu_used`` / ``gpu_name`` (added M-GPU follow-up, 2026-05-25) record - whether GPU offload was active for the run; reading these back lets + ``gpu_used`` / ``gpu_name`` (added 2026-05-25) record whether GPU + offload was active for the run; reading these back lets ``quantui.analytics.build_dashboard`` compute GPU-vs-CPU speedups across runs of the same (method, basis, formula) tuple. """ @@ -486,14 +486,14 @@ def log_calculation( record["gpu_used"] = bool(gpu_used) if gpu_name is not None: record["gpu_name"] = gpu_name - # B3: outer-loop step count (geom-opt BFGS steps / PES scan points). Enables + # Outer-loop step count (geom-opt BFGS steps / PES scan points). Enables # a history-based "step k / ~N" prior for the live progress fraction. if n_steps is not None: record["n_steps"] = n_steps _append(_perf_path(), record) -#: Hessian-cost multipliers used by the EST.2 frequency cost model. +#: Hessian-cost multipliers used by the frequency cost model. #: PySCF's analytical Hessian for HF/DFT runs in ~2-3× SCF time; for #: post-HF methods it falls back to numerical Hessian which is much #: more expensive (effectively 6N SCFs by itself, on top of the IR @@ -516,7 +516,7 @@ def _estimate_frequency_cost( n_cores: Optional[int] = None, gpu_used: Optional[bool] = None, ) -> Optional[dict]: - """EST.2: structured frequency-time estimate from an SP anchor. + """Structured frequency-time estimate from an SP anchor. Decomposition:: @@ -646,13 +646,13 @@ def estimate_time( (for example, Single Point). Legacy records without ``calc_type`` are only included when estimating ``single_point``. - **GPU-aware filtering** (M-EST / EST.1, 2026-05-25): when ``gpu_used`` + **GPU-aware filtering** (2026-05-25): when ``gpu_used`` is passed, the candidate pool is partitioned by device — GPU-history - predicts GPU runs and CPU-history predicts CPU runs. Records written - before session 55 don't have ``gpu_used`` at all; those are treated + predicts GPU runs and CPU-history predicts CPU runs. Older records + don't have ``gpu_used`` at all; those are treated as "device unknown" and admitted only when ``gpu_used=False`` is requested (the conservative assumption, since QuantUI was CPU-only - before M-GPU shipped). When ``gpu_used=None`` (default), the device + before GPU offload shipped). When ``gpu_used=None`` (default), the device axis is ignored and all records are eligible — back-compat with callers that don't know which device the upcoming run will use. @@ -682,7 +682,7 @@ def estimate_time( scoped = [r for r in converged if r.get("calc_type") == calc_type] if len(scoped) < 2: - # EST.2: frequency calcs can still produce a prediction via the + # Frequency calcs can still produce a prediction via the # SP-anchored cost model even when direct freq history is empty. # The cost model lives at the end of this function — fall through # for freq, bail for everything else. @@ -692,8 +692,8 @@ def estimate_time( # will all no-op (their pool checks require len >= 2), and the # freq cost-model fallback at the end will fire. - # M-EST / EST.1: partition by device when the caller specified one. - # Records pre-dating session 55 don't carry ``gpu_used`` — admit them + # Partition by device when the caller specified one. + # Older records don't carry ``gpu_used`` — admit them # only into the CPU pool, since QuantUI was CPU-only when they were # written. Track whether we downgraded for the fall-back path below. _gpu_filtered = False @@ -743,7 +743,7 @@ def _eff(r: dict) -> Optional[float]: ] effs = [e for r in exact_nb for e in [_eff(r)] if e is not None] if len(effs) >= 2: - # EST.3: drop Tukey outliers before computing the predictor. + # Drop Tukey outliers before computing the predictor. # The variance of the *filtered* pool drives confidence. filtered_effs = _iqr_filter(effs) predicted = ( @@ -823,7 +823,7 @@ def _eff(r: dict) -> Optional[float]: "n_samples": len(same_basis), } - # ── EST.2 frequency cost-model fallback ─────────────────────────────────── + # ── Frequency cost-model fallback ───────────────────────────────────────── # When all four direct-history strategies fail for a freq calc, fall # back to the structural decomposition: SP anchor + Hessian + 6N # inner SCFs. The SP anchor comes from the much richer single-point @@ -848,10 +848,10 @@ def _eff(r: dict) -> Optional[float]: def estimate_opt_steps( method: str, basis: str, calc_type: str = "geometry_opt" ) -> Optional[float]: - """Median historical outer-step count for *calc_type* (B3 progress prior). + """Median historical outer-step count for *calc_type* (progress prior). Reads ``perf_log`` for converged records of *calc_type* that recorded - ``n_steps`` (added B3). Prefers exact method+basis (>= 2 records), falls back + ``n_steps``. Prefers exact method+basis (>= 2 records), falls back to same-basis (>= 2), then to all matching-calc_type records. Returns the median or ``None`` when there is no usable history — a rough prior used only to seed / floor the live progress fraction, not a hard prediction. @@ -922,7 +922,7 @@ def get_perf_history() -> list[dict]: # --------------------------------------------------------------------------- -# Prediction log (M-EST / EST.6, 2026-05-25) +# Prediction log (2026-05-25) # --------------------------------------------------------------------------- # # Captures one record per ``_do_run`` invocation with the estimator's @@ -1017,7 +1017,7 @@ def clear_event_log() -> None: # Event log (7-day TTL) # --------------------------------------------------------------------------- -# M8 audit fix (2026-07-14): log_event() used to call prune_events() after +# Audit fix (2026-07-14): log_event() used to call prune_events() after # every single append, and prune_events() itself read the file (acquiring # and releasing _LOCK) and only later reacquired _LOCK to rewrite it. Two # problems: diff --git a/quantui/cancellation.py b/quantui/cancellation.py index a4b3371..2ae631c 100644 --- a/quantui/cancellation.py +++ b/quantui/cancellation.py @@ -1,9 +1,9 @@ -"""Shared cancellation primitive for in-flight calculations (UXP.5). +"""Shared cancellation primitive for in-flight calculations. The Cancel button sets a ``threading.Event`` on the app; the run's ``_LogCapture`` checks it on every output line and raises :class:`CalcCancelled` at the next line — a cooperative stop that needs no -(unsafe) thread-kill. UXP.5 tightens that latency by also attaching the same +(unsafe) thread-kill. This module tightens that latency by also attaching the same check to PySCF SCF callbacks and ASE optimizer observers, so cancellation fires between SCF cycles / optimizer steps even when the calc is running silently (verbose=0) and no output line is triggering the stream check. @@ -57,9 +57,9 @@ def attach_scf_cancel_callback( is the kernel's locals dict — cycle index, ``e_tot``, etc.). We use that single hook for two things: - - **Cancel** (UXP.5): raise :class:`CalcCancelled` between cycles so a + - **Cancel**: raise :class:`CalcCancelled` between cycles so a Cancel click stops the run regardless of whether output is streamed. - - **Progress** (M-PROGRESS A3): call *progress_cb(envs)* each cycle so the + - **Progress**: call *progress_cb(envs)* each cycle so the caller can surface a live "SCF cycle N" status even when the SCF runs at ``verbose=0`` (the optimizer/PES per-step SCF, which streams nothing). diff --git a/quantui/descriptor_cards.py b/quantui/descriptor_cards.py index f18d19b..464c015 100644 --- a/quantui/descriptor_cards.py +++ b/quantui/descriptor_cards.py @@ -1,4 +1,4 @@ -"""Method / basis descriptor cards (UXP.7 — FR-DESCRIPTOR-CARDS). +"""Method / basis descriptor cards. Replaces the inline multi-paragraph educational-notes block next to the method / basis dropdowns with two compact "descriptor cards" — an icon, a diff --git a/quantui/freq_calc.py b/quantui/freq_calc.py index 8c16f87..5e58411 100644 --- a/quantui/freq_calc.py +++ b/quantui/freq_calc.py @@ -186,7 +186,7 @@ def run_freq_calc( stream: IO[str] = progress_stream if progress_stream is not None else sys.stdout - # M-STDERR / STDERR.1: see quantui/c_stderr.py — captures fd-2 stderr + # See quantui/c_stderr.py — captures fd-2 stderr # from libcint / BLAS / LAPACK / Hessian C code and relays to ``stream`` # on exit. POSIX-only; no-op on Windows. from quantui.c_stderr import capture_c_stderr @@ -217,7 +217,7 @@ def _run_freq_calc_body( _pyscf_thermo: Any, stream: IO[str], ) -> FreqResult: - """Inner body of :func:`run_freq_calc` (split out for STDERR.1 wrap).""" + """Inner body of :func:`run_freq_calc` (split out for stderr-capture wrap).""" dft, gto, scf, pyscf_thermo = _dft, _gto, _scf, _pyscf_thermo def _status(msg: str) -> None: @@ -244,7 +244,7 @@ def _status(msg: str) -> None: elif method_upper == "UHF": mf = scf.UHF(mol) else: - # session 55: route through resolve_xc + maybe_apply_d3 so + # Route through resolve_xc + maybe_apply_d3 so # methods like wB97X-D (PySCF rejects "wb97x-d") map to the # bare functional + external D3 dispersion. from .session_calc import maybe_apply_d3, resolve_xc @@ -253,7 +253,7 @@ def _status(msg: str) -> None: mf.xc = resolve_xc(method) mf = maybe_apply_d3(mf, method, progress_stream=stream) - # UXP.5: cooperative cancel between SCF cycles (the Hessian block that + # Cooperative cancel between SCF cycles (the Hessian block that # follows is a single long native call the callback can't interrupt). from .cancellation import attach_scf_cancel_callback, cancel_check_from_stream @@ -316,9 +316,9 @@ def _status(msg: str) -> None: for atom, coords in zip(molecule.atoms, molecule.coordinates) ] except Exception as exc: - # Same class as session_calc bug-A: silent failure here ships - # a FreqResult with no MO data, breaking the Energies panel on - # history replay. Log to surface in the Log tab. + # Silent failure here ships a FreqResult with no MO data, + # breaking the Energies panel on history replay. Log to surface + # in the Log tab. logger.warning( "MO data extraction failed in freq calc for %s: %s", molecule.get_formula(), @@ -398,7 +398,7 @@ def _status(msg: str) -> None: _dm0 = mf.make_rdm1() _dpdx = _np_ir.zeros((_n_ir * 3, 3)) _xc = getattr(mf, "xc", None) - # M5 audit fix (2026-07-14): whether the inner displaced-SCF + # Fix (2026-07-14): whether the inner displaced-SCF # loop needs an unrestricted (UHF/UKS) object is determined # by _dm0's actual shape — (2, nao, nao) for UHF/UKS/ROHF, # (nao, nao) for RHF/RKS — NOT by mol.spin == 0. Those two @@ -421,7 +421,7 @@ def _status(msg: str) -> None: # Inner-SCF helper: builds the right RHF/UHF/RKS/UKS object # for the current ``mol`` geometry, attempts gpu4pyscf - # offload (M-GPU extension to the IR-intensity loop — + # offload (GPU extension to the IR-intensity loop — # without this wrap, the per-displacement SCFs run on CPU # even when the outer SCF was GPU-offloaded), and returns # the dipole moment as a numpy array. Used for both +Δ and diff --git a/quantui/gpu_offload.py b/quantui/gpu_offload.py index 3f7916d..a3b9d77 100644 --- a/quantui/gpu_offload.py +++ b/quantui/gpu_offload.py @@ -1,9 +1,9 @@ -"""GPU offload helpers (M-GPU / GPU.1). +"""GPU offload helpers. Wraps the runtime decision "should this SCF object be migrated to GPU?". Detection probes ``gpu4pyscf`` + ``cupy`` for a CUDA-capable device; if anything is missing or broken the helpers silently report "no GPU" so the -caller falls back to CPU. This means M-GPU integration is safe to leave +caller falls back to CPU. This means GPU integration is safe to leave enabled by default on every platform — Windows users without CUDA, WSL users without gpu4pyscf installed, and remote machines with broken NVIDIA drivers all converge to the same "CPU" outcome with no exception leakage. @@ -39,7 +39,7 @@ # # - ``CCSD(T)`` is documented as unsupported in the gpu4pyscf README. # - ``MP2`` and ``CCSD`` are labelled "experimental" by gpu4pyscf and -# were observed (session 55, 2026-05-25 user tier-4 run) to fail +# were observed (2026-05-25) to fail # immediately after a successful RHF reference on GPU — the failure # fingerprint was "step completed in RHF wall time + small delta, # then errored", which fits the post-HF code choking on a diff --git a/quantui/log_utils.py b/quantui/log_utils.py index 51faa07..f85e20b 100644 --- a/quantui/log_utils.py +++ b/quantui/log_utils.py @@ -176,7 +176,7 @@ def get_system_info() -> Dict[str, Any]: # ============================================================================ -# Live run-status helpers (M-PROGRESS Phase A) +# Live run-status helpers # ============================================================================ @@ -201,10 +201,10 @@ def emit_status(stream: Any, message: str) -> None: def emit_progress(stream: Any, fraction: float) -> None: """Report a completion fraction (0..1) via a progress stream, if supported. - M-PROGRESS B2: calc modules that know a real completion fraction (PES scan + Calc modules that know a real completion fraction (PES scan points, optimizer fmax-convergence trend) call this so the live ticker can show a *self-correcting* ``elapsed·(1−f)/f`` remaining-time estimate instead - of the static B1 total. Duck-typed on ``stream.set_progress_fraction`` — a + of the static total. Duck-typed on ``stream.set_progress_fraction`` — a no-op for plain streams, keeping calc modules decoupled from the widget layer. """ setter = getattr(stream, "set_progress_fraction", None) diff --git a/quantui/nmr_calc.py b/quantui/nmr_calc.py index fc35ac2..c6476c8 100644 --- a/quantui/nmr_calc.py +++ b/quantui/nmr_calc.py @@ -37,7 +37,7 @@ class NMRResult: formula: str reference_compound: str = "TMS" converged: bool = True - # M4 audit fix (2026-07-14): which config.NMR_REFERENCE_SHIELDINGS entry + # Fix (2026-07-14): which config.NMR_REFERENCE_SHIELDINGS entry # was actually applied, and whether it's an exact match for method/basis # or a fallback. NMR_REFERENCE_SHIELDINGS only tabulates a handful of # method/basis combinations; any other combination previously fell back @@ -140,7 +140,7 @@ def run_nmr_calc( stream = progress_stream if progress_stream is not None else sys.stdout - # M-STDERR / STDERR.1: see quantui/c_stderr.py — captures fd-2 stderr + # See quantui/c_stderr.py — captures fd-2 stderr # from libcint / BLAS / LAPACK / GIAO / NMR-CPHF C code and relays to # ``stream`` on exit. POSIX-only; no-op on Windows. from quantui.c_stderr import capture_c_stderr @@ -158,7 +158,7 @@ def run_nmr_calc( ) -# L audit fix (2026-07-14): bump this whenever the patch bodies below +# Fix (2026-07-14): bump this whenever the patch bodies below # change — it doubles as the idempotency sentinel's value, so a bumped # version forces re-patching instead of silently keeping stale closures # from an older QuantUI version installed earlier in the process. @@ -329,7 +329,7 @@ def _run_nmr_calc_body( _scf: Any, stream: Any, ) -> NMRResult: - """Inner body of :func:`run_nmr_calc` (split out for STDERR.1 wrap).""" + """Inner body of :func:`run_nmr_calc` (split out for stderr-capture wrap).""" dft, gto, scf = _dft, _gto, _scf import numpy as _np @@ -352,7 +352,7 @@ def _run_nmr_calc_body( elif method_upper == "UHF": mf = scf.UHF(mol) else: - # session 55: route through resolve_xc + maybe_apply_d3 so + # Route through resolve_xc + maybe_apply_d3 so # wB97X-D / PBE-D3 work for NMR calcs (was using raw _XC_ALIAS # lookup before, which would fail for wB97X-D after the alias # change to "wb97x" + external D3). @@ -360,13 +360,13 @@ def _run_nmr_calc_body( mf.xc = resolve_xc(method) mf = maybe_apply_d3(mf, method, progress_stream=stream) - # UXP.5: cooperative cancel between SCF cycles. + # Cooperative cancel between SCF cycles. from .cancellation import attach_scf_cancel_callback, cancel_check_from_stream from .log_utils import emit_status attach_scf_cancel_callback(mf, cancel_check_from_stream(stream)) - emit_status(stream, "Running SCF…") # M-PROGRESS A4 + emit_status(stream, "Running SCF…") try: mf.kernel() except Exception as exc: @@ -390,7 +390,7 @@ def _run_nmr_calc_body( _ensure_nmr_compat_patches_applied() - emit_status(stream, "Computing NMR shielding tensors (GIAO)…") # M-PROGRESS A4 + emit_status(stream, "Computing NMR shielding tensors (GIAO)…") try: if method_upper == "RHF": nmr_obj = _pyscf_nmr.RHF(mf) diff --git a/quantui/optimizer.py b/quantui/optimizer.py index 2c7eeb7..6abd3f3 100644 --- a/quantui/optimizer.py +++ b/quantui/optimizer.py @@ -104,16 +104,16 @@ def __init__( self.basis = basis self.charge = charge self.spin = spin - # UXP.5: cooperative-cancel predicate; checked per step + wired into + # Cooperative-cancel predicate; checked per step + wired into # the per-step SCF callback (the SCF runs silent here, so the # stream-based cancel can't see it). self.cancel_check = cancel_check - # M-PROGRESS A2: progress stream + label for per-step status + # Progress stream + label for per-step status # heartbeats (the SCF runs at verbose=0, so nothing else surfaces # progress during a step). ``_eval_count`` counts force evaluations. self.progress_stream = progress_stream self.status_label = status_label - self.expected_steps = expected_steps # B3: history-based ~N prior + self.expected_steps = expected_steps # history-based ~N prior self._eval_count = 0 def calculate( @@ -124,7 +124,7 @@ def calculate( ) -> None: super().calculate(atoms, properties, system_changes) - # UXP.5: bail before starting this step's SCF if cancel was clicked + # Bail before starting this step's SCF if cancel was clicked # (fires between BFGS force evaluations, independent of ASE output). from .cancellation import ( attach_scf_cancel_callback, @@ -133,7 +133,7 @@ def calculate( raise_if_cancelled(self.cancel_check) - # M-PROGRESS A2: heartbeat so the status line advances during the + # Heartbeat so the status line advances during the # (silent) per-step SCF + gradient. self._eval_count += 1 from .log_utils import emit_status @@ -181,7 +181,7 @@ def calculate( elif method_upper == "UHF": mf = scf.UHF(mol) else: - # DFT functional. session 55: route through resolve_xc + + # DFT functional. Route through resolve_xc + # maybe_apply_d3 so wB97X-D / PBE-D3 work mid-optimization. from .session_calc import maybe_apply_d3, resolve_xc @@ -192,7 +192,7 @@ def calculate( mf.verbose = 0 mf.stdout = _sink - # M-PROGRESS A3: per-SCF-cycle heartbeat during the (silent) step, + # Per-SCF-cycle heartbeat during the (silent) step, # so the status advances mid-SCF, not just per optimizer step. _k = self._eval_count @@ -457,7 +457,7 @@ def optimize_geometry( expected_steps=expected_steps, ) - # M-STDERR / STDERR.1: PySCF gradients (called by ASE-BFGS at every + # PySCF gradients (called by ASE-BFGS at every # step) emit fd-2 stderr from libcint / BLAS. Wrap the full BFGS run # in capture_c_stderr so those bytes go to _stream instead of the red- # text channel. POSIX-only; no-op on Windows. @@ -474,12 +474,12 @@ def optimize_geometry( trajectory=str(traj_path), logfile=_stream, # BFGS step table → progress_stream ) - # UXP.5: check cancel after every BFGS step (belt-and-suspenders + # Check cancel after every BFGS step (belt-and-suspenders # with the per-step calculator check above). if _cancel_check is not None: dyn.attach(lambda: raise_if_cancelled(_cancel_check), interval=1) - # M-PROGRESS B2: estimate completion from the fmax-convergence trend + # Estimate completion from the fmax-convergence trend # (log-scale between the first step's fmax and the target). Data-free # and self-correcting. Skipped when report_fraction is False (e.g. # reorg drives several sub-optimizations, whose 0→1 resets would make @@ -502,7 +502,7 @@ def _report_opt_fraction() -> None: return denom = math.log(_fmax0[0] / fmax) if fmax > 0 else 0.0 frac = math.log(_fmax0[0] / fmax_now) / denom if denom > 0 else 0.0 - # B3: floor with the history-based step prior so early steps + # Floor with the history-based step prior so early steps # (where the fmax trend is noisy / near 0) still advance. if expected_steps: step = getattr(atoms.calc, "_eval_count", 0) @@ -572,9 +572,9 @@ def _report_opt_fraction() -> None: _opt_mol_atom = _last_atom_list _opt_mol_basis = basis except Exception as exc: - # Bug-A class — silent failure here ships an OptimizationResult - # with no MO data, breaking Energies + Isosurface panels on - # history replay. (Same root-cause class as session_calc.) + # Silent failure here ships an OptimizationResult with no MO data, + # breaking Energies + Isosurface panels on history replay. + # (Same root-cause class as session_calc.) logger.warning( "Final-step MO extraction failed in optimizer for %s: %s", molecule.get_formula(), diff --git a/quantui/pes_scan.py b/quantui/pes_scan.py index 12a93a7..8140369 100644 --- a/quantui/pes_scan.py +++ b/quantui/pes_scan.py @@ -79,7 +79,7 @@ class PESScanResult: def _finite_energies(self) -> List[float]: """``energies_hartree`` with failed-point NaN placeholders dropped. - M6 audit fix (2026-07-14): a failed scan point appends + Fix (2026-07-14): a failed scan point appends ``float("nan")`` to ``energies_hartree`` (see :func:`run_pes_scan`). Python's ``min``/``max`` are order-dependent with NaN present — a NaN as the first element "wins" (everything compares False against @@ -290,7 +290,7 @@ def run_pes_scan( _stream: IO[str] = progress_stream if progress_stream is not None else sys.stdout _null = io.StringIO() - # UXP.5: cooperative cancel — checked between scan points, per BFGS step, + # Cooperative cancel — checked between scan points, per BFGS step, # and inside each point's SCF (via the shared calculator). from .cancellation import cancel_check_from_stream, raise_if_cancelled @@ -304,7 +304,7 @@ def run_pes_scan( energies_hartree: List[float] = [] coordinates_list: List[Molecule] = [] converged_all = True - # M6 audit fix (2026-07-14): on a failed scan point, fall back to the + # Fix (2026-07-14): on a failed scan point, fall back to the # last successfully-computed geometry rather than the original input # molecule. Snapping every failed frame back to the starting geometry # produced a bogus discontinuity in the trajectory animation/plot — @@ -318,7 +318,7 @@ def run_pes_scan( for step_num, val in enumerate(scan_values, start=1): raise_if_cancelled(_cancel_check) - # M-PROGRESS A2/B2: live per-point status + exact completion fraction + # Live per-point status + exact completion fraction # (points already done / total) for the self-correcting time estimate. from .log_utils import emit_progress, emit_status @@ -348,7 +348,7 @@ def run_pes_scan( constraint = FixInternals(bonds=[[val, [i1, i2]]]) elif scan_type == "angle": atoms.set_angle(i1, i2, i3, val) - # M7 audit fix (2026-07-14): ASE's radian-based `angles=` + # (2026-07-14): ASE's radian-based `angles=` # kwarg is not just deprecated, it's flat-out broken with # the currently-targeted ASE (>=3.22, verified against # 3.29.0) — internally it does @@ -370,7 +370,7 @@ def run_pes_scan( dyn = BFGS(atoms, logfile=_stream) if _cancel_check is not None: dyn.attach(lambda: raise_if_cancelled(_cancel_check), interval=1) - # M-STDERR / STDERR.1: capture fd-2 stderr from PySCF C + # Capture fd-2 stderr from PySCF C # extensions for the duration of this scan-point optimisation. from quantui.c_stderr import capture_c_stderr diff --git a/quantui/preopt.py b/quantui/preopt.py index 81f077a..fda5875 100644 --- a/quantui/preopt.py +++ b/quantui/preopt.py @@ -17,7 +17,7 @@ If RDKit is unavailable, bond perception fails, or no force field has parameters for the molecule, :func:`preoptimize` returns the **original** geometry unchanged (RMSD 0.0) rather than a mangled one. Pre-opt can only improve or -no-op — never degrade. (M-PREOPT / PREOPT.1.) +no-op — never degrade. Limitation: bond perception is distance-based, so a *wildly* broken input (atoms so far apart or so clashed that bonds can't be inferred) yields the no-op rather @@ -314,8 +314,8 @@ def preoptimize_with_trajectory( Like :func:`preoptimize`, but returns ``(optimized_molecule, rmsd, frames)`` where ``frames`` is a list of per-iteration coordinate snapshots (the starting geometry first, the relaxed geometry last) for animating the - relaxation in the interactive "Preview pre-optimization" flow (M-PREOPT - PREOPT.2). Same non-destructive guarantee: on any failure the original + relaxation in the interactive "Preview pre-optimization" flow. Same + non-destructive guarantee: on any failure the original geometry is returned unchanged with ``rmsd = 0.0`` and a single-frame trajectory (just the input), so the viewer always has something to show. """ diff --git a/quantui/reorganization_energy.py b/quantui/reorganization_energy.py index 55a9823..ca780ec 100644 --- a/quantui/reorganization_energy.py +++ b/quantui/reorganization_energy.py @@ -320,7 +320,7 @@ def _single_point(mol: Molecule, mth: str, tag: str) -> float: steps=steps, progress_stream=stream, # type: ignore[arg-type] status_label="Reorg: optimizing neutral geometry", - report_fraction=False, # B2: don't let sub-opt 0→1 resets oscillate ETA + report_fraction=False, # Don't let sub-opt 0→1 resets oscillate ETA ) neutral_mol = neutral_opt.molecule n_total_steps = neutral_opt.n_steps @@ -364,7 +364,7 @@ def _single_point(mol: Molecule, mth: str, tag: str) -> float: steps=steps, progress_stream=stream, # type: ignore[arg-type] status_label=f"Reorg: optimizing {kind} ion geometry", - report_fraction=False, # B2: see neutral-opt note above + report_fraction=False, # See neutral-opt note above ) ion_mol = ion_opt.molecule n_total_steps += ion_opt.n_steps diff --git a/quantui/results_storage.py b/quantui/results_storage.py index 17b1bc2..54c5cb8 100644 --- a/quantui/results_storage.py +++ b/quantui/results_storage.py @@ -206,8 +206,8 @@ def save_result( "ccsd_t_correction_hartree": _opt_float( getattr(result, "ccsd_t_correction_hartree", None) ), - # Persisted so the saved-result card matches the live card (M-CLEAN - # formatter-parity fix). Additive — absent on older results, where the + # Persisted so the saved-result card matches the live card + # (formatter-parity fix). Additive — absent on older results, where the # history card falls back exactly as before (CPU / no dipole / no # charges). Coerced JSON-safe (numpy scalars/arrays → float/list). "solvent": getattr(result, "solvent", None), @@ -318,7 +318,7 @@ def save_molden( normal_modes=None, filename: str = "result.molden", ) -> Optional[Path]: - """Write a Molden-format file alongside ``result.json`` (M-EXPORT / EXPORT.1+2). + """Write a Molden-format file alongside ``result.json``. Molden is the lingua franca for orbital + vibration interop with Avogadro / IQmol / Jmol / Multiwfn. This helper writes whichever data @@ -450,7 +450,7 @@ def save_trajectory_xyz( energies: list, filename: str = "trajectory.xyz", ) -> Optional[Path]: - """Write a multi-frame XYZ trajectory file (M-EXPORT / EXPORT.3). + """Write a multi-frame XYZ trajectory file. Universal format readable by Avogadro, VMD, OVITO, Jmol, Pymol, OpenBabel, ASE (``ase.io.read``), and basically any molecular tool @@ -508,7 +508,7 @@ def save_trajectory_ase( energies: list, filename: str = "trajectory.traj", ) -> Optional[Path]: - """Write an ASE Trajectory (.traj) file (M-EXPORT / EXPORT.7). + """Write an ASE Trajectory (.traj) file. Lets users open the result in ``ase gui trajectory.traj``, slice frames (``trajectory.traj@0:10:2``), and use ASE-GUI's interactive @@ -568,7 +568,7 @@ def export_cube( *, orbital_label: str = "orbital", ) -> Optional[Path]: - """Copy a cube file to the top-level result dir with a friendly name (EXPORT.5). + """Copy a cube file to the top-level result dir with a friendly name. Internal cube files live in ``/isosurfaces/`` with timestamped filenames (``H2O_HOMO_2026-05-23_19-30-00.cube``) — fine @@ -603,7 +603,7 @@ def export_result_bundle( *, output_dir: Optional[Path] = None, ) -> Optional[Path]: - """Zip an entire result directory for sharing (EXPORT.5 stretch goal). + """Zip an entire result directory for sharing. Produces ``/.zip`` containing every file the calc wrote — ``result.json``, ``pyscf.log``, ``orbitals.npz``, @@ -772,7 +772,7 @@ def save_thumbnail(result_dir: Path, data: dict) -> None: except ImportError: return - # M9 audit fix (2026-07-14): only the matplotlib import itself was + # Fix (2026-07-14): only the matplotlib import itself was # guarded — figure construction, text rendering, and fig.savefig() (a # real filesystem write, so it can hit disk-full / permission errors) # could all raise past this function despite the docstring's promise @@ -818,7 +818,7 @@ def _build_thumbnail_figure(plt: Any, data: dict) -> Any: fg, bg = _colors.get(ct, ("#555555", "#f3f4f6")) ct_label = _ct_labels.get(ct, ct.replace("_", " ").title()) - # POLISH.7 (M-POLISH, 2026-05-25): bumped figsize 2.4→3.6 + dpi 72→144 + # (2026-05-25): bumped figsize 2.4→3.6 + dpi 72→144 # so the History-card text is readable on 1× displays. Source PNG goes # from 173×108 px (~8 KB) to 518×324 px (~25 KB); the History dropdown # downscales to its native ~250–300 px width, so the user sees crisp diff --git a/quantui/session_calc.py b/quantui/session_calc.py index b7dc438..6184962 100644 --- a/quantui/session_calc.py +++ b/quantui/session_calc.py @@ -77,7 +77,7 @@ class SessionResult: # method is ``"CCSD(T)"``. ``None`` for plain CCSD. Again, included in # ``energy_hartree`` when set. ccsd_t_correction_hartree: Optional[float] = None - # GPU offload status (M-GPU / GPU.2). ``gpu_used`` is True only when the + # GPU offload status. ``gpu_used`` is True only when the # SCF object was successfully migrated to gpu4pyscf for this run. # ``gpu_name`` carries the CUDA device name when ``gpu_used`` is True so # the result card can show *which* GPU ran the calc. @@ -157,8 +157,8 @@ def resolve_xc(method: str) -> str: translation. Every DFT entry point — ``session_calc``, ``freq_calc``, ``tddft_calc``, ``optimizer``, ``freq_ir_workers``, ``nmr_calc``, and the script-export path in ``config.py`` — should use this - helper rather than passing ``method`` to PySCF directly. (Before - session 55 they didn't, which is why wB97X-D errored in tier 3 + helper rather than passing ``method`` to PySCF directly. (Previously + they didn't, which is why wB97X-D errored in tier 3 SP calcs but ALSO would have errored in freq / opt / tddft.) """ method_upper = method.upper() @@ -258,7 +258,7 @@ def run_in_session( stream: IO[str] = progress_stream if progress_stream is not None else sys.stdout - # M-STDERR / STDERR.1: capture C-level (fd-2) stderr from libcint / BLAS + # Capture C-level (fd-2) stderr from libcint / BLAS # / LAPACK and relay it to ``stream`` on exit. Without this wrapper, the # bytes surface as red text above the cell output in Voilà / Jupyter. # POSIX-only; no-op on Windows. See quantui/c_stderr.py for design. @@ -348,7 +348,7 @@ def _run_session_calc_body( else: # DFT: resolve alias then auto-select RKS / UKS. ``resolve_xc`` # handles the wB97X-D → wb97x + external D3 dispersion mapping - # (session 55 fix; see _XC_ALIAS docstring). + # (see _XC_ALIAS docstring). if mol.spin == 0: mf = dft.RKS(mol) else: @@ -378,7 +378,7 @@ def _run_session_calc_body( "\n⚠ PCM solvent unavailable — running in gas phase.\n" ) - # --- Try GPU offload (M-GPU / GPU.1) --- + # --- Try GPU offload --- # Migrate the SCF object to gpu4pyscf when (a) the package is installed, # (b) a CUDA device is available, and (c) the method is supported. # Failures fall back to CPU silently — the calc still runs. The @@ -393,7 +393,7 @@ def _run_session_calc_body( except Exception: # noqa: BLE001 — cleanup (progress stream may be closed) pass - # --- Cooperative cancellation (UXP.5) --- + # --- Cooperative cancellation --- # Attach the run's cancel predicate (carried on the progress stream) to # the SCF callback so a Cancel click stops between SCF cycles even when the # calc is running with sparse/no streamed output. @@ -404,7 +404,7 @@ def _run_session_calc_body( attach_scf_cancel_callback(mf, _cancel_check) # --- Run SCF --- - emit_status(stream, "Running SCF…") # M-PROGRESS A4 + emit_status(stream, "Running SCF…") try: energy_hartree = float(mf.kernel()) except Exception as exc: @@ -419,7 +419,7 @@ def _run_session_calc_body( try: from pyscf import mp as _mp - emit_status(stream, "Running MP2 correlation…") # M-PROGRESS A4 + emit_status(stream, "Running MP2 correlation…") _mp2 = _mp.MP2(mf) _e_corr, _ = _mp2.kernel() mp2_correlation_hartree = float(_e_corr) @@ -429,7 +429,7 @@ def _run_session_calc_body( f"MP2 correction failed for {molecule.get_formula()}: {exc}" ) from exc - # --- Coupled cluster correlation (M8.1) --- + # --- Coupled cluster correlation --- # CCSD adds singles + doubles excitations on top of the RHF reference; # CCSD(T) adds a perturbative-triples correction on top of CCSD. Both # report their corrections as separate result fields so the UI can @@ -440,7 +440,7 @@ def _run_session_calc_body( try: from pyscf import cc as _cc - emit_status(stream, "Running CCSD correlation…") # M-PROGRESS A4 + emit_status(stream, "Running CCSD correlation…") _ccsd_obj = _cc.CCSD(mf) _e_corr_ccsd, _t1, _t2 = _ccsd_obj.kernel() ccsd_correlation_hartree = float(_e_corr_ccsd) @@ -451,7 +451,7 @@ def _run_session_calc_body( ) from exc if method_upper == "CCSD(T)": try: - emit_status(stream, "Computing CCSD(T) triples…") # M-PROGRESS A4 + emit_status(stream, "Computing CCSD(T) triples…") _e_t = _ccsd_obj.ccsd_t() ccsd_t_correction_hartree = float(_e_t) energy_hartree += float(_e_t) @@ -511,7 +511,7 @@ def _to_numpy_array(arr: Any) -> Any: mulliken_charges: Optional[List[float]] = None dipole_moment_debye: Optional[float] = None - # M3 audit fix (2026-07-14): both mf.mulliken_pop() and mf.dip_moment() + # Audit fix (2026-07-14): both mf.mulliken_pop() and mf.dip_moment() # are well-defined and work correctly for a genuine UHF object (verified # empirically against PySCF) — the previous ``method_upper != "UHF"`` # guard around this whole block was an unnecessary restriction that @@ -540,7 +540,7 @@ def _to_numpy_array(arr: Any) -> Any: # MO arrays for orbital visualization (non-fatal if extraction fails). # Uses the same ``_to_numpy_array`` CuPy→host helper defined above - # (GPU-offload note, BUG fix 2026-05-25): + # (GPU-offload note, fix 2026-05-25): # when gpu4pyscf migrated ``mf`` to the GPU, ``mf.mo_energy`` / ``mo_coeff`` # / ``mo_occ`` are CuPy arrays. ``numpy.array(cupy_array)`` raises (numpy # refuses implicit device transfers), which silently shipped a @@ -562,7 +562,7 @@ def _to_numpy_array(arr: Any) -> Any: ] _pyscf_mol_basis = basis except Exception as exc: - # Bug-A class (session 55): a silent failure here ships a + # A silent failure here ships a # SessionResult with mo_coeff=None, which makes save_orbitals # no-op and breaks Energies + Isosurface panels on history # replay. Surface to the event log so a future regression is diff --git a/quantui/tddft_calc.py b/quantui/tddft_calc.py index 633fa5b..a0c9312 100644 --- a/quantui/tddft_calc.py +++ b/quantui/tddft_calc.py @@ -156,7 +156,7 @@ def run_tddft_calc( stream: IO[str] = progress_stream if progress_stream is not None else sys.stdout - # M-STDERR / STDERR.1: see quantui/c_stderr.py — captures fd-2 stderr + # See quantui/c_stderr.py — captures fd-2 stderr # from libcint / BLAS / LAPACK / TDA solver C code and relays to # ``stream`` on exit. POSIX-only; no-op on Windows. from quantui.c_stderr import capture_c_stderr @@ -187,7 +187,7 @@ def _run_tddft_calc_body( _scf: Any, stream: IO[str], ) -> TDDFTResult: - """Inner body of :func:`run_tddft_calc` (split out for STDERR.1 wrap).""" + """Inner body of :func:`run_tddft_calc` (split out for stderr-capture wrap).""" dft, gto, scf = _dft, _gto, _scf # ── Build Mole object ──────────────────────────────────────────────────── @@ -209,7 +209,7 @@ def _run_tddft_calc_body( elif method_upper == "UHF": mf = scf.UHF(mol) else: - # session 55: route through resolve_xc + maybe_apply_d3 so + # Route through resolve_xc + maybe_apply_d3 so # methods like wB97X-D (PySCF rejects "wb97x-d") map cleanly. from .session_calc import maybe_apply_d3, resolve_xc @@ -227,13 +227,13 @@ def _run_tddft_calc_body( except Exception: # noqa: BLE001 — cleanup (stream may be closed) pass - # UXP.5: cooperative cancel between SCF cycles. + # Cooperative cancel between SCF cycles. from .cancellation import attach_scf_cancel_callback, cancel_check_from_stream from .log_utils import emit_status attach_scf_cancel_callback(mf, cancel_check_from_stream(stream)) - emit_status(stream, "Running SCF (ground state)…") # M-PROGRESS A4 + emit_status(stream, "Running SCF (ground state)…") try: energy_hartree = float(mf.kernel()) except Exception as exc: @@ -270,7 +270,7 @@ def _run_tddft_calc_body( oscillator_strengths: List[float] = [] try: - emit_status( # M-PROGRESS A4 + emit_status( stream, f"Solving {'TDHF (CIS)' if using_hf else 'TD-DFT'} " f"excited states ({nstates})…", diff --git a/quantui/vib_cache.py b/quantui/vib_cache.py index f3cee29..57a249c 100644 --- a/quantui/vib_cache.py +++ b/quantui/vib_cache.py @@ -1,5 +1,5 @@ """ -Vibrational animation disk cache (VIZBACK.9). +Vibrational animation disk cache. Caches rendered vibrational-animation HTML per calculation result directory so mode switches on repeat visits and history replay are instant — no diff --git a/quantui/visualization_py3dmol.py b/quantui/visualization_py3dmol.py index a6ccaef..52b82df 100644 --- a/quantui/visualization_py3dmol.py +++ b/quantui/visualization_py3dmol.py @@ -426,8 +426,9 @@ def render_molecule_html( Mirrors :func:`display_molecule` but emits a single HTML string so callers can route through an atomic ``Output.outputs`` swap (Rule 6 in ``reflections/01-voila-rendering-and-display.md``) rather than - ``with output: display(viz)`` — the latter is the BUG.6/BUG.7 root cause - family. Errors are caught and returned as inline HTML so the caller sees a + ``with output: display(viz)`` — the latter is a known root-cause + family for trajectory and Analysis-tab rendering regressions. Errors are + caught and returned as inline HTML so the caller sees a visible failure message in the viewer slot instead of a blank 🙁 panel. """ if not is_visualization_available(): diff --git a/quantui/viz_backend_router.py b/quantui/viz_backend_router.py index 326883c..fc3a734 100644 --- a/quantui/viz_backend_router.py +++ b/quantui/viz_backend_router.py @@ -5,8 +5,7 @@ render task, taking into account user preference and installed-package availability. Pure function — no I/O, no widget state, no app reference. -The routing policy mirrors the Capability Routing Policy table in -`M-VIZ-BACKEND` roadmap (`22-m-viz-backend-routing-roadmap.md`). +The routing policy mirrors a capability-based routing policy table. Typical usage -------------