From af6151a928ce9bf91e635e28eaa8d59a6e144354 Mon Sep 17 00:00:00 2001
From: NCCU-Schultz-Lab
Date: Fri, 17 Jul 2026 15:21:33 -0400
Subject: [PATCH 1/2] Remove reorg auto-run button from UI
Simplified the Reorganization Energy flow by removing the dedicated "Calc. Reorganization Energy" button and relying on the existing calc-type selector + shared Run Calculation path. Cleaned up related app wiring/state toggles and updated tests/docs to match the single-path behavior. Also adjusted method/basis descriptor cards to a horizontal, wrapping layout and streamlined the run-controls row to keep the setup UI more compact.
---
quantui/app.py | 20 --------------------
quantui/app_builders.py | 30 +++++++++++-------------------
tests/test_app.py | 22 +++++++++++-----------
3 files changed, 22 insertions(+), 50 deletions(-)
diff --git a/quantui/app.py b/quantui/app.py
index cc40a7f..897152d 100644
--- a/quantui/app.py
+++ b/quantui/app.py
@@ -1684,7 +1684,6 @@ def _wire_callbacks(self) -> None:
self.basis_help_btn.on_click(self._on_basis_help)
# Run
self.run_btn.on_click(self._on_run_clicked)
- self._reorg_auto_btn.on_click(self._on_reorg_auto_clicked)
self.cancel_btn.on_click(self._safe_cb(self._on_cancel))
self.preopt_preview_btn.on_click(self._safe_cb(self._on_preopt_preview))
self.preopt_accept_btn.on_click(self._safe_cb(self._on_preopt_accept))
@@ -3024,22 +3023,6 @@ def _on_run_clicked(self, btn) -> None:
)
_run_on_run_clicked(self, btn)
- def _on_reorg_auto_clicked(self, btn) -> None:
- """One-click reorganization energy: set up the mode, then run.
-
- Switches the calc-type to "Reorganization Energy" (which reveals the
- channel selector via the calc-type observer), defaults the channel to
- both hole + electron, and immediately launches the 4-point run.
- """
- if self._molecule is None:
- self.run_status.value = "Load a molecule first."
- return
- # Setting the dropdown value fires _on_calc_type_changed synchronously,
- # which swaps calc_extra_opts to show the channel selector + note.
- self.calc_type_dd.value = "Reorganization Energy"
- self._reorg_mode_dd.value = "both"
- self._on_run_clicked(btn)
-
def _on_cancel(self, btn=None) -> None:
"""Request graceful cancellation of the in-flight calculation.
@@ -3639,7 +3622,6 @@ def _set_molecule(self, mol: Molecule, label: str = "") -> None:
"""Update shared state and refresh dependent widgets."""
self._molecule = mol
self.run_btn.disabled = False
- self._reorg_auto_btn.disabled = False
self.export_btn.disabled = False
self.export_xyz_btn.disabled = False
self.export_mol_btn.disabled = not _RDKIT_AVAILABLE
@@ -4021,7 +4003,6 @@ def _do_run(self) -> None:
kind="compute",
)
self.run_btn.disabled = True
- self._reorg_auto_btn.disabled = True
self.run_status.value = "Starting..."
# Run-in-flight state: arm Cancel, lock out Clear (so it can't wipe the
# live output mid-run), reset the cancel flag for this fresh run.
@@ -5018,7 +4999,6 @@ def _run_required_final_single_point(target_mol, reason: str):
finally:
self.run_btn.disabled = False
- self._reorg_auto_btn.disabled = self._molecule is None
# Disarm Cancel, re-enable Clear, clear run-in-flight state.
self._calc_running = False
self._cancel_event.clear()
diff --git a/quantui/app_builders.py b/quantui/app_builders.py
index 746a9b3..9697bea 100644
--- a/quantui/app_builders.py
+++ b/quantui/app_builders.py
@@ -578,9 +578,17 @@ def build_shared_widgets(
app._method_card_html = widgets.HTML(value=method_card_html(default_method))
app._basis_card_html = widgets.HTML(value=basis_card_html(default_basis))
- app._descriptor_cards_box = widgets.VBox(
+ # Cards sit side-by-side (horizontal) so the setup row stays short — a
+ # vertical stack made the row tall and left a large gap above Calc. Type.
+ # flex_wrap lets the pair wrap gracefully on a narrow window.
+ app._descriptor_cards_box = widgets.HBox(
[app._method_card_html, app._basis_card_html],
- layout=layout_fn(margin="0 0 0 4px"),
+ layout=layout_fn(
+ gap="8px",
+ align_items="flex-start",
+ flex_wrap="wrap",
+ margin="0 0 0 4px",
+ ),
)
app.perf_estimate_html = widgets.HTML()
@@ -903,21 +911,6 @@ def build_shared_widgets(
)
app.run_status = widgets.Label()
- # One-click reorganization energy: switches the calc type to
- # "Reorganization Energy", applies sensible defaults (both channels), and
- # immediately launches the 4-point run.
- app._reorg_auto_btn = widgets.Button(
- description="Calc. Reorganization Energy",
- button_style="warning",
- icon="bolt",
- disabled=True,
- layout=layout_fn(width="250px", height="36px"),
- tooltip=(
- "Set up and run the 4-point Marcus reorganization energy "
- "(hole + electron) on the loaded molecule"
- ),
- )
-
# Gracefully stops a running calculation at the next SCF cycle / opt step.
# Disabled unless a calc is in flight (toggled by _do_run).
app.cancel_btn = widgets.Button(
@@ -1343,10 +1336,9 @@ def build_run_section(app: Any, *, layout_fn: Any) -> None:
),
app.perf_estimate_html,
widgets.HBox(
- [app.run_btn, app._reorg_auto_btn, app.run_status],
+ [app.run_btn, app.cancel_btn, app.run_status],
layout=layout_fn(align_items="center", gap="8px"),
),
- widgets.HBox([app.run_btn, app.cancel_btn, app.run_status]),
widgets.HBox(
[
widgets.HTML(
diff --git a/tests/test_app.py b/tests/test_app.py
index 7db3740..651f8ec 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1019,7 +1019,14 @@ def test_switching_away_from_nmr_clears_opts(self):
class TestReorganizationEnergyUI:
- """UI wiring for the Reorganization Energy calc-type + auto-setup button."""
+ """UI wiring for the Reorganization Energy calc-type.
+
+ The reorg run is driven entirely through the calc-type dropdown + the
+ channel-mode selector it reveals + the shared Run Calculation button
+ (``_do_run`` has a ``reorganization_energy`` branch). The old dedicated
+ "Calc. Reorganization Energy" auto-button was removed as a redundant
+ duplicate of that path.
+ """
def test_reorg_mode_shows_channel_selector(self):
app = QuantUIApp()
@@ -1034,19 +1041,12 @@ def test_reorg_hides_preopt_checkbox(self):
app.calc_type_dd.value = "Reorganization Energy"
assert app._freq_preopt_cb.layout.display == "none"
- def test_auto_button_enabled_after_molecule_load(self):
- app = QuantUIApp()
- assert app._reorg_auto_btn.disabled is True
- mol = Molecule(["H", "H"], [[0, 0, 0], [0, 0, 0.74]])
- app._set_molecule(mol)
- assert app._reorg_auto_btn.disabled is False
-
- def test_auto_button_sets_up_mode(self):
+ def test_reorg_calc_type_sets_up_mode(self):
app = QuantUIApp()
mol = Molecule(["H", "H"], [[0, 0, 0], [0, 0, 0.74]])
app._set_molecule(mol)
- # Drive only the setup portion (not the background run thread) by
- # replicating what the handler does before dispatch.
+ # Selecting the calc type reveals the channel selector; Run Calculation
+ # then dispatches the reorg run via _do_run's reorg branch.
app.calc_type_dd.value = "Reorganization Energy"
app._reorg_mode_dd.value = "both"
assert app.calc_type_dd.value == "Reorganization Energy"
From 798e490bc3f6c7be86168188ef091e90882b72a7 Mon Sep 17 00:00:00 2001
From: NCCU-Schultz-Lab
Date: Fri, 17 Jul 2026 16:11:05 -0400
Subject: [PATCH 2/2] Add calc-type help and open-shell hint
Adds a new `calc_type` help topic, wires a new `?` button beside the Calc. Type dropdown, and routes it through `app_runflow`/`app.py` so the Help overlay opens directly to calculation-type guidance. Also restores open-shell guidance in setup notes with a dynamic hint that appears only when multiplicity > 1, warning RHF users to switch to UHF (or DFT) and showing informational text for unrestricted methods. Includes new app tests covering the help topic/button behavior and open-shell hint visibility/content.
---
quantui/app.py | 11 ++++++++++
quantui/app_builders.py | 17 ++++++++++++++-
quantui/app_runflow.py | 46 +++++++++++++++++++++++++++++++++++++----
quantui/help_content.py | 45 ++++++++++++++++++++++++++++++++++++++++
tests/test_app.py | 44 +++++++++++++++++++++++++++++++++++++++
5 files changed, 158 insertions(+), 5 deletions(-)
diff --git a/quantui/app.py b/quantui/app.py
index 897152d..9ea1672 100644
--- a/quantui/app.py
+++ b/quantui/app.py
@@ -209,6 +209,9 @@
from quantui.app_runflow import (
on_calc_type_changed as _run_on_calc_type_changed,
)
+from quantui.app_runflow import (
+ on_calc_type_help as _run_on_calc_type_help,
+)
from quantui.app_runflow import (
on_clear as _run_on_clear,
)
@@ -972,6 +975,7 @@ class QuantUIApp:
basis_help_btn: Any
calc_extra_opts: Any
calc_type_dd: Any
+ calc_type_help_btn: Any
charge_si: Any
clear_btn: Any
_completion_banner: Any
@@ -993,6 +997,7 @@ class QuantUIApp:
_method_card_html: Any
_basis_card_html: Any
_descriptor_cards_box: Any
+ _open_shell_hint: Any
nstates_si: Any
perf_estimate_html: Any
post_calc_panel: Any
@@ -1677,11 +1682,14 @@ def _wire_callbacks(self) -> None:
# Notes + estimate
self.method_dd.observe(self._safe_cb(self._update_notes), names="value")
self.basis_dd.observe(self._safe_cb(self._update_notes), names="value")
+ # Multiplicity drives the open-shell hint (part of _update_notes).
+ self.mult_si.observe(self._safe_cb(self._update_notes), names="value")
self.method_dd.observe(self._safe_cb(self._update_estimate), names="value")
self.basis_dd.observe(self._safe_cb(self._update_estimate), names="value")
# Help buttons
self.method_help_btn.on_click(self._on_method_help)
self.basis_help_btn.on_click(self._on_basis_help)
+ self.calc_type_help_btn.on_click(self._on_calc_type_help)
# Run
self.run_btn.on_click(self._on_run_clicked)
self.cancel_btn.on_click(self._safe_cb(self._on_cancel))
@@ -3013,6 +3021,9 @@ def _on_method_help(self, btn) -> None:
def _on_basis_help(self, btn) -> None:
_run_on_basis_help(self, btn)
+ def _on_calc_type_help(self, btn) -> None:
+ _run_on_calc_type_help(self, btn)
+
# ── Run ───────────────────────────────────────────────────────────────
def _on_run_clicked(self, btn) -> None:
diff --git a/quantui/app_builders.py b/quantui/app_builders.py
index 9697bea..7f414ec 100644
--- a/quantui/app_builders.py
+++ b/quantui/app_builders.py
@@ -590,6 +590,11 @@ def build_shared_widgets(
margin="0 0 0 4px",
),
)
+ # Open-shell hint — shown only when multiplicity > 1 (updated by
+ # ``update_notes``). Restores the guidance the old notes block carried.
+ app._open_shell_hint = widgets.HTML(
+ value="", layout=layout_fn(display="none", margin="2px 0 0 0")
+ )
app.perf_estimate_html = widgets.HTML()
app.step_progress = step_progress_cls(
@@ -901,6 +906,12 @@ def build_shared_widgets(
layout=layout_fn(width="28px", height="28px"),
tooltip="Choosing a basis set — opens Help tab",
)
+ app.calc_type_help_btn = widgets.Button(
+ description="?",
+ button_style="",
+ layout=layout_fn(width="28px", height="28px"),
+ tooltip="What each calculation type computes — opens Help tab",
+ )
app.run_btn = widgets.Button(
description="Run Calculation",
@@ -1308,7 +1319,11 @@ def build_calc_setup(app: Any, *, layout_fn: Any) -> None:
],
layout=layout_fn(flex_wrap="wrap", align_items="flex-start"),
),
- app.calc_type_dd,
+ app._open_shell_hint,
+ widgets.HBox(
+ [app.calc_type_dd, app.calc_type_help_btn],
+ layout=layout_fn(align_items="center", gap="4px"),
+ ),
app.calc_extra_opts,
widgets.HBox(
[app.preopt_preview_label, app.preopt_preview_btn],
diff --git a/quantui/app_runflow.py b/quantui/app_runflow.py
index f0dea85..a0e5886 100644
--- a/quantui/app_runflow.py
+++ b/quantui/app_runflow.py
@@ -784,6 +784,12 @@ def on_basis_help(app: Any, btn: Any) -> None:
app._show_help_topic("basis_set")
+def on_calc_type_help(app: Any, btn: Any) -> None:
+ """Open help overlay focused on calculation-type guidance."""
+ _ = btn
+ app._show_help_topic("calc_type")
+
+
def on_exit_clicked(app: Any, _unused: Any = None) -> None:
"""Update UI and request shutdown of Voilà/Jupyter parent and kernel."""
import os
@@ -1107,11 +1113,12 @@ def _progress(
def update_notes(app: Any, change: Any = None) -> None:
- """Refresh the method / basis descriptor cards (UXP.7).
+ """Refresh the method / basis descriptor cards + open-shell hint.
- 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.
+ Replaces the old inline educational-notes text block (UXP.7). 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.
"""
try:
from quantui.descriptor_cards import basis_card_html, method_card_html
@@ -1120,6 +1127,37 @@ def update_notes(app: Any, change: Any = None) -> None:
app._basis_card_html.value = basis_card_html(app.basis_dd.value)
except Exception:
pass
+ _update_open_shell_hint(app)
+
+
+def _update_open_shell_hint(app: Any) -> None:
+ """Show/hide the open-shell (multiplicity > 1 → UHF) guidance hint."""
+ try:
+ mult = int(app.mult_si.value)
+ except Exception:
+ mult = 1
+ if mult <= 1:
+ app._open_shell_hint.value = ""
+ app._open_shell_hint.layout.display = "none"
+ return
+ n_unpaired = mult - 1
+ plural = "s" if n_unpaired != 1 else ""
+ if app.method_dd.value.upper() == "RHF":
+ # Actionable: RHF is the one method that will misbehave for open-shell.
+ app._open_shell_hint.value = (
+ ''
+ f"⚠ Open-shell: {n_unpaired} unpaired electron{plural} "
+ f"(multiplicity {mult}). RHF assumes all electrons are paired — "
+ "switch to UHF (or a DFT method) for this system."
+ )
+ else:
+ # Informational: UHF / DFT already handle open-shell correctly.
+ app._open_shell_hint.value = (
+ ''
+ f"Open-shell: {n_unpaired} unpaired electron{plural} "
+ f"(multiplicity {mult}) — running unrestricted."
+ )
+ app._open_shell_hint.layout.display = ""
def update_estimate(app: Any, *, calc_log_mod: Any, change: Any = None) -> None:
diff --git a/quantui/help_content.py b/quantui/help_content.py
index e6431d7..7d800f7 100644
--- a/quantui/help_content.py
+++ b/quantui/help_content.py
@@ -48,6 +48,51 @@
"context-sensitive help on that specific option.
"
),
},
+ "calc_type": {
+ "title": "Calculation types — what does each one do?",
+ "body": (
+ "The Calc. Type dropdown chooses what QuantUI computes for "
+ "your molecule at the selected method / basis.
"
+ ""
+ ""
+ " | Type | "
+ " Computes | "
+ " Cost |
"
+ "| Single Point | "
+ " Energy + properties at the input "
+ "geometry (no atoms move) | "
+ " Fastest |
"
+ "| Geometry Opt | "
+ " Relaxes the structure to a minimum-"
+ "energy geometry | "
+ " Moderate |
"
+ "| Frequency | "
+ " Vibrational modes + IR spectrum; "
+ "confirms a true minimum (no imaginary modes) | "
+ " Higher |
"
+ "| UV-Vis (TD-DFT) | "
+ " Electronic excitations / absorption "
+ "spectrum (needs a DFT functional) | "
+ " Higher |
"
+ "| NMR Shielding | "
+ " Isotropic shielding → predicted "
+ "¹H / ¹³C chemical shifts | "
+ " Higher |
"
+ "| PES Scan | "
+ " Energy profile along one bond / "
+ "angle / dihedral coordinate | "
+ " Many points |
"
+ "| Reorganization Energy | "
+ " Marcus 4-point λ for charge transfer "
+ "(hole / electron channels) | "
+ " Slowest (2–3 optimizations) |
"
+ "
"
+ "Typical workflow: optimize the geometry first "
+ "(Geometry Opt), then run Frequency, UV-Vis, or "
+ "NMR on the optimized structure for meaningful results — most "
+ "spectra are only valid at a minimum-energy geometry.
"
+ ),
+ },
"method": {
"title": "RHF vs UHF — which method should I use?",
"body": (
diff --git a/tests/test_app.py b/tests/test_app.py
index 651f8ec..a88c50e 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -706,6 +706,50 @@ def test_script_filename_sanitizes_basis_with_asterisk(self, tmp_path, monkeypat
assert "Error" not in app.export_status.value
+class TestCalcTypeHelp:
+ """A '?' help button next to Calc. Type opens the calc_type help topic."""
+
+ def test_calc_type_help_topic_exists(self):
+ from quantui.help_content import HELP_TOPICS
+
+ assert "calc_type" in HELP_TOPICS
+ # Covers every calc type the dropdown offers.
+ body = HELP_TOPICS["calc_type"]["body"]
+ for label in ("Single Point", "Geometry Opt", "Reorganization Energy"):
+ assert label in body
+
+ def test_help_button_opens_calc_type_topic(self):
+ app = QuantUIApp()
+ app._on_calc_type_help(None)
+ assert app.help_topic_dd.value == "calc_type"
+
+
+class TestOpenShellHint:
+ """The open-shell hint appears only when multiplicity > 1 (UXP.7 restore)."""
+
+ def test_hidden_for_singlet(self):
+ app = QuantUIApp()
+ app.mult_si.value = 1
+ assert app._open_shell_hint.layout.display == "none"
+
+ def test_shown_and_warns_for_rhf_open_shell(self):
+ app = QuantUIApp()
+ app.method_dd.value = "RHF"
+ app.mult_si.value = 2
+ assert app._open_shell_hint.layout.display == ""
+ val = app._open_shell_hint.value
+ assert "UHF" in val
+ assert "unpaired electron" in val
+
+ def test_informational_for_uhf_open_shell(self):
+ app = QuantUIApp()
+ app.method_dd.value = "UHF"
+ app.mult_si.value = 3
+ assert app._open_shell_hint.layout.display == ""
+ # UHF already handles open-shell → no "switch to UHF" nag.
+ assert "unrestricted" in app._open_shell_hint.value
+
+
class TestDescriptorCards:
"""UXP.7: method / basis descriptor cards replace the inline notes block.