Skip to content

Silent-phase log heartbeat (M-PROGRESS Phase D) + launcher banner, pre-opt suppression, geo-opt seed geometry - #33

Merged
NCCU-Schultz-Lab merged 4 commits into
mainfrom
progress-phase-d-and-ux
Jul 30, 2026
Merged

Silent-phase log heartbeat (M-PROGRESS Phase D) + launcher banner, pre-opt suppression, geo-opt seed geometry#33
NCCU-Schultz-Lab merged 4 commits into
mainfrom
progress-phase-d-and-ux

Conversation

@NCCU-Schultz-Lab

Copy link
Copy Markdown
Collaborator

One milestone package and three UX requests, one commit each.

⚠️ None of this is verified in the real app yet — the suite covers the Python
contracts, not how any of it looks or feels. A Voilà pass is worth doing before
merge; see "What to check live" at the bottom.

M-PROGRESS Phase D (PROG.D1) — silent-phase heartbeat

Measured motivation. An aspirin (21 atoms) B3LYP/6-31G* UV-Vis run printed
nothing for 120 s after converged SCF energy while the TD-DFT solve ran. The
status label advanced the whole time — Phase A covers that — but the output log,
which is what a user actually watches, looked frozen. Phase A solved this for the
status label; this is the same problem for the log.

_LogCapture already sees every write, so it is the natural place to know when
output last happened. A watchdog appends
… still working — <stage> · <elapsed> after _HEARTBEAT_AFTER_S of silence.

25 s, sized from the measurement rather than guessed: ~4 lines across that
120 s gap — enough to prove liveness, few enough to stay quiet. Gaps grow steeply
with system size and aspirin is a small case, so the interval errs short.

Three decisions worth reviewing:

  1. The beat writes directly to the widget, not through write(). That path
    checks cancellation, so a heartbeat routed through it would raise
    _CalcCancelled on the watchdog thread, where nothing can catch it — the
    beat would vanish silently. It would also reset the very timer being measured.
  2. The beat is not written to the capture buffer. getvalue() becomes the
    result directory's pyscf.log, which should stay a faithful record of what
    PySCF emitted; padding it with UI chrome would make a long silent run look
    chatty after the fact. Live view gets the beat, the archive does not.
  3. Any real write resets the timer, so a steadily-printing run never beats.

Started next to self._active_log in _do_run and stopped in the same finally
as the elapsed ticker, so it cannot outlive a run and write into a finished log.

This was only viable because M-LOGSCROLL shipped first. Before route C, a line
appended every 25 s would have yanked the user's scroll position to the bottom on
a timer — the roadmap's sequencing warning was real, not theoretical.

D2 (TD-DFT root progress) and D3 (specific progress for NMR/Hessian/post-HF) stay
open, but Phase D's exit criterion is met by D1 alone: no silent gap can now
exceed ~25 s.

UXP2.2 — QuantUI wordmark in the shell launchers

The launcher terminal opened with two bare lines of text. It is the first thing a
user sees on every run, and for students often the only terminal they ever look at.

launchers/_banner.sh is sourced by launch-native.sh, launch-native.command
and launch-native-jupyter.sh — shared rather than pasted, since three copies of
ASCII art would drift.

  • The art is duplicated from log_utils rather than printed by Python because the
    banner runs before the conda env is activated — there is no interpreter yet.
  • Colour only when stdout is a TTY, so piping or redirecting stays clean.
  • Every call is || true behind a file-exists check: all three launchers run under
    set -eu, and decoration must never be what aborts a launch.
  • Windows .bat launchers deliberately excluded — batch escaping of the
    backslashes and pipes in the art is error-prone and unverifiable from this dev
    environment.

UXP2.3 — no pre-opt preview when the geometry barely moves

"It's confusing to have them pop up if the animation doesn't really show any
evolution and the original geometry is effectively the same as the pre-optimized
one."

Below 0.05 Å RMS displacement the preview reports the number and stops: no
animation, no Keep/Revert, and _preopt_relaxed_mol left unset so there is
nothing to accept. The status line says what happens next — the calculation uses
the geometry as-is.

  • Threshold rationale: bond lengths are ~1.0–1.5 Å, so 0.05 Å RMS is a few
    percent and invisible at viewer scale; perceptible motion starts near 0.1 Å.
    Deliberately conservative — showing a real-but-small change is far less bad than
    hiding one. Easy to tune if it silences previews you wanted.
  • The old 1e-3 Å test only chose wording; it distinguished mathematically-zero
    from nonzero, which is not the question a user is asking.
  • The meaningful-change path explicitly restores both panes, so a negligible
    preview followed by a real one cannot leave Keep/Revert invisible (tested).

UXP2.4 — seed geometry for Geometry Opt

"I might optimize at a lower level of theory and then input that as the starting
point for a higher level of theory."

Frequency and UV-Vis already supported this; Geometry Opt did not. Same filtered
dropdown (the _refresh_seed_options helper was already shared), plus consumption
in _do_run — when a seed is selected the optimisation starts from that result's
final geometry, logged with source, formula and atom count.

One deliberate asymmetry: on_geo_seed_changed does not disable
_freq_preopt_cb the way the other two handlers do. That checkbox means "optimise
before the calculation", which is meaningless when the optimisation is the
calculation. A test guards it so a future "consistency" pass doesn't quietly add it.

Known debt, recorded not hidden: this makes three near-duplicate seed
widget groups differing only in note text and that checkbox behaviour. Only one
calc type is visible at a time, so they could collapse into one — but that refactor
touches two working paths, so it is filed as M-UX2 UXP2.5 rather than bundled into
a feature commit.

Testing

  • tests/test_log_heartbeat.py (12) — fires during silence, repeats, carries the
    stage and elapsed, stays silent while output flows, stops cleanly, is idempotent,
    tolerates a missing status label, keeps out of the capture buffer, and is not
    suppressed by a pending cancellation.
  • tests/test_geo_seed_geometry.py (10) — including a regression guard that the
    refresh button targets the geo dropdown (an early revision bound it to the
    Frequency button) and that each calc type keeps its own dropdown.
  • tests/test_preopt_preview.py — 5 new cases covering the threshold boundary in
    both directions and pane restoration.
  • Full suite 1968 passed, 17 skipped; pre-commit clean.

What to check live

  1. Heartbeat — run a UV-Vis job with some heft and confirm the log shows
    … still working during the TD-DFT solve, at a rate that reassures rather than
    nags.
  2. Launcher banner — colours and alignment in your actual terminal.
  3. Pre-opt — a geometry that barely moves should show only the message; one
    that really relaxes should still animate with Keep/Revert.
  4. Geo-opt seed — the dropdown appears under Geometry Opt, lists prior
    optimisations, and the run log records the seed.

🤖 Generated with Claude Code

NCCU-Schultz-Lab and others added 4 commits July 30, 2026 15:42
The launcher terminal opened with two bare lines of text. It is the first
thing a user sees on every run, and for students it is often the only
terminal they ever look at, so it may as well identify itself.

Adds launchers/_banner.sh — a sourced helper printing the same figlet
wordmark as the in-app run header, plus a mode line. Shared rather than
pasted into each launcher: three copies of ASCII art would drift.

  - Colour only when stdout is a TTY, so piping or redirecting stays clean.
  - Every call is `|| true` and guarded by a file-exists check: all three
    launchers run under `set -eu`, and decoration must never be what aborts
    a launch.
  - The art is duplicated from log_utils rather than printed by Python
    because the banner runs before the conda env is activated — there is no
    interpreter to ask yet.
  - Windows .bat launchers deliberately excluded: batch escaping of the
    backslashes and pipes in the art is error-prone and unverifiable from
    this dev environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
User report: when a pre-optimization changes essentially nothing, the
animation pane and the Keep/Revert buttons still appear. The animation shows
a molecule sitting still, and the buttons ask the user to choose between two
effectively identical geometries — which reads as "something happened, now
judge it" when the honest answer is "your geometry was already fine".

Below _PREOPT_NEGLIGIBLE_RMSD_A (0.05 A RMS displacement) the preview now
reports the number and stops: no animation, no Keep/Revert, and
_preopt_relaxed_mol is left unset so there is nothing to accept. The status
line says what happens next — the calculation uses the geometry as-is.

The threshold is deliberately conservative. Bond lengths are ~1.0-1.5 A, so
0.05 A RMS is a few percent and invisible at viewer scale, while perceptible
motion starts around 0.1 A. Showing a real-but-small change is much less bad
than hiding one. The old 1e-3 A test only distinguished mathematically-zero
from nonzero, which is not the question the user is asking.

The meaningful-change path explicitly restores both panes, so a negligible
preview followed by a real one does not leave Keep/Revert invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Frequency and UV-Vis could already start from a previously optimised
geometry; Geometry Opt could not. The motivating workflow is standard:
optimise at a cheap level of theory, then feed that geometry in as the
starting point for a more expensive one.

Adds the same filtered-dropdown pattern used by the other two (the
_refresh_seed_options helper was already shared), plus consumption in
_do_run: when a seed is selected the optimisation starts from that result's
final geometry instead of the current molecule, and the choice is recorded
in the run log alongside formula and atom count.

One deliberate difference from the Frequency/UV-Vis handlers: this one does
NOT disable _freq_preopt_cb. That checkbox means "optimise before the
calculation", which is meaningless when the optimisation IS the calculation.

Note this makes three near-duplicate seed widget groups (geo, freq, tddft)
differing only in their notes. Consolidating them into one reusable group is
worth doing — only one calc type is visible at a time — but that refactor
touches two working paths, so it is left as an M-UX2 item rather than bundled
into a feature commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured motivation: an aspirin (21 atoms) B3LYP/6-31G* UV-Vis run printed
nothing for 120 s after "converged SCF energy" while the TD-DFT solve ran.
The status label advanced the whole time — Phase A covers that — but the
output log, which is what a user actually watches, looked frozen.

_LogCapture already sees every write, so it is the natural place to know when
output last happened. A watchdog thread appends "… still working — <stage> ·
<elapsed>" whenever the stream has been quiet for _HEARTBEAT_AFTER_S.

Sized from the measurement rather than guessed: 25 s gives ~4 lines across
that 120 s gap — enough to prove liveness, few enough to stay quiet. Gaps grow
steeply with system size and aspirin is a small case, so the interval errs
short.

Three deliberate details:

  - The beat writes directly to the widget, NOT through write(): that path
    checks cancellation, which would raise _CalcCancelled on the watchdog
    thread where nothing can catch it, and would reset the very timer being
    measured.
  - It appends to the widget only, not to the capture buffer. The buffer
    becomes the result directory's pyscf.log, which should stay a faithful
    record of PySCF's output rather than being padded with UI chrome.
  - Any real write resets the timer, so a steadily-printing run never shows a
    heartbeat at all.

Stopped in _do_run's finally alongside the elapsed ticker, so it cannot
outlive a run and write into a finished log.

This is only viable now that M-LOGSCROLL shipped: before route C, a line
appended every 25 s would have yanked the user's scroll position back to the
bottom on a timer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NCCU-Schultz-Lab
NCCU-Schultz-Lab merged commit 50a089a into main Jul 30, 2026
5 checks passed
@NCCU-Schultz-Lab
NCCU-Schultz-Lab deleted the progress-phase-d-and-ux branch July 31, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant