From 9793704a87b55e6b185164c712a0c3234e1c991a Mon Sep 17 00:00:00 2001
From: NCCU-Schultz-Lab
Date: Sat, 11 Apr 2026 23:40:25 -0400
Subject: [PATCH 01/13] Implement FR-012 Phase 1: QuantUIApp class (app.py)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Consolidates all notebook widget logic (cells 3–10) into a single
QuantUIApp class. Also adds None guard to optimizer.py calculate()
to satisfy mypy.
Co-Authored-By: Claude Sonnet 4.6
---
quantui/__init__.py | 7 +
quantui/app.py | 1917 ++++++++++++++++++++++++++++++++++++++++++
quantui/optimizer.py | 3 +
3 files changed, 1927 insertions(+)
create mode 100644 quantui/app.py
diff --git a/quantui/__init__.py b/quantui/__init__.py
index 9d5c104..1fb2cbb 100644
--- a/quantui/__init__.py
+++ b/quantui/__init__.py
@@ -146,6 +146,11 @@
VISUALIZATION_AVAILABLE = False
PY3DMOL_AVAILABLE = False
+# App class — imported last so all package symbols are defined first.
+# app.py imports from submodules directly, but placing this last is an
+# extra safeguard against accidental circular-import issues in the future.
+from .app import QuantUIApp
+
__all__ = [
# Config constants
"MOLECULE_LIBRARY",
@@ -187,6 +192,8 @@
"plot_orbital_diagram",
"orbital_summary_html",
"parse_cube_file",
+ # App class
+ "QuantUIApp",
# Comparison
"CalcSummary",
"summary_from_session_result",
diff --git a/quantui/app.py b/quantui/app.py
new file mode 100644
index 0000000..64f8484
--- /dev/null
+++ b/quantui/app.py
@@ -0,0 +1,1917 @@
+"""
+QuantUI-local application class.
+
+All widget creation, state management, callbacks, and tab wiring live here.
+The notebook is a thin launcher::
+
+ from quantui.app import QuantUIApp
+ QuantUIApp().display()
+
+CSS is injected inside ``display()`` — not on import — so importing this
+module in tests or tutorials does not pollute the IPython display.
+"""
+
+from __future__ import annotations
+
+import io
+import re
+import threading
+import time
+from pathlib import Path
+from typing import Any, List, Optional
+
+import ipywidgets as widgets
+from IPython.display import HTML, Javascript, display
+
+import quantui
+import quantui.calc_log as _calc_log
+
+# Import directly from submodules to avoid circular-import issues.
+# quantui/__init__.py imports this module (app.py), so using
+# `from quantui import X` at module load time would see a partially-
+# initialised package namespace (symbols defined after the app import
+# in __init__.py would not yet exist).
+from quantui.config import (
+ DEFAULT_BASIS,
+ DEFAULT_CHARGE,
+ DEFAULT_METHOD,
+ DEFAULT_MULTIPLICITY,
+ MOLECULE_LIBRARY,
+ SUPPORTED_BASIS_SETS,
+ SUPPORTED_METHODS,
+)
+from quantui.help_content import HELP_TOPICS
+from quantui.molecule import Molecule, parse_xyz_input
+from quantui.progress import StepProgress
+from quantui.utils import get_session_resources
+
+# ── Availability flags (computed once at import, not per-instantiation) ───────
+try:
+ from quantui.ase_bridge import ASE_AVAILABLE
+except ImportError:
+ ASE_AVAILABLE = False
+
+try:
+ from quantui.visualization_py3dmol import display_molecule as _display_molecule
+
+ VISUALIZATION_AVAILABLE = True
+except ImportError:
+ VISUALIZATION_AVAILABLE = False
+ _display_molecule = None # type: ignore[assignment]
+
+try:
+ from quantui.pubchem import student_friendly_fetch as _student_friendly_fetch
+
+ PUBCHEM_AVAILABLE = True
+except ImportError:
+ PUBCHEM_AVAILABLE = False
+ _student_friendly_fetch = None # type: ignore[assignment]
+
+try:
+ from quantui.session_calc import SessionResult, run_in_session # noqa: F401
+
+ _PYSCF_AVAILABLE = True
+except (ImportError, AttributeError):
+ _PYSCF_AVAILABLE = False
+
+try:
+ from quantui.preopt import preoptimize
+
+ _PREOPT_AVAILABLE = True
+except (ImportError, AttributeError):
+ _PREOPT_AVAILABLE = False
+
+# ── Module-level constants ────────────────────────────────────────────────────
+_THEME_HUE: dict = {"Dark": 180, "Dark Blue": 200, "Dark Maroon": 160}
+
+_APP_CSS: str = """"""
+
+
+# ── SCF regex (module-level so _LogCapture can use them) ─────────────────────
+_RE_CYCLE = re.compile(
+ r"cycle=\s*(\d+)\s+E=\s*([\-\d\.]+)\s+delta_E=\s*([\-\d\.Ee+\-]+)"
+)
+_RE_CONV = re.compile(r"converged SCF energy\s*=\s*([\-\d\.]+)")
+
+
+# ══ LOG CAPTURE ══════════════════════════════════════════════════════════════
+
+
+class _LogCapture:
+ """Write PySCF output to an Output widget and capture it to a buffer."""
+
+ def __init__(
+ self,
+ output_widget: widgets.Output,
+ status_label: Optional[widgets.Label] = None,
+ ) -> None:
+ self._w = output_widget
+ self._buf = io.StringIO()
+ self._line_buf = ""
+ self._status = status_label
+
+ def write(self, text: str) -> None:
+ if not text:
+ return
+ self._w.append_stdout(text)
+ self._buf.write(text)
+ self._line_buf += text
+ while "\n" in self._line_buf:
+ line, self._line_buf = self._line_buf.split("\n", 1)
+ m = _RE_CYCLE.search(line)
+ if m and self._status is not None:
+ n, delta = m.group(1), m.group(3)
+ try:
+ self._status.value = f"SCF cycle {n} · ΔE = {float(delta):.4g} Ha"
+ except Exception:
+ self._status.value = f"SCF cycle {n}"
+ continue
+ m = _RE_CONV.search(line)
+ if m and self._status is not None:
+ self._status.value = "SCF converged ✓"
+
+ def flush(self) -> None:
+ pass
+
+ def getvalue(self) -> str:
+ return self._buf.getvalue()
+
+
+# ══ APP CLASS ════════════════════════════════════════════════════════════════
+
+
+class QuantUIApp:
+ """
+ Self-contained QuantUI-local application widget.
+
+ Instantiate once; call ``display()`` to inject CSS and show the UI::
+
+ app = QuantUIApp()
+ app.display()
+ """
+
+ def __init__(self) -> None:
+ # ── Instance state ────────────────────────────────────────────────
+ self._molecule: Optional[Molecule] = None
+ self._last_result: Any = None
+ self._results: List = []
+
+ # Availability (copied from module-level flags)
+ self._pyscf_available: bool = _PYSCF_AVAILABLE
+ self._preopt_available: bool = _PREOPT_AVAILABLE
+
+ # ── Build → wire → assemble ───────────────────────────────────────
+ self._build_widgets()
+ self._wire_callbacks()
+ self._assemble_tabs()
+
+ # Log startup
+ _calc_log.log_event("startup", f"QuantUI-local {quantui.__version__} started")
+
+ def display(self) -> None:
+ """Inject global CSS and render the application widget."""
+ display(HTML(_APP_CSS))
+ display(
+ widgets.VBox(
+ [
+ widgets.HBox(
+ [self.theme_btn],
+ layout=widgets.Layout(
+ justify_content="flex-end", margin="0 0 4px"
+ ),
+ ),
+ self._theme_style,
+ self._status_html,
+ self.root_tab,
+ ]
+ )
+ )
+
+ @property
+ def widget(self) -> widgets.Tab:
+ """The root tab widget (for callers that want the widget object)."""
+ return self.root_tab
+
+ # ══ BUILD METHODS ════════════════════════════════════════════════════════
+
+ def _build_widgets(self) -> None:
+ self._build_theme_selector()
+ self._build_status_panel()
+ self._build_shared_widgets()
+ self._build_molecule_section()
+ self._build_calc_setup()
+ self._build_run_section()
+ self._build_results_section()
+ self._build_history_section()
+ self._build_compare_section()
+ self._build_output_tab()
+ self._build_help_section()
+
+ # ── Theme selector ────────────────────────────────────────────────────
+
+ def _build_theme_selector(self) -> None:
+ self._theme_style = widgets.Output(
+ layout=widgets.Layout(
+ height="0px", overflow="hidden", margin="0", padding="0"
+ )
+ )
+ self.theme_btn = widgets.ToggleButtons(
+ options=["Light", "Dark", "Dark Blue", "Dark Maroon"],
+ value="Dark",
+ description="Theme:",
+ style={"description_width": "48px", "button_width": "90px"},
+ layout=widgets.Layout(margin="0"),
+ )
+ # Apply Dark theme immediately
+ with self._theme_style:
+ display(HTML(self._theme_css("Dark")))
+
+ def _theme_css(self, theme: str) -> str:
+ """Return the CSS filter block for *theme*, or '' for Light."""
+ if theme not in _THEME_HUE:
+ return ""
+ deg = _THEME_HUE[theme]
+ return (
+ ""
+ )
+
+ # ── Status panel ──────────────────────────────────────────────────────
+
+ def _build_status_panel(self) -> None:
+ _cores, _mem_gb = get_session_resources()
+ _mem = f"{_mem_gb} GB" if _mem_gb is not None else "unknown"
+
+ def _ok(flag: bool, extra: str = "") -> str:
+ tick = '✓'
+ cross = '✗'
+ return (tick if flag else cross) + (" " + extra if extra else "")
+
+ _items = [
+ (
+ "PySCF (calculations)",
+ _ok(
+ _PYSCF_AVAILABLE,
+ "" if _PYSCF_AVAILABLE else "— Linux / macOS / WSL required",
+ ),
+ ),
+ ("ASE (structure I/O, opt.)", _ok(ASE_AVAILABLE)),
+ ("PubChem search", _ok(PUBCHEM_AVAILABLE)),
+ ("3D viewer (py3Dmol)", _ok(VISUALIZATION_AVAILABLE)),
+ ("CPU cores / Memory", f"{_cores} cores / {_mem}"),
+ ]
+ _rows = "".join(
+ f'| {k} | '
+ f'{v} |
'
+ for k, v in _items
+ )
+ self._status_html = widgets.HTML(
+ f''
+ f'
'
+ f"QuantUI-local {quantui.__version__}"
+ f'
'
+ f"
"
+ )
+
+ # ── Shared widgets (Cell 3) ───────────────────────────────────────────
+
+ def _build_shared_widgets(self) -> None:
+ # Output widgets
+ self.mol_info_html = widgets.HTML(
+ value='No molecule loaded yet.'
+ )
+ self.mol_summary_compact = widgets.HTML(value="")
+ self.viz_output = widgets.Output(layout=widgets.Layout(min_height="50px"))
+ self.run_output = widgets.Output(
+ layout=widgets.Layout(
+ border="1px solid #c0ccd8",
+ min_height="80px",
+ max_height="400px",
+ padding="8px",
+ overflow_y="auto",
+ )
+ )
+ with self.run_output:
+ display(
+ HTML(
+ ''
+ "No calculation run yet. PySCF output and any errors will appear here."
+ "
"
+ )
+ )
+ self.result_output = widgets.Output()
+ self.comparison_output = widgets.Output()
+ self.notes_output = widgets.Output()
+ self.perf_estimate_html = widgets.HTML()
+
+ # Step indicator
+ self.step_progress = StepProgress(
+ ["Choose molecule", "Set method", "Run", "Results"]
+ )
+ self.step_progress.start(0)
+
+ # Calculation setup dropdowns
+ self.method_dd = widgets.Dropdown(
+ options=SUPPORTED_METHODS,
+ value=DEFAULT_METHOD,
+ description="Method:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="260px"),
+ )
+ self.basis_dd = widgets.Dropdown(
+ options=SUPPORTED_BASIS_SETS,
+ value=DEFAULT_BASIS,
+ description="Basis Set:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="260px"),
+ )
+ self.charge_si = widgets.BoundedIntText(
+ value=DEFAULT_CHARGE,
+ min=-10,
+ max=10,
+ description="Charge:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="190px"),
+ )
+ self.mult_si = widgets.BoundedIntText(
+ value=DEFAULT_MULTIPLICITY,
+ min=1,
+ max=10,
+ description="Multiplicity:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="190px"),
+ )
+ self.preopt_cb = widgets.Checkbox(
+ value=False,
+ description="Pre-optimize geometry (fast LJ force-field)",
+ disabled=not _PREOPT_AVAILABLE,
+ layout=widgets.Layout(width="400px"),
+ )
+
+ # Calculation type + extra options
+ self.calc_type_dd = widgets.Dropdown(
+ options=["Single Point", "Geometry Opt", "Frequency", "UV-Vis (TD-DFT)"],
+ value="Single Point",
+ description="Calc. Type:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="310px"),
+ )
+ self.fmax_fi = widgets.BoundedFloatText(
+ value=0.05,
+ min=0.001,
+ max=1.0,
+ step=0.005,
+ description="Force thr. (eV/Å):",
+ style={"description_width": "130px"},
+ layout=widgets.Layout(width="270px"),
+ )
+ self.max_steps_si = widgets.BoundedIntText(
+ value=200,
+ min=10,
+ max=1000,
+ description="Max steps:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="200px"),
+ )
+ self.nstates_si = widgets.BoundedIntText(
+ value=10,
+ min=1,
+ max=50,
+ description="# states:",
+ style={"description_width": "100px"},
+ layout=widgets.Layout(width="180px"),
+ )
+ self.calc_extra_opts = widgets.VBox([])
+
+ # Context-help buttons
+ self.method_help_btn = widgets.Button(
+ description="?",
+ button_style="",
+ layout=widgets.Layout(width="28px", height="28px"),
+ tooltip="RHF vs UHF — opens Help tab",
+ )
+ self.basis_help_btn = widgets.Button(
+ description="?",
+ button_style="",
+ layout=widgets.Layout(width="28px", height="28px"),
+ tooltip="Choosing a basis set — opens Help tab",
+ )
+
+ # Run widgets
+ self.run_btn = widgets.Button(
+ description="Run Calculation",
+ button_style="success",
+ icon="play",
+ disabled=True,
+ layout=widgets.Layout(width="200px", height="36px"),
+ )
+ self.run_status = widgets.Label()
+
+ # Log clear button (in run panel)
+ self.log_clear_btn = widgets.Button(
+ description="Clear",
+ button_style="",
+ icon="times",
+ layout=widgets.Layout(width="90px", height="26px"),
+ tooltip="Clear calculation output",
+ )
+
+ # Comparison / export widgets
+ self.accumulate_btn = widgets.Button(
+ description="Add to Comparison",
+ button_style="info",
+ icon="plus",
+ disabled=True,
+ layout=widgets.Layout(width="190px"),
+ )
+ self.clear_btn = widgets.Button(
+ description="Clear",
+ button_style="warning",
+ icon="trash",
+ layout=widgets.Layout(width="100px"),
+ )
+ self.export_btn = widgets.Button(
+ description="Export Script",
+ button_style="",
+ icon="download",
+ disabled=True,
+ layout=widgets.Layout(width="160px"),
+ )
+ self.export_status = widgets.Label()
+
+ # ── Molecule section (Cell 4) ─────────────────────────────────────────
+
+ def _build_molecule_section(self) -> None:
+ # Preset dropdown
+ _preset_opts = ["(select a molecule)"] + list(MOLECULE_LIBRARY.keys())
+ self.preset_dd = widgets.Dropdown(
+ options=_preset_opts,
+ value="(select a molecule)",
+ description="Molecule:",
+ style={"description_width": "90px"},
+ layout=widgets.Layout(width="320px"),
+ )
+
+ # XYZ input
+ self.xyz_area = widgets.Textarea(
+ placeholder=(
+ "Paste XYZ coordinates (symbol x y z):\n"
+ "O 0.000 0.000 0.000\n"
+ "H 0.757 0.587 0.000\n"
+ "H -0.757 0.587 0.000"
+ ),
+ layout=widgets.Layout(width="440px", height="130px"),
+ )
+ self.xyz_btn = widgets.Button(
+ description="Load XYZ", button_style="info", icon="upload"
+ )
+ self.xyz_msg = widgets.Label()
+
+ # PubChem search
+ self.pubchem_txt = widgets.Text(
+ placeholder="name or SMILES (e.g. aspirin, caffeine, CC(=O)O)",
+ layout=widgets.Layout(width="380px"),
+ )
+ self.pubchem_btn = widgets.Button(
+ description="Search",
+ button_style="info",
+ icon="search",
+ disabled=not PUBCHEM_AVAILABLE,
+ layout=widgets.Layout(width="100px"),
+ )
+ self.pubchem_msg = widgets.Label(
+ value=(
+ ""
+ if PUBCHEM_AVAILABLE
+ else "PubChem unavailable — check internet connection"
+ )
+ )
+
+ # Assemble input tab
+ _hint = ''
+ tab_preset = widgets.VBox(
+ [
+ widgets.HTML(
+ _hint + "Choose from 20+ curated educational molecules.
"
+ ),
+ self.preset_dd,
+ ]
+ )
+ tab_xyz = widgets.VBox(
+ [
+ widgets.HTML(
+ _hint
+ + "Paste XYZ coordinates (element x y z, one atom per line).
"
+ ),
+ self.xyz_area,
+ widgets.HBox([self.xyz_btn, self.xyz_msg]),
+ ]
+ )
+ tab_pubchem = widgets.VBox(
+ [
+ widgets.HTML(
+ _hint
+ + "Search by name or SMILES. Requires internet connection."
+ ),
+ widgets.HBox([self.pubchem_txt, self.pubchem_btn]),
+ self.pubchem_msg,
+ ]
+ )
+ input_tab = widgets.Tab(children=[tab_preset, tab_xyz, tab_pubchem])
+ for _i, _t in enumerate(["Preset Library", "XYZ Input", "PubChem Search"]):
+ input_tab.set_title(_i, _t)
+
+ # Collapsible container
+ self.mol_input_expanded = widgets.VBox(
+ [
+ widgets.HTML('Molecule Input
'),
+ input_tab,
+ ]
+ )
+ self.change_mol_btn = widgets.Button(
+ description="Change",
+ button_style="",
+ icon="pencil",
+ layout=widgets.Layout(width="100px", height="32px"),
+ tooltip="Re-expand the molecule input panel",
+ )
+ self.mol_input_collapsed = widgets.HBox(
+ [self.mol_summary_compact, self.change_mol_btn],
+ layout=widgets.Layout(align_items="center", gap="12px", padding="6px 0"),
+ )
+ self.mol_input_container = widgets.VBox(
+ [self.mol_input_expanded, self.mol_info_html, self.viz_output],
+ layout=widgets.Layout(margin="0 0 4px 0"),
+ )
+
+ # ── Calculation setup panel (Cell 5) ──────────────────────────────────
+
+ def _build_calc_setup(self) -> None:
+ self.calc_setup_panel = widgets.VBox(
+ [
+ widgets.HTML('Calculation Setup
'),
+ widgets.HBox(
+ [
+ widgets.VBox(
+ [
+ widgets.HBox(
+ [self.method_dd, self.method_help_btn],
+ layout=widgets.Layout(
+ align_items="center", gap="4px"
+ ),
+ ),
+ widgets.HBox(
+ [self.basis_dd, self.basis_help_btn],
+ layout=widgets.Layout(
+ align_items="center", gap="4px"
+ ),
+ ),
+ ]
+ ),
+ widgets.HTML(" "),
+ widgets.VBox([self.charge_si, self.mult_si]),
+ ]
+ ),
+ self.calc_type_dd,
+ self.calc_extra_opts,
+ self.preopt_cb,
+ self.notes_output,
+ ]
+ )
+
+ # ── Run panel (Cell 6) ────────────────────────────────────────────────
+
+ def _build_run_section(self) -> None:
+ self.run_panel = widgets.VBox(
+ [
+ widgets.HTML(
+ 'Run Calculation
'
+ 'PySCF runs in this '
+ "kernel. Output appears live below. Large molecules or high-accuracy basis "
+ "sets may take several minutes on a laptop.
"
+ ),
+ self.perf_estimate_html,
+ widgets.HBox([self.run_btn, self.run_status]),
+ widgets.HBox(
+ [
+ widgets.HTML(
+ ''
+ "Calculation Output"
+ ),
+ self.log_clear_btn,
+ ],
+ layout=widgets.Layout(
+ align_items="center",
+ justify_content="space-between",
+ margin="10px 0 4px",
+ max_width="460px",
+ ),
+ ),
+ self.run_output,
+ ]
+ )
+
+ # ── Results panel (Cell 7) ────────────────────────────────────────────
+
+ def _build_results_section(self) -> None:
+ self.results_panel = widgets.VBox(
+ [
+ widgets.HTML('Results
'),
+ self.result_output,
+ ]
+ )
+
+ # ── History panel (Cell 8) ────────────────────────────────────────────
+
+ def _build_history_section(self) -> None:
+ self.past_dd = widgets.Dropdown(
+ description="Load:",
+ options=[("(no saved results)", "")],
+ style={"description_width": "50px"},
+ layout=widgets.Layout(width="500px"),
+ )
+ self.past_refresh_btn = widgets.Button(
+ description="Refresh",
+ button_style="",
+ icon="refresh",
+ layout=widgets.Layout(width="100px"),
+ tooltip="Rescan the results directory",
+ )
+ self.copy_path_btn = widgets.Button(
+ description="Copy path",
+ button_style="",
+ icon="clipboard",
+ layout=widgets.Layout(width="120px"),
+ tooltip="Copy the results directory path to clipboard",
+ )
+ self.results_path_lbl = widgets.HTML()
+ self.past_output = widgets.Output()
+ self.view_log_btn = widgets.Button(
+ description="View log",
+ button_style="",
+ icon="file-text-o",
+ layout=widgets.Layout(width="110px"),
+ tooltip="Open the full PySCF output log in the Output tab",
+ )
+
+ # Performance stats widgets
+ self._perf_stats_html = widgets.HTML()
+ self._perf_events_html = widgets.HTML()
+ self._reset_btn = widgets.Button(
+ description="Reset performance database",
+ button_style="danger",
+ icon="trash",
+ layout=widgets.Layout(width="230px"),
+ )
+ self._reset_confirm_html = widgets.HTML(
+ ''
+ "Warning: This will permanently delete all performance records. "
+ "Time estimates will reset to “no data”."
+ )
+ self._reset_confirm_yes = widgets.Button(
+ description="Yes, delete all records",
+ button_style="danger",
+ icon="check",
+ layout=widgets.Layout(width="190px"),
+ )
+ self._reset_confirm_no = widgets.Button(
+ description="Cancel",
+ button_style="",
+ icon="times",
+ layout=widgets.Layout(width="90px"),
+ )
+ self._reset_confirm_box = widgets.VBox(
+ [
+ self._reset_confirm_html,
+ widgets.HBox(
+ [self._reset_confirm_yes, self._reset_confirm_no],
+ layout=widgets.Layout(gap="8px", margin="4px 0 0"),
+ ),
+ ],
+ layout=widgets.Layout(
+ display="none",
+ border="1px solid #fca5a5",
+ padding="8px 10px",
+ margin="6px 0 0",
+ ),
+ )
+
+ _perf_stats_panel = widgets.VBox(
+ [
+ self._perf_stats_html,
+ widgets.HTML(
+ ''
+ "Recent events (last 20)
"
+ ),
+ self._perf_events_html,
+ widgets.HBox(
+ [self._reset_btn],
+ layout=widgets.Layout(margin="14px 0 4px"),
+ ),
+ self._reset_confirm_box,
+ ]
+ )
+ self._perf_accordion = widgets.Accordion(
+ children=[_perf_stats_panel], selected_index=None
+ )
+ self._perf_accordion.set_title(0, "Performance stats")
+
+ self.history_panel = widgets.VBox(
+ [
+ widgets.HTML(
+ ''
+ "Calculations are saved automatically. Select one below to view its results.
"
+ ),
+ widgets.HBox(
+ [
+ self.past_dd,
+ self.past_refresh_btn,
+ self.copy_path_btn,
+ self.view_log_btn,
+ ],
+ layout=widgets.Layout(align_items="center", gap="8px"),
+ ),
+ self.results_path_lbl,
+ self.past_output,
+ self._perf_accordion,
+ ]
+ )
+
+ # Populate on startup
+ self._refresh_results_browser()
+ self._refresh_perf_stats()
+
+ # ── Compare panel (Cell 9) ────────────────────────────────────────────
+
+ def _build_compare_section(self) -> None:
+ self.compare_select = widgets.SelectMultiple(
+ options=[("(no saved results)", "")],
+ rows=8,
+ description="",
+ layout=widgets.Layout(width="100%"),
+ )
+ self.compare_refresh_btn = widgets.Button(
+ description="Refresh",
+ button_style="",
+ icon="refresh",
+ layout=widgets.Layout(width="100px"),
+ )
+ self.compare_btn = widgets.Button(
+ description="Compare selected",
+ button_style="primary",
+ icon="bar-chart",
+ disabled=True,
+ layout=widgets.Layout(width="180px"),
+ )
+ self.compare_clear_btn = widgets.Button(
+ description="Clear",
+ button_style="warning",
+ icon="times",
+ layout=widgets.Layout(width="90px"),
+ )
+ self.compare_output = widgets.Output()
+
+ self.compare_panel = widgets.VBox(
+ [
+ widgets.HTML(
+ 'Compare Calculations
'
+ ''
+ "Select two or more saved calculations to compare side-by-side. "
+ "Hold Ctrl (or ⌘) to select multiple entries.
"
+ ),
+ widgets.HBox([self.compare_refresh_btn]),
+ self.compare_select,
+ widgets.HBox(
+ [self.compare_btn, self.compare_clear_btn],
+ layout=widgets.Layout(gap="8px", margin="6px 0"),
+ ),
+ self.compare_output,
+ ],
+ layout=widgets.Layout(padding="8px 0"),
+ )
+
+ # Export accordion (Advanced)
+ _export_content = widgets.VBox(
+ [
+ widgets.HTML(
+ ''
+ "Download a self-contained PySCF script you can study or run outside the notebook.
"
+ ),
+ widgets.HBox([self.export_btn, self.export_status]),
+ ]
+ )
+ self.advanced_accordion = widgets.Accordion(children=[_export_content])
+ self.advanced_accordion.set_title(0, "Export Script")
+ self.advanced_accordion.selected_index = None
+
+ # Populate on startup
+ self._populate_compare_list()
+
+ # ── Output log tab (Cell 10) ──────────────────────────────────────────
+
+ def _build_output_tab(self) -> None:
+ self._log_output_html = widgets.HTML(
+ ''
+ "No log yet — run a calculation first, or use "
+ "View log in the History tab."
+ )
+ self._log_source_lbl = widgets.HTML()
+ self._log_clear_btn = widgets.Button(
+ description="Clear",
+ button_style="",
+ icon="times",
+ layout=widgets.Layout(width="80px"),
+ )
+ self.log_tab_panel = widgets.VBox(
+ [
+ widgets.HTML(
+ ''
+ "Full PySCF output for the most recent calculation. "
+ "Use View log in the History tab to load a saved result's log.
"
+ ),
+ widgets.HBox(
+ [self._log_clear_btn],
+ layout=widgets.Layout(margin="0 0 8px"),
+ ),
+ self._log_source_lbl,
+ self._log_output_html,
+ ],
+ layout=widgets.Layout(padding="8px 0"),
+ )
+
+ # ── Help section (Cell 10) ────────────────────────────────────────────
+
+ def _build_help_section(self) -> None:
+ _help_keys = list(HELP_TOPICS.keys())
+ _help_labels = [HELP_TOPICS[k]["title"] for k in _help_keys]
+ self.help_topic_dd = widgets.Dropdown(
+ options=list(zip(_help_labels, _help_keys)),
+ description="Topic:",
+ style={"description_width": "60px"},
+ layout=widgets.Layout(width="460px"),
+ )
+ self.help_content_html = widgets.HTML()
+ self._render_help_topic() # render first topic immediately
+
+ self.help_tab_panel = widgets.VBox(
+ [
+ widgets.HTML(
+ ''
+ "Browse help topics below. Click ? next to the Method or Basis Set "
+ "dropdown in the Calculate tab to jump directly to a relevant topic.
"
+ ),
+ self.help_topic_dd,
+ self.help_content_html,
+ ],
+ layout=widgets.Layout(padding="8px 0"),
+ )
+
+ # ── Tab assembly (Cell 10) ────────────────────────────────────────────
+
+ def _assemble_tabs(self) -> None:
+ _calculate_content = widgets.VBox(
+ [
+ self.step_progress.widget,
+ self.mol_input_container,
+ self.calc_setup_panel,
+ self.run_panel,
+ self.results_panel,
+ self.advanced_accordion,
+ ],
+ layout=widgets.Layout(padding="8px 0"),
+ )
+
+ self.root_tab = widgets.Tab(
+ children=[
+ _calculate_content,
+ self.history_panel,
+ self.compare_panel,
+ self.log_tab_panel,
+ self.help_tab_panel,
+ ]
+ )
+ self.root_tab.set_title(0, "Calculate")
+ self.root_tab.set_title(1, "History")
+ self.root_tab.set_title(2, "Compare")
+ self.root_tab.set_title(3, "Output")
+ self.root_tab.set_title(4, "Help")
+
+ # ══ CALLBACK WIRING ══════════════════════════════════════════════════════
+
+ def _wire_callbacks(self) -> None:
+ # Theme
+ self.theme_btn.observe(self._on_theme_changed, names="value")
+ # Molecule input
+ self.preset_dd.observe(self._on_load_preset, names="value")
+ self.xyz_btn.on_click(self._on_load_xyz)
+ self.pubchem_btn.on_click(self._on_search_pubchem)
+ self.change_mol_btn.on_click(self._on_expand_mol_input)
+ # Calc type
+ self.calc_type_dd.observe(self._on_calc_type_changed, names="value")
+ # Notes + estimate
+ self.method_dd.observe(self._update_notes, names="value")
+ self.basis_dd.observe(self._update_notes, names="value")
+ self.method_dd.observe(self._update_estimate, names="value")
+ self.basis_dd.observe(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)
+ # Run
+ self.run_btn.on_click(self._on_run_clicked)
+ self.log_clear_btn.on_click(self._on_clear_log)
+ # Accumulate / export
+ self.accumulate_btn.on_click(self._on_accumulate)
+ self.clear_btn.on_click(self._on_clear)
+ self.export_btn.on_click(self._on_export)
+ # History
+ self.past_dd.observe(self._on_past_dd_changed, names="value")
+ self.past_refresh_btn.on_click(self._on_past_refresh)
+ self.copy_path_btn.on_click(self._on_copy_results_path)
+ self.view_log_btn.on_click(self._on_view_log)
+ # Perf stats reset
+ self._reset_btn.on_click(self._on_reset_click)
+ self._reset_confirm_yes.on_click(self._on_confirm_yes)
+ self._reset_confirm_no.on_click(self._on_confirm_no)
+ # Compare
+ self.compare_refresh_btn.on_click(self._on_compare_refresh)
+ self.compare_btn.on_click(self._on_compare)
+ self.compare_clear_btn.on_click(self._on_compare_clear)
+ # Output log
+ self._log_clear_btn.on_click(self._on_log_clear)
+ # Help
+ self.help_topic_dd.observe(self._on_help_topic_changed, names="value")
+
+ # ══ CALLBACK METHODS ═════════════════════════════════════════════════════
+
+ # ── Theme ─────────────────────────────────────────────────────────────
+
+ def _on_theme_changed(self, change) -> None:
+ self._theme_style.clear_output()
+ css = self._theme_css(change["new"])
+ if css:
+ with self._theme_style:
+ display(HTML(css))
+
+ # ── Molecule input ────────────────────────────────────────────────────
+
+ def _on_load_preset(self, change) -> None:
+ name = change["new"]
+ if name.startswith("("):
+ return
+ d = MOLECULE_LIBRARY[name]
+ self._set_molecule(
+ Molecule(
+ atoms=d["atoms"],
+ coordinates=d["coordinates"],
+ charge=d["charge"],
+ multiplicity=d["multiplicity"],
+ ),
+ d["description"],
+ )
+
+ def _on_load_xyz(self, btn) -> None:
+ try:
+ atoms, coords = parse_xyz_input(self.xyz_area.value.strip())
+ mol = Molecule(atoms=atoms, coordinates=coords)
+ self._set_molecule(mol, "Loaded from XYZ input")
+ self.xyz_msg.value = ""
+ except Exception as exc:
+ self.xyz_msg.value = f"Parse error: {exc}"
+
+ def _on_search_pubchem(self, btn) -> None:
+ query = self.pubchem_txt.value.strip()
+ if not query:
+ self.pubchem_msg.value = "Enter a molecule name or SMILES."
+ return
+ if _student_friendly_fetch is None:
+ self.pubchem_msg.value = "PubChem module not available."
+ return
+ self.pubchem_msg.value = f'Searching for "{query}"...'
+ self.pubchem_btn.disabled = True
+
+ def _do():
+ try:
+ xyz_str, _msg = _student_friendly_fetch(query)
+ if xyz_str is None:
+ raise ValueError(_msg)
+ atoms, coords = parse_xyz_input(xyz_str)
+ mol = Molecule(atoms=atoms, coordinates=coords)
+ self._set_molecule(mol, f"PubChem: {query}")
+ self.pubchem_msg.value = f"Loaded {mol.get_formula()} from PubChem."
+ except Exception as exc:
+ self.pubchem_msg.value = f"Not found: {exc}"
+ finally:
+ self.pubchem_btn.disabled = False
+
+ threading.Thread(target=_do, daemon=True).start()
+
+ def _on_expand_mol_input(self, btn) -> None:
+ self.mol_input_container.children = [
+ self.mol_input_expanded,
+ self.mol_info_html,
+ self.viz_output,
+ ]
+
+ # ── Calc type ─────────────────────────────────────────────────────────
+
+ def _on_calc_type_changed(self, change) -> None:
+ ct = change["new"]
+ if ct == "Geometry Opt":
+ self.calc_extra_opts.children = [
+ widgets.HBox(
+ [self.fmax_fi, self.max_steps_si],
+ layout=widgets.Layout(gap="8px"),
+ ),
+ ]
+ elif ct == "UV-Vis (TD-DFT)":
+ self.calc_extra_opts.children = [
+ self.nstates_si,
+ widgets.HTML(
+ '⚠ Requires a DFT '
+ "functional (e.g. B3LYP, PBE0). RHF/UHF will run TDHF (CIS) "
+ "instead."
+ ),
+ ]
+ else:
+ self.calc_extra_opts.children = []
+
+ # ── Help buttons ──────────────────────────────────────────────────────
+
+ def _on_method_help(self, btn) -> None:
+ self._show_help_topic("method")
+
+ def _on_basis_help(self, btn) -> None:
+ self._show_help_topic("basis_set")
+
+ # ── Run ───────────────────────────────────────────────────────────────
+
+ def _on_run_clicked(self, btn) -> None:
+ threading.Thread(target=self._do_run, daemon=True).start()
+
+ def _on_clear_log(self, btn) -> None:
+ self.run_output.clear_output()
+
+ # ── Accumulate / export ───────────────────────────────────────────────
+
+ def _on_accumulate(self, btn) -> None:
+ r = self._last_result
+ if r is None:
+ return
+ self._results.append(r)
+ self._refresh_comparison()
+
+ def _on_clear(self, btn) -> None:
+ self._results.clear()
+ self.comparison_output.clear_output()
+
+ def _on_export(self, btn) -> None:
+ if self._molecule is None:
+ self.export_status.value = "Load a molecule first."
+ return
+ try:
+ from quantui import PySCFCalculation
+
+ calc = PySCFCalculation(
+ self._molecule,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ )
+ fname = (
+ f"{self._molecule.get_formula()}"
+ f"_{self.method_dd.value}_{self.basis_dd.value}.py"
+ )
+ calc.generate_calculation_script(Path(fname))
+ self.export_status.value = f"Saved: {fname}"
+ except Exception as exc:
+ self.export_status.value = f"Error: {exc}"
+
+ # ── Compare ───────────────────────────────────────────────────────────
+
+ def _on_compare_refresh(self, btn) -> None:
+ self._populate_compare_list()
+
+ def _on_compare(self, btn) -> None:
+ selected = self.compare_select.value
+ if not selected or selected == ("",):
+ return
+ self.compare_output.clear_output(wait=True)
+ from quantui import (
+ comparison_table_html,
+ plot_comparison,
+ summary_from_saved_result,
+ )
+ from quantui.results_storage import load_result
+
+ summaries = []
+ for path_str in selected:
+ if not path_str:
+ continue
+ try:
+ data = load_result(Path(path_str))
+ summaries.append(summary_from_saved_result(data))
+ except Exception as exc:
+ with self.compare_output:
+ display(
+ HTML(
+ f'Error loading result: {exc}
'
+ )
+ )
+ if not summaries:
+ return
+ with self.compare_output:
+ display(HTML(comparison_table_html(summaries)))
+ if len(summaries) > 1:
+ try:
+ import matplotlib.pyplot as plt
+
+ fig = plot_comparison(summaries)
+ display(fig)
+ plt.close(fig)
+ except Exception:
+ pass
+
+ def _on_compare_clear(self, btn) -> None:
+ self.compare_select.value = ()
+ self.compare_output.clear_output()
+
+ # ── History ───────────────────────────────────────────────────────────
+
+ def _on_past_dd_changed(self, change) -> None:
+ path_str = change["new"]
+ if not path_str:
+ self.past_output.clear_output()
+ return
+ self.past_output.clear_output(wait=True)
+ try:
+ from quantui import load_result
+
+ data = load_result(Path(path_str))
+ self.past_output.append_display_data(HTML(self._format_past_result(data)))
+ except Exception as exc:
+ self.past_output.append_stdout(f"Could not load result: {exc}\n")
+
+ def _on_past_refresh(self, btn) -> None:
+ self._refresh_results_browser()
+
+ def _on_copy_results_path(self, btn) -> None:
+ p = self._get_results_dir()
+ p.mkdir(parents=True, exist_ok=True)
+ path_str = str(p).replace("\\", "\\\\").replace("'", "\\'")
+ display(Javascript(f"navigator.clipboard.writeText('{path_str}')"))
+ self.results_path_lbl.value = (
+ f'Copied: {p}'
+ )
+
+ def _reset():
+ time.sleep(3)
+ self.results_path_lbl.value = (
+ f'{p}'
+ )
+
+ threading.Thread(target=_reset, daemon=True).start()
+
+ def _on_view_log(self, btn) -> None:
+ path_str = self.past_dd.value
+ if not path_str:
+ return
+ log_path = Path(path_str) / "pyscf.log"
+ if log_path.exists():
+ text = log_path.read_text(encoding="utf-8", errors="replace")
+ label = Path(path_str).name
+ else:
+ text = "(No pyscf.log found for this result.)"
+ label = ""
+ self._update_log_panel(text, label)
+ self._goto_output_tab()
+
+ # ── Perf stats reset ──────────────────────────────────────────────────
+
+ def _on_reset_click(self, btn) -> None:
+ self._reset_confirm_box.layout.display = ""
+
+ def _on_confirm_yes(self, btn) -> None:
+ from quantui.calc_log import reset_perf_log
+
+ reset_perf_log()
+ self._reset_confirm_box.layout.display = "none"
+ self._refresh_perf_stats()
+
+ def _on_confirm_no(self, btn) -> None:
+ self._reset_confirm_box.layout.display = "none"
+
+ # ── Output log ────────────────────────────────────────────────────────
+
+ def _on_log_clear(self, btn) -> None:
+ self._log_output_html.value = (
+ 'Log cleared.'
+ )
+ self._log_source_lbl.value = ""
+
+ # ── Help ──────────────────────────────────────────────────────────────
+
+ def _on_help_topic_changed(self, change=None) -> None:
+ self._render_help_topic()
+
+ # ══ LOGIC METHODS ════════════════════════════════════════════════════════
+
+ 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.export_btn.disabled = False
+
+ try:
+ n_e = mol.get_electron_count()
+ e_str = f"{n_e} electrons"
+ except Exception:
+ e_str = ""
+
+ _lbl = f'
{label}' if label else ""
+ _summary = (
+ f'{mol.get_formula()}'
+ f' '
+ f"{len(mol.atoms)} atoms"
+ + (f" • {e_str}" if e_str else "")
+ + f" • charge {mol.charge} • mult {mol.multiplicity}"
+ + f"{_lbl}"
+ )
+ self.mol_info_html.value = _summary
+ self.mol_summary_compact.value = (
+ f''
+ f"{_summary}
"
+ )
+
+ self.charge_si.value = mol.charge
+ self.mult_si.value = mol.multiplicity
+ if mol.multiplicity > 1 and self.method_dd.value == "RHF":
+ self.method_dd.value = "UHF"
+
+ self.viz_output.clear_output(wait=True)
+ if _display_molecule is not None:
+ with self.viz_output:
+ _display_molecule(mol)
+
+ self._update_notes()
+
+ # Advance step indicator
+ if self.step_progress._states[2] != "active":
+ if self.step_progress._states[2] in ("done", "fail"):
+ self.step_progress.reset()
+ self.step_progress.complete(0)
+ self.step_progress.start(1)
+
+ self._update_estimate()
+
+ # Collapse molecule input to compact view
+ self.mol_input_container.children = [self.mol_input_collapsed, self.viz_output]
+
+ def _do_run(self) -> None:
+ """Main calculation dispatch — runs in a background thread."""
+ mol = self._molecule
+ if mol is None:
+ self.run_status.value = "Load a molecule first."
+ return
+ self.run_btn.disabled = True
+ self.run_status.value = "Starting..."
+ self.run_output.clear_output()
+ self.result_output.clear_output()
+
+ self.step_progress.complete(1)
+ self.step_progress.start(2)
+
+ _calc_log.log_event(
+ "calc_start",
+ f"{self.method_dd.value}/{self.basis_dd.value} on {mol.get_formula()}",
+ n_atoms=len(mol.atoms),
+ )
+ _run_wall_t = time.perf_counter()
+ log = _LogCapture(self.run_output, self.run_status)
+
+ try:
+ calc_mol = mol
+ if self.preopt_cb.value and _PREOPT_AVAILABLE:
+ self.run_status.value = "Pre-optimizing..."
+ calc_mol, _rmsd = preoptimize(mol)
+ self._set_molecule(
+ calc_mol,
+ f"Geometry pre-optimized (LJ, RMSD={_rmsd:.3f} Å)",
+ )
+
+ ct = self.calc_type_dd.value
+ result: Any = None
+ result_html: str = ""
+ save_spectra: dict = {}
+ save_type: str = "single_point"
+ if ct == "Geometry Opt":
+ self.run_status.value = "Optimizing geometry..."
+ from quantui import optimize_geometry
+
+ result = optimize_geometry(
+ molecule=calc_mol,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ fmax=self.fmax_fi.value,
+ steps=self.max_steps_si.value,
+ progress_stream=log, # type: ignore[arg-type]
+ )
+ result_html = self._format_opt_result(result)
+ save_spectra, save_type = {}, "geometry_opt"
+ elif ct == "Frequency":
+ self.run_status.value = "Computing frequencies (SCF + Hessian)..."
+ from quantui.freq_calc import run_freq_calc
+
+ result = run_freq_calc(
+ molecule=calc_mol,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ progress_stream=log, # type: ignore[arg-type]
+ )
+ result_html = self._format_freq_result(result)
+ save_spectra = {
+ "ir": {
+ "frequencies_cm1": result.frequencies_cm1,
+ "ir_intensities": result.ir_intensities,
+ "zpve_hartree": result.zpve_hartree,
+ }
+ }
+ save_type = "frequency"
+ elif ct == "UV-Vis (TD-DFT)":
+ self.run_status.value = "Running TD-DFT excited states..."
+ from quantui.tddft_calc import run_tddft_calc
+
+ result = run_tddft_calc(
+ molecule=calc_mol,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ nstates=self.nstates_si.value,
+ progress_stream=log, # type: ignore[arg-type]
+ )
+ result_html = self._format_tddft_result(result)
+ save_spectra = {
+ "uv_vis": {
+ "excitation_energies_ev": result.excitation_energies_ev,
+ "oscillator_strengths": result.oscillator_strengths,
+ "wavelengths_nm": result.wavelengths_nm(),
+ }
+ }
+ save_type = "tddft"
+ else: # Single Point
+ self.run_status.value = "Calculating..."
+ from quantui import run_in_session
+
+ result = run_in_session(
+ molecule=calc_mol,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ progress_stream=log, # type: ignore[arg-type]
+ )
+ result_html = self._format_result(result)
+ save_spectra, save_type = {}, "single_point"
+
+ _elapsed = time.perf_counter() - _run_wall_t
+ self._last_result = result
+ self.accumulate_btn.disabled = False
+
+ self.result_output.append_display_data(HTML(result_html))
+ self.run_status.value = f"Done in {_elapsed:.1f} s."
+
+ self.step_progress.complete(2)
+ self.step_progress.complete(3)
+
+ # Persist to disk
+ try:
+ from quantui import save_result
+
+ save_result(
+ result,
+ pyscf_log=log.getvalue(),
+ calc_type=save_type,
+ spectra=save_spectra,
+ )
+ self._refresh_results_browser()
+ self._populate_compare_list()
+ self._update_log_panel(
+ log.getvalue(),
+ f"{result.formula} {self.method_dd.value}/{self.basis_dd.value}",
+ )
+ except Exception:
+ pass
+
+ # Log performance
+ try:
+ _calc_log.log_calculation(
+ formula=result.formula,
+ n_atoms=len(calc_mol.atoms),
+ n_electrons=calc_mol.get_electron_count(),
+ method=result.method,
+ basis=result.basis,
+ n_iterations=getattr(result, "n_iterations", -1),
+ elapsed_s=_elapsed,
+ converged=result.converged,
+ )
+ _calc_log.log_event(
+ "calc_done",
+ f"{result.method}/{result.basis} on {result.formula}",
+ elapsed_s=round(_elapsed, 2),
+ converged=result.converged,
+ )
+ self._update_estimate()
+ except Exception:
+ pass
+
+ except ImportError as _import_err:
+ _err_detail = str(_import_err)
+ log.write(
+ f"Import error: {_err_detail}\n\n"
+ "A required calculation dependency could not be loaded.\n"
+ "On Windows: use the Apptainer container.\n"
+ " apptainer run quantui-local.sif\n"
+ )
+ self.run_status.value = "Import error — see output."
+ self.step_progress.fail(2, _err_detail[:60])
+ _calc_log.log_event("calc_error", _err_detail[:200])
+
+ except Exception as exc:
+ import traceback as _tb
+
+ _elapsed = time.perf_counter() - _run_wall_t
+ log.write(f"Error: {exc}\n\n{_tb.format_exc()}")
+ self.run_status.value = "Error — see Calculation Output below."
+ self.step_progress.fail(2, str(exc)[:60])
+ _calc_log.log_event(
+ "calc_error", str(exc)[:200], elapsed_s=round(_elapsed, 2)
+ )
+
+ finally:
+ self.run_btn.disabled = False
+
+ def _update_notes(self, change=None) -> None:
+ self.notes_output.clear_output(wait=True)
+ if self._molecule is None:
+ return
+ try:
+ from quantui import PySCFCalculation
+
+ calc = PySCFCalculation(
+ self._molecule,
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ )
+ notes = calc.get_educational_notes()
+ if notes:
+ safe = (
+ notes.replace("**", "", 1)
+ .replace("**", "", 1)
+ .replace("\n\n", "
")
+ )
+ with self.notes_output:
+ display(
+ HTML(
+ ''
+ + safe
+ + "
"
+ )
+ )
+ except Exception:
+ pass
+
+ def _update_estimate(self, change=None) -> None:
+ if self._molecule is None:
+ self.perf_estimate_html.value = ""
+ return
+ try:
+ est = _calc_log.estimate_time(
+ n_atoms=len(self._molecule.atoms),
+ n_electrons=self._molecule.get_electron_count(),
+ method=self.method_dd.value,
+ basis=self.basis_dd.value,
+ )
+ self.perf_estimate_html.value = _calc_log.format_estimate(est)
+ except Exception:
+ self.perf_estimate_html.value = ""
+
+ def _refresh_results_browser(self) -> None:
+ try:
+ from quantui import list_results, load_result
+ except ImportError:
+ return
+ self.results_path_lbl.value = (
+ f''
+ f"{self._get_results_dir()}"
+ )
+ dirs = list_results()
+ if not dirs:
+ self.past_dd.options = [("(no saved results)", "")]
+ return
+ options = []
+ for d in dirs:
+ try:
+ data = load_result(d)
+ ts = data.get("timestamp", d.name)
+ label = f"{ts} · {data['formula']} {data['method']}/{data['basis']}"
+ options.append((label, str(d)))
+ except Exception:
+ pass
+ self.past_dd.options = options if options else [("(no saved results)", "")]
+
+ def _refresh_comparison(self) -> None:
+ from quantui import comparison_table_html, summary_from_session_result
+
+ self.comparison_output.clear_output(wait=True)
+ if not self._results:
+ return
+ summaries = [summary_from_session_result(r) for r in self._results]
+ with self.comparison_output:
+ display(HTML(comparison_table_html(summaries)))
+ if len(summaries) > 1:
+ try:
+ from quantui import plot_comparison
+
+ plot_comparison(summaries)
+ except Exception:
+ pass
+
+ def _populate_compare_list(self) -> None:
+ from quantui.results_storage import list_results, load_result
+
+ dirs = list_results()
+ if not dirs:
+ self.compare_select.options = [("(no saved results)", "")]
+ self.compare_btn.disabled = True
+ return
+ options = []
+ for d in dirs:
+ try:
+ data = load_result(d)
+ ts = data.get("timestamp", d.name[:19])
+ label = f"{ts} {data['formula']} {data['method']}/{data['basis']}"
+ options.append((label, str(d)))
+ except Exception:
+ options.append((d.name, str(d)))
+ self.compare_select.options = options
+ self.compare_btn.disabled = False
+
+ def _show_help_topic(self, topic: str) -> None:
+ if topic in HELP_TOPICS:
+ self.help_topic_dd.value = topic
+ self.root_tab.selected_index = 4
+
+ def _update_log_panel(self, log_text: str, label: str = "") -> None:
+ self._render_log(log_text, label)
+
+ def _goto_output_tab(self) -> None:
+ self.root_tab.selected_index = 3
+
+ def _render_log(self, text: str, source_label: str = "") -> None:
+ import html as _html_mod
+
+ lines = text.splitlines()
+ rows = []
+ for line in lines:
+ esc = _html_mod.escape(line)
+ if "converged SCF energy" in line or "SCF converged" in line:
+ style = "color:#16a34a;font-weight:600"
+ elif "cycle=" in line and "E=" in line:
+ style = "color:#475569"
+ elif "HOMO" in line or "LUMO" in line:
+ style = "color:#2563eb"
+ elif "Warning" in line or "warning" in line:
+ style = "color:#d97706"
+ elif "Error" in line or "error" in line or "failed" in line:
+ style = "color:#dc2626"
+ else:
+ style = "color:#1e293b"
+ rows.append(f'{esc}
')
+ self._log_output_html.value = (
+ ''
+ + "".join(rows)
+ + "
"
+ )
+ self._log_source_lbl.value = (
+ f'Source: {source_label}'
+ if source_label
+ else ""
+ )
+
+ def _render_help_topic(self, change=None) -> None:
+ key = self.help_topic_dd.value
+ if key and key in HELP_TOPICS:
+ entry = HELP_TOPICS[key]
+ self.help_content_html.value = (
+ f''
+ f'
'
+ f'{entry["title"]}
'
+ f'
'
+ f'{entry["body"]}
'
+ f"
"
+ )
+
+ def _refresh_perf_stats(self) -> None:
+ self._perf_stats_html.value = self._build_perf_stats_html()
+ self._perf_events_html.value = self._build_events_html()
+
+ def _build_perf_stats_html(self) -> str:
+ from quantui.calc_log import get_perf_history
+
+ records = get_perf_history()
+ if not records:
+ return (
+ ''
+ "No performance data recorded yet."
+ )
+ groups: dict = {}
+ for r in records:
+ key = (r.get("method", "?"), r.get("basis", "?"))
+ groups.setdefault(key, []).append(r)
+ rows = ""
+ for (meth, bas), recs in sorted(groups.items()):
+ times = [r["elapsed_s"] for r in recs if "elapsed_s" in r]
+ n = len(recs)
+ if times:
+ avg = sum(times) / len(times)
+ rows += (
+ ""
+ f'| {meth} | '
+ f'{bas} | '
+ f'{n} | '
+ f'{avg:.1f} s | '
+ f'{min(times):.1f} s | '
+ f'{max(times):.1f} s | '
+ "
"
+ )
+ header = (
+ ""
+ '| Method | '
+ 'Basis | '
+ 'Runs | '
+ 'Avg | '
+ 'Min | '
+ 'Max | '
+ "
"
+ )
+ return (
+ '"
+ )
+
+ def _build_events_html(self) -> str:
+ from quantui.calc_log import get_recent_events
+
+ events = get_recent_events(20)
+ if not events:
+ return (
+ ''
+ "No events recorded yet."
+ )
+ rows = ""
+ for e in reversed(events):
+ ts = e.get("timestamp", "")[:19].replace("T", " ")
+ evt = e.get("event", "")
+ msg = e.get("message", "")
+ rows += (
+ ""
+ f'| {ts} | '
+ f'{evt} | '
+ f'{msg} | '
+ "
"
+ )
+ return (
+ '"
+ )
+
+ # ══ RESULT FORMATTERS ════════════════════════════════════════════════════
+
+ def _format_result(self, r) -> str:
+ _conv = "Yes" if r.converged else "No (treat results with caution)"
+ _cc = "green" if r.converged else "#c00"
+ _gap = (
+ f"{r.homo_lumo_gap_ev:.4f} eV" if r.homo_lumo_gap_ev is not None else "N/A"
+ )
+ _rows = "".join(
+ f""
+ f'| {k} | '
+ f'{v} | '
+ f"
"
+ for k, v, vc in [
+ (
+ "Total energy",
+ f"{r.energy_hartree:.8f} Ha ({r.energy_ev:.4f} eV)",
+ "#000",
+ ),
+ ("HOMO-LUMO gap", _gap, "#000"),
+ ("SCF converged", _conv, _cc),
+ ("SCF iterations", str(r.n_iterations), "#000"),
+ ]
+ )
+ return (
+ f''
+ f"
{r.formula} — {r.method}/{r.basis}"
+ f'
"
+ )
+
+ def _format_opt_result(self, r) -> str:
+ _conv = "Yes" if r.converged else "No (max steps reached)"
+ _cc = "green" if r.converged else "#c00"
+ _rows = "".join(
+ f""
+ f'| {k} | '
+ f'{v} | '
+ f"
"
+ for k, v, vc in [
+ ("Final energy", f"{r.energy_hartree:.8f} Ha", "#000"),
+ ("Energy change", f"{r.energy_change_hartree:+.6f} Ha", "#000"),
+ ("Opt converged", _conv, _cc),
+ ("Steps taken", str(r.n_steps), "#000"),
+ ("Geometry RMSD", f"{r.rmsd_angstrom:.4f} Å", "#000"),
+ ]
+ )
+ return (
+ f''
+ f"
Geometry Optimisation — {r.formula} ({r.method}/{r.basis})"
+ f'
"
+ )
+
+ def _format_freq_result(self, r) -> str:
+ _conv = "Yes" if r.converged else "No (treat with caution)"
+ _cc = "green" if r.converged else "#c00"
+ n_real = r.n_real_modes()
+ n_imag = r.n_imaginary_modes()
+ real_freqs = sorted(f for f in r.frequencies_cm1 if f > 0)[:6]
+ freq_str = " ".join(f"{f:.1f}" for f in real_freqs)
+ if len([f for f in r.frequencies_cm1 if f > 0]) > 6:
+ freq_str += " …"
+ imag_note = ""
+ if n_imag > 0:
+ imag_note = (
+ f'| Imaginary modes | '
+ f'{n_imag} — geometry may not be a minimum |
'
+ )
+ _rows = (
+ f'| SCF energy | '
+ f'{r.energy_hartree:.8f} Ha |
'
+ f'| SCF converged | '
+ f'{_conv} |
'
+ f'| Real modes | '
+ f'{n_real} |
'
+ + imag_note
+ + (
+ f'| Frequencies (cm⁻¹) | '
+ f'{freq_str or "none"} |
'
+ if real_freqs
+ else ""
+ )
+ + f'| ZPVE | '
+ f'{r.zpve_hartree:.6f} Ha '
+ f"({r.zpve_hartree * 27.211386245988:.4f} eV) |
"
+ )
+ return (
+ f''
+ f"
Frequency Analysis — {r.formula} ({r.method}/{r.basis})"
+ f'
"
+ )
+
+ def _format_tddft_result(self, r) -> str:
+ _conv = "Yes" if r.converged else "No (treat with caution)"
+ _cc = "green" if r.converged else "#c00"
+ header_rows = (
+ f'| Ground-state energy | '
+ f'{r.energy_hartree:.8f} Ha |
'
+ f'| SCF converged | '
+ f'{_conv} |
'
+ f'| States computed | '
+ f'{len(r.excitation_energies_ev)} |
'
+ )
+ exc_table = ""
+ if r.excitation_energies_ev:
+ wl = r.wavelengths_nm()
+ exc_rows = []
+ for i, (e_ev, f_osc) in enumerate(
+ zip(r.excitation_energies_ev[:8], r.oscillator_strengths[:8]), 1
+ ):
+ bold = "font-weight:bold" if f_osc > 0.05 else ""
+ exc_rows.append(
+ f''
+ f'| S{i} | '
+ f'{e_ev:.3f} eV | '
+ f'{wl[i - 1]:.1f} nm | '
+ f'f = {f_osc:.4f} | '
+ f"
"
+ )
+ if len(r.excitation_energies_ev) > 8:
+ exc_rows.append(
+ f'| … '
+ f"and {len(r.excitation_energies_ev) - 8} more states |
"
+ )
+ exc_table = (
+ '| '
+ "Vertical excitations: |
"
+ ""
+ '| State | '
+ 'Energy | '
+ 'λ | '
+ 'Osc. str. |
'
+ + "".join(exc_rows)
+ )
+ return (
+ f''
+ f"
TD-DFT / UV-Vis — {r.formula} ({r.method}/{r.basis})"
+ f'
'
+ f"{header_rows}{exc_table}
"
+ )
+
+ def _format_past_result(self, data: dict) -> str:
+ _conv = "Yes" if data.get("converged") else "No (treat results with caution)"
+ _cc = "green" if data.get("converged") else "#c00"
+ _gap = (
+ f"{data['homo_lumo_gap_ev']:.4f} eV"
+ if data.get("homo_lumo_gap_ev") is not None
+ else "N/A"
+ )
+ _rows = "".join(
+ f""
+ f'| {k} | '
+ f'{v} | '
+ f"
"
+ for k, v, vc in [
+ (
+ "Total energy",
+ f"{data['energy_hartree']:.8f} Ha ({data['energy_ev']:.4f} eV)",
+ "#000",
+ ),
+ ("HOMO-LUMO gap", _gap, "#000"),
+ ("SCF converged", _conv, _cc),
+ ("SCF iterations", str(data.get("n_iterations", "?")), "#000"),
+ ]
+ )
+ ts = data.get("timestamp", "")
+ return (
+ f''
+ f'
{data["formula"]} — {data["method"]}/{data["basis"]}'
+ f'
{ts}'
+ f'
"
+ )
+
+ # ══ HELPERS ══════════════════════════════════════════════════════════════
+
+ def _get_results_dir(self) -> Path:
+ from quantui.results_storage import _default_results_dir
+
+ return _default_results_dir().resolve()
diff --git a/quantui/optimizer.py b/quantui/optimizer.py
index c5dac21..fe9fa2e 100644
--- a/quantui/optimizer.py
+++ b/quantui/optimizer.py
@@ -116,6 +116,9 @@ def calculate(
_sink = io.StringIO() # absorb all PySCF output
+ if self.atoms is None:
+ raise RuntimeError("No Atoms object attached to calculator.")
+
# Build PySCF molecule from the current ASE geometry
mol = gto.Mole()
mol.atom = [
From c740ffc88758aa671a91f8e4c791e5edf6887804 Mon Sep 17 00:00:00 2001
From: NCCU-Schultz-Lab
Date: Thu, 16 Apr 2026 16:33:03 -0400
Subject: [PATCH 02/13] Add Copilot context, app tests, and container checks
Add stable AI assistant context and developer test artifacts. Introduces .github/copilot-instructions.md describing repo structure, architecture, constraints, and development guidance for QuantUI-local. Adds notebooks/_inspect_nb.py (notebook inspector) and a temporary notebooks/app_test.ipynb to exercise the QuantUIApp UI, plus a pre-FR012 backup notebook (molecule_computations.pre-fr012.ipynb). Streamlines/updates notebooks/molecule_computations.ipynb in preparation for the FR-012 app refactor. Update apptainer/quantui-local.def to run import checks for QuantUIApp during build/runtime. Add tests/test_app.py and update tests/test_pubchem.py to support the new test coverage.
---
.github/copilot-instructions.md | 394 ++++
apptainer/quantui-local.def | 2 +
notebooks/_inspect_nb.py | 10 +
notebooks/app_test.ipynb | 43 +
notebooks/molecule_computations.ipynb | 1726 +---------------
.../molecule_computations.pre-fr012.ipynb | 1791 +++++++++++++++++
tests/test_app.py | 338 ++++
tests/test_pubchem.py | 1 +
8 files changed, 2581 insertions(+), 1724 deletions(-)
create mode 100644 .github/copilot-instructions.md
create mode 100644 notebooks/_inspect_nb.py
create mode 100644 notebooks/app_test.ipynb
create mode 100644 notebooks/molecule_computations.pre-fr012.ipynb
create mode 100644 tests/test_app.py
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..fd887c7
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,394 @@
+# QuantUI-local — AI Assistant Context
+
+> Stable project context for GitHub Copilot, Claude, and other AI coding assistants.
+> Describes what the project IS and how it is built — not where development currently
+> stands (see `planning/SESSION-HANDOFF.md` for that). Update this file when
+> architecture or conventions change, not every session.
+
+---
+
+## Overview
+
+QuantUI-local is an interactive Jupyter/Voilà interface for running PySCF quantum
+chemistry calculations locally — no cluster account, no SLURM, no queueing. Students
+design molecules, launch RHF/UHF/DFT calculations in their own Python kernel, and
+visualize results in minutes. It is a downstream port of the cluster-focused
+`QuantUI` repo with all SLURM infrastructure removed.
+
+**Target audience:** Undergraduate chemistry students at North Carolina Central
+University. The UI runs as a Voilà app — students never see code.
+
+---
+
+## Repository Structure
+
+```
+QuantUI-local/
+├── quantui/ ← Main Python package (imports as `quantui`)
+│ ├── app.py ← QuantUIApp class — all widgets, callbacks, state
+│ ├── molecule.py ← Molecule dataclass + XYZ/SMILES parsing
+│ ├── session_calc.py ← In-session PySCF runner (run_in_session)
+│ ├── optimizer.py ← QM geometry optimization (ASE-BFGS + PySCF)
+│ ├── freq_calc.py ← Frequency / vibrational analysis
+│ ├── tddft_calc.py ← TD-DFT UV-Vis excited states
+│ ├── calculator.py ← PySCFCalculation abstraction
+│ ├── comparison.py ← Side-by-side result comparison table
+│ ├── results_storage.py ← Persist/reload calculation results (JSON + log)
+│ ├── calc_log.py ← Performance + event logging (JSONL)
+│ ├── pubchem.py ← PubChem molecule search
+│ ├── visualization_py3dmol.py ← 3D molecular viewer (py3Dmol)
+│ ├── ase_bridge.py ← ASE structure I/O + molecule library
+│ ├── preopt.py ← ASE force-field pre-optimisation (fast, no PySCF)
+│ ├── progress.py ← StepProgress widget
+│ ├── help_content.py ← HELP_TOPICS dict — in-app educational text
+│ ├── orbital_visualization.py ← Orbital energy diagrams, cube file viewer
+│ ├── config.py ← All constants/defaults (methods, basis sets, etc.)
+│ ├── utils.py ← Session resource checks, sanitize_filename, etc.
+│ └── security.py ← SecurityError exception class
+├── notebooks/
+│ ├── molecule_computations.ipynb ← Student-facing Voilà app (thin launcher)
+│ └── tutorials/ ← 01–05 step-by-step tutorial notebooks
+├── tests/ ← pytest suite (~440 tests)
+├── planning/ ← Planning docs (not committed to git)
+│ ├── SESSION-HANDOFF.md ← Start here each session — current state
+│ ├── feature-requests.md ← FR backlog
+│ └── FR-*.md ← Individual feature request specs
+├── apptainer/
+│ ├── quantui-local.def ← Apptainer container definition
+│ └── build.sh ← Build script
+├── local-setup/ ← Conda environment YAMLs
+├── launch-app.bat ← Windows double-click launcher (Voilà app mode)
+├── launch-dev.bat ← Windows double-click launcher (JupyterLab mode)
+├── pyproject.toml ← Package config (name: quantui-local, imports as quantui)
+└── pytest.ini ← pytest configuration
+```
+
+---
+
+## Architecture
+
+```
+notebooks/molecule_computations.ipynb
+ Cell 0: Markdown title
+ Cell 1: Conda env check (skip-execution, remove-input)
+ Cell 2: from quantui.app import QuantUIApp; QuantUIApp().display()
+ │
+ ▼
+ quantui/app.py — QuantUIApp
+ ┌──────────────────────────────────────────────────────────┐
+ │ _build_shared_widgets() → StepProgress, run_output │
+ │ _build_molecule_section() → mol_input_container │
+ │ _build_calc_setup() → method_dd, basis_dd, etc. │
+ │ _build_run_section() → run_btn, run_panel │
+ │ _build_results_section() → results_panel │
+ │ _build_history_section() → history_panel │
+ │ _build_compare_section() → compare_panel │
+ │ _build_output_tab() → log viewer │
+ │ _build_help_section() → help panel │
+ │ _assemble_tabs() → root_tab (Tab widget) │
+ └──────────────────────────────────────────────────────────┘
+ │ _do_run() dispatches by calc_type_dd.value:
+ ▼
+ ┌──────────────────────────────────────────────────────────────┐
+ │ Single Point → session_calc.run_in_session() │
+ │ Geometry Opt → optimizer.optimize_geometry() │
+ │ Frequency → freq_calc.run_freq_calc() │
+ │ UV-Vis → tddft_calc.run_tddft_calc() │
+ └──────────────────────────────────────────────────────────────┘
+ │
+ ▼
+ results_storage.save_result() calc_log.log_perf_record()
+```
+
+**Tab order in the app:** Calculate (0) → History (1) → Compare (2) → Output (3) → Help (4)
+
+---
+
+## Key Files
+
+| File | Purpose |
+| ---- | ------- |
+| `quantui/app.py` | **Primary development target.** All widget logic lives here. |
+| `quantui/session_calc.py` | `run_in_session()` — PySCF SCF runner; returns `SessionResult` |
+| `quantui/molecule.py` | `Molecule` dataclass, `parse_xyz_input()` |
+| `quantui/config.py` | `SUPPORTED_METHODS`, `SUPPORTED_BASIS_SETS`, `MOLECULE_LIBRARY`, widget layout constants |
+| `quantui/results_storage.py` | `save_result()`, `load_result()`, `list_results()` — result.json schema v2 |
+| `quantui/calc_log.py` | `perf_log.jsonl` + `event_log.jsonl` under `~/.quantui/logs/` |
+| `quantui/optimizer.py` | `optimize_geometry()` — ASE-BFGS + custom `_QuantUIPySCFCalc` |
+| `quantui/freq_calc.py` | `run_freq_calc()` — vibrational analysis via `pyscf.hessian` |
+| `quantui/tddft_calc.py` | `run_tddft_calc()` — excited states via `pyscf.tddft` |
+| `notebooks/molecule_computations.ipynb` | Thin launcher — 3 cells only (do not add logic here) |
+| `planning/SESSION-HANDOFF.md` | **Read this first every session** — current state, git log, open tasks |
+| `planning/feature-requests.md` | FR backlog |
+
+---
+
+## Critical Constraints
+
+> These are hard rules that must be respected in all implementation work.
+
+1. **PySCF is Linux/macOS/WSL only.** Never assume PySCF is available. All
+ PySCF-dependent code must be guarded with `try/except ImportError`. The
+ availability flags in `app.py` (`_PYSCF_AVAILABLE`, `_PREOPT_AVAILABLE`,
+ `ASE_AVAILABLE`, `VISUALIZATION_AVAILABLE`) are computed once at module
+ import — check these instead of importing inline.
+
+2. **No notebook cell logic.** `notebooks/molecule_computations.ipynb` is a
+ three-cell thin launcher. Never add widget creation, callbacks, or business
+ logic to notebook cells. All logic belongs in `quantui/app.py`.
+
+3. **Thread-safe widget updates only.** `_do_run()` runs in a background thread.
+ Widget updates from threads must use `.value =` assignment,
+ `.append_stdout()`, or `.append_display_data()`. Never call `display()` inside
+ `with output_widget:` from a background thread.
+
+4. **No new top-level dependencies** without updating both `pyproject.toml`
+ and the Apptainer container `apptainer/quantui-local.def`.
+
+5. **All constants in `config.py`.** Method names, basis sets, layout widths,
+ and other shared literals must be defined in `config.py` and imported from
+ there — never hard-coded in `app.py` or other modules.
+
+6. **Result schema versioning.** `results_storage.py` uses `_SCHEMA_VERSION = 2`.
+ Any new fields must be additive (never remove or rename existing keys). Bump
+ `_SCHEMA_VERSION` only when a breaking change is unavoidable.
+
+---
+
+## Supported Calculations
+
+| Calc type | Module | Key function | Returns |
+| --- | --- | --- | --- |
+| Single Point | `session_calc` | `run_in_session()` | `SessionResult` |
+| Geometry Opt | `optimizer` | `optimize_geometry()` | `OptResult` |
+| Frequency | `freq_calc` | `run_freq_calc()` | `FreqResult` |
+| UV-Vis TD-DFT | `tddft_calc` | `run_tddft_calc()` | `TDDFTResult` |
+
+**Supported methods:** RHF, UHF, B3LYP, PBE, PBE0, M06-2X (defined in
+`config.SUPPORTED_METHODS`).
+
+**Supported basis sets:** STO-3G, 3-21G, 6-31G, 6-31G\*, 6-31G\*\*, cc-pVDZ,
+cc-pVTZ, def2-SVP, def2-TZVP (defined in `config.SUPPORTED_BASIS_SETS`).
+
+---
+
+## `QuantUIApp` Class (`quantui/app.py`)
+
+### Construction order
+
+```
+__init__()
+ _build_shared_widgets()
+ _build_molecule_section()
+ _build_calc_setup()
+ _build_run_section() # uses self.calc_type_dd from _build_calc_setup
+ _build_results_section()
+ _build_history_section()
+ _build_compare_section()
+ _build_output_tab()
+ _build_help_section()
+ _assemble_tabs() # builds self.root_tab
+ _wire_callbacks() # all .observe() and .on_click() wiring
+```
+
+### Key instance state
+
+| Attribute | Type | Purpose |
+| --- | --- | --- |
+| `self._molecule` | `Optional[Molecule]` | Currently loaded molecule |
+| `self._last_result` | `Optional[...]` | Most recent calculation result |
+| `self._results` | `list` | All results from this session |
+| `self._pyscf_available` | `bool` | Mirrors module-level `_PYSCF_AVAILABLE` |
+| `self.root_tab` | `widgets.Tab` | Top-level displayed widget |
+| `self.method_dd` | `widgets.Dropdown` | Selected QC method |
+| `self.basis_dd` | `widgets.Dropdown` | Selected basis set |
+| `self.calc_type_dd` | `widgets.Dropdown` | Single Point / Geo Opt / Frequency / UV-Vis |
+
+### Molecule collapse/expand pattern
+
+`mol_input_container` is a `widgets.VBox` whose `.children` is swapped:
+- **Expanded** (initial): `[mol_input_expanded, mol_info_html, viz_output]`
+- **Collapsed** (after `_set_molecule()`): `[mol_input_collapsed, viz_output]`
+
+Clicking "Change molecule" re-expands.
+
+### CSS injection
+
+`display(HTML(_APP_CSS))` fires inside `display()` before `display(self.root_tab)`.
+Never import this module in a context where IPython display is not available without
+catching the resulting error — or just don't call `.display()` (instantiation is safe).
+
+---
+
+## Result Storage (`quantui/results_storage.py`)
+
+Results are saved to timestamped subdirectories:
+```
+/___/
+ result.json ← schema v2 (see below)
+ pyscf.log ← raw PySCF stdout (may be absent)
+```
+
+Default results dir: `Path("results")` relative to cwd, or `$QUANTUI_RESULTS_DIR`.
+In the Apptainer container: `$HOME/.quantui/results`.
+
+### result.json schema (version 2)
+
+```json
+{
+ "_schema_version": 2,
+ "timestamp": "YYYY-MM-DD_HH-MM-SS-ffffff",
+ "calc_type": "single_point | geometry_opt | frequency | tddft",
+ "formula": "H2O",
+ "method": "RHF",
+ "basis": "STO-3G",
+ "energy_hartree": -75.0,
+ "energy_ev": -2040.8,
+ "homo_lumo_gap_ev": 12.3,
+ "converged": true,
+ "n_iterations": 12,
+ "spectra": {
+ "ir": {"frequencies_cm1": [...], "ir_intensities": [...], "zpve_hartree": 0.021},
+ "uv_vis": {"excitation_energies_ev": [...], "oscillator_strengths": [...], "wavelengths_nm": [...]}
+ }
+}
+```
+
+Timestamp includes microseconds (`-%f`) to prevent same-second directory collisions.
+
+---
+
+## Performance Logging (`quantui/calc_log.py`)
+
+Two JSONL files under `~/.quantui/logs/` (override with `$QUANTUI_LOG_DIR`):
+
+| File | Contents | Retention |
+| --- | --- | --- |
+| `perf_log.jsonl` | One record per converged run: formula, n_atoms, n_electrons, method, basis, elapsed_s | Permanent |
+| `event_log.jsonl` | Startup / calc_start / calc_done / calc_error events | 7-day auto-prune |
+
+Key API: `log_perf_record()`, `get_perf_history()`, `get_recent_events(n)`,
+`reset_perf_log()`, `estimate_time(n_atoms, n_electrons, method, basis)`.
+
+---
+
+## Naming Conventions
+
+- **Functions:** `verb_noun()` — e.g., `parse_xyz_input()`, `run_in_session()`, `save_result()`
+- **Classes:** `PascalCase` — e.g., `QuantUIApp`, `SessionResult`, `FreqResult`
+- **Private methods/helpers:** leading underscore — e.g., `_do_run()`, `_set_molecule()`
+- **Builder methods in `QuantUIApp`:** `_build_()` — e.g., `_build_calc_setup()`
+- **Callback methods:** `_on__()` — e.g., `_on_run_clicked()`, `_on_theme_changed()`
+- **Module-level availability flags:** `_PYSCF_AVAILABLE`, `_PREOPT_AVAILABLE`, `ASE_AVAILABLE`
+- **Config constants:** `ALL_CAPS_SNAKE_CASE` in `config.py`
+- **Section banners in `app.py`:** `# ══ SECTION NAME ══` delimiters for VS Code outline navigation
+
+---
+
+## How to Run
+
+```powershell
+# Activate environment (Windows PowerShell)
+& "$env:USERPROFILE\miniconda3\shell\condabin\conda-hook.ps1"
+conda activate quantui-local
+
+# Voilà app mode (student-facing — no code visible)
+voila notebooks/molecule_computations.ipynb
+
+# JupyterLab (development)
+jupyter lab notebooks/molecule_computations.ipynb
+
+# Run tests
+python -m pytest --tb=short -q
+
+# Install/update package
+pip install -e ".[dev]"
+
+# Verify app.py import
+python -c "from quantui.app import QuantUIApp; print('OK')"
+```
+
+**Python executable:** `C:\Users\schul\miniconda3\envs\quantui-local\python.exe`
+
+Note: PySCF calculations will show "unavailable" on Windows — this is expected.
+All UI, molecule, visualization, and PubChem features work natively on Windows.
+
+---
+
+## Testing
+
+Test files in `tests/`:
+
+| File | What it covers |
+| --- | --- |
+| `test_molecule.py` | Molecule parsing, validation, formula |
+| `test_session_calc.py` | `run_in_session()` — PySCF-gated with `pyscf_only` marker |
+| `test_notebook_workflows.py` | End-to-end HF/DFT/preopt/thread-safety — PySCF-gated |
+| `test_optimizer.py` | `optimize_geometry()` — PySCF + ASE required |
+| `test_comparison.py` | Result comparison tables |
+| `test_results_storage.py` | Save/load/list round-trip |
+| `test_security.py` | `SecurityError`, `sanitize_filename()` |
+| `test_phase1.py` | `QuantUIApp` instantiation (no display) |
+
+**PySCF-gated tests** use `@pytest.mark.skipif(not _PYSCF_AVAILABLE, ...)`.
+On Windows, these become skips — not failures.
+
+---
+
+## Optional Dependencies
+
+| Extra | Packages | Gated by |
+| --- | --- | --- |
+| `pyscf` | `pyscf>=2.3.0` | `_PYSCF_AVAILABLE` flag |
+| `ase` | `ase>=3.22.0` | `ASE_AVAILABLE` flag |
+| `app` | `voila, jupyterlab` | Always present in the conda env |
+
+Install all: `pip install -e ".[pyscf,ase,app,dev]"`
+
+---
+
+## Environment Variables
+
+| Variable | Default | Purpose |
+| --- | --- | --- |
+| `QUANTUI_RESULTS_DIR` | `./results` | Where calculation results are saved |
+| `QUANTUI_LOG_DIR` | `~/.quantui/logs` | Where perf_log and event_log live |
+
+---
+
+## Apptainer Container
+
+The container at `apptainer/quantui-local.def` bundles Python + PySCF + ASE +
+py3Dmol + Voilà into a single portable `.sif` file. This is the supported path
+for Windows users.
+
+- Build: `bash apptainer/build.sh --clean` (requires Linux/WSL with Apptainer ≥ 1.0)
+- Run (app mode): `apptainer run quantui-local.sif app`
+- Run (JupyterLab): `apptainer run quantui-local.sif`
+- Verify: `apptainer test quantui-local.sif`
+
+The container sets `QUANTUI_RESULTS_DIR=$HOME/.quantui/results` so results survive
+across kernel restarts and are accessible from the host (home dir is bind-mounted).
+
+---
+
+## Relationship to Source Repo
+
+QuantUI-local is a downstream port of `NCCU-Schultz-Lab/QuantUI` (the cluster version).
+Bug fixes and module updates originate in `QuantUI` and are ported here.
+Never make independent architectural changes in this repo — propose them in `QuantUI` first.
+
+| Removed from source | Reason |
+| --- | --- |
+| `job_manager.py` | SLURM batch submission |
+| `storage.py` | SLURM job metadata |
+| `slurm_errors.py` | SLURM error translation |
+| `visualization.py` | PlotlyMol fallback (excluded here) |
+| SLURM templates in `config.py` | No cluster |
+
+---
+
+## Active Development Branch
+
+Branch: `app-restructure` — FR-012 App Module Refactor in progress.
+See `planning/SESSION-HANDOFF.md` for current phase and uncommitted changes.
diff --git a/apptainer/quantui-local.def b/apptainer/quantui-local.def
index 32e3358..00434d6 100644
--- a/apptainer/quantui-local.def
+++ b/apptainer/quantui-local.def
@@ -73,6 +73,7 @@ FROM: continuumio/miniconda3:latest
python -c "import ase; print('ASE OK')"
python -c "from quantui import optimize_geometry; print('optimize_geometry OK')"
python -c "from quantui import run_freq_calc, run_tddft_calc; print('freq/tddft OK')"
+ python -c "from quantui.app import QuantUIApp; print('QuantUIApp OK')"
echo "Cleaning up unnecessary files..."
# Version control and caches
@@ -125,4 +126,5 @@ EOF
python -c "import py3Dmol" || exit 1
python -c "import ase" || exit 1
python -c "from quantui import Molecule; print('Molecule OK')" || exit 1
+ python -c "from quantui.app import QuantUIApp; print('QuantUIApp OK')" || exit 1
echo "All tests passed!"
diff --git a/notebooks/_inspect_nb.py b/notebooks/_inspect_nb.py
new file mode 100644
index 0000000..34f6288
--- /dev/null
+++ b/notebooks/_inspect_nb.py
@@ -0,0 +1,10 @@
+import json
+with open('notebooks/molecule_computations.ipynb', encoding='utf-8') as f:
+ nb = json.load(f)
+print("nbformat: {}.{}".format(nb["nbformat"], nb["nbformat_minor"]))
+print("Total cells: {}".format(len(nb["cells"])))
+for i, cell in enumerate(nb["cells"]):
+ cid = cell.get("id", "N/A")
+ src_preview = "".join(cell["source"])[:70].replace("\n", " ")
+ tags = cell.get("metadata", {}).get("tags", [])
+ print(" Cell {}: {} id={} tags={} src={}".format(i, cell["cell_type"], cid, tags, repr(src_preview)))
diff --git a/notebooks/app_test.ipynb b/notebooks/app_test.ipynb
new file mode 100644
index 0000000..bc61992
--- /dev/null
+++ b/notebooks/app_test.ipynb
@@ -0,0 +1,43 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# QuantUI-local — App Class Test (FR-012 Phase 2)\n",
+ "\n",
+ "Side-by-side test notebook for the `QuantUIApp` class before replacing the production notebook.\n",
+ "\n",
+ "**Verify:**\n",
+ "- All 5 tabs render correctly (Calculate, History, Compare, Output, Help)\n",
+ "- Theme toggle (Light / Dark / Dark Blue / Dark Maroon) works\n",
+ "- Molecule input (library, XYZ, PubChem search) loads and collapses correctly\n",
+ "- Calculation type dropdown (Single Point / Geometry Opt / Frequency / UV-Vis) shows/hides extra options\n",
+ "- Run button dispatches correctly (PySCF unavailable on Windows — error message expected)\n",
+ "- History tab loads and displays past results\n",
+ "- Compare tab allows adding and comparing results\n",
+ "\n",
+ "> This notebook is temporary — delete after Phase 3 is complete."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from quantui.app import QuantUIApp\n",
+ "QuantUIApp().display()"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/molecule_computations.ipynb b/notebooks/molecule_computations.ipynb
index 6140e0d..a242219 100644
--- a/notebooks/molecule_computations.ipynb
+++ b/notebooks/molecule_computations.ipynb
@@ -48,1730 +48,8 @@
"metadata": {},
"outputs": [],
"source": [
- "import threading\n",
- "import ipywidgets as widgets\n",
- "from IPython.display import display, HTML\n",
- "\n",
- "import quantui\n",
- "from quantui import (\n",
- " Molecule, parse_xyz_input,\n",
- " MOLECULE_LIBRARY, SUPPORTED_METHODS, SUPPORTED_BASIS_SETS,\n",
- " DEFAULT_METHOD, DEFAULT_BASIS, DEFAULT_CHARGE, DEFAULT_MULTIPLICITY,\n",
- " session_can_handle, get_session_resources,\n",
- " PUBCHEM_AVAILABLE, VISUALIZATION_AVAILABLE, ASE_AVAILABLE,\n",
- " QUICK_START_TEMPLATES,\n",
- ")\n",
- "\n",
- "# Optional — degrade gracefully if unavailable\n",
- "try:\n",
- " from quantui import run_in_session, SessionResult\n",
- " PYSCF_AVAILABLE = True\n",
- "except (ImportError, AttributeError):\n",
- " PYSCF_AVAILABLE = False\n",
- "\n",
- "try:\n",
- " from quantui import student_friendly_fetch\n",
- "except (ImportError, AttributeError):\n",
- " student_friendly_fetch = None\n",
- "\n",
- "try:\n",
- " from quantui import display_molecule\n",
- "except (ImportError, AttributeError):\n",
- " display_molecule = None\n",
- "\n",
- "try:\n",
- " from quantui import preoptimize\n",
- " PREOPT_AVAILABLE = True\n",
- "except (ImportError, AttributeError):\n",
- " PREOPT_AVAILABLE = False\n",
- "\n",
- "# Mutable session state shared across all callbacks\n",
- "_state = {\"molecule\": None, \"last_result\": None, \"results\": []}\n",
- "\n",
- "# ── Global app styles ─────────────────────────────────────────────────────────\n",
- "# Permanent — not toggled by dark mode (dark mode uses filter inversion on top).\n",
- "display(HTML(\"\"\"\"\"\"))\n",
- "\n",
- "# ── Dark mode toggle ─────────────────────────────────────────────────────────\n",
- "# Uses CSS filter invert+hue-rotate on the html element so it works with all\n",
- "# inline-styled elements. canvas/img/iframe are re-inverted to keep their\n",
- "# original appearance (e.g. the py3Dmol 3D viewer).\n",
- "# Map theme name → hue-rotate angle. Light uses no filter.\n",
- "_THEME_HUE = {\"Dark\": 180, \"Dark Blue\": 200, \"Dark Maroon\": 160}\n",
- "\n",
- "\n",
- "def _theme_css(theme: str) -> str:\n",
- " \"\"\"Return the CSS filter block for *theme*, or '' for Light.\"\"\"\n",
- " if theme not in _THEME_HUE:\n",
- " return \"\"\n",
- " deg = _THEME_HUE[theme]\n",
- " return (\n",
- " \"\"\n",
- " )\n",
- "\n",
- "\n",
- "_theme_style = widgets.Output(\n",
- " layout=widgets.Layout(height=\"0px\", overflow=\"hidden\", margin=\"0\", padding=\"0\")\n",
- ")\n",
- "\n",
- "theme_btn = widgets.ToggleButtons(\n",
- " options=[\"Light\", \"Dark\", \"Dark Blue\", \"Dark Maroon\"],\n",
- " value=\"Dark\",\n",
- " description=\"Theme:\",\n",
- " style={\n",
- " \"description_width\": \"48px\",\n",
- " \"button_width\": \"90px\",\n",
- " },\n",
- " layout=widgets.Layout(margin=\"0\"),\n",
- ")\n",
- "\n",
- "\n",
- "def _toggle_theme(change):\n",
- " _theme_style.clear_output()\n",
- " css = _theme_css(change[\"new\"])\n",
- " if css:\n",
- " with _theme_style:\n",
- " display(HTML(css))\n",
- "\n",
- "\n",
- "theme_btn.observe(_toggle_theme, names=\"value\")\n",
- "\n",
- "# Apply Dark theme on startup\n",
- "with _theme_style:\n",
- " display(HTML(_theme_css(\"Dark\")))\n",
- "\n",
- "display(widgets.HBox(\n",
- " [theme_btn],\n",
- " layout=widgets.Layout(justify_content=\"flex-end\", margin=\"0 0 4px\"),\n",
- "))\n",
- "display(_theme_style)\n",
- "\n",
- "# ── Status panel ────────────────────────────────────────────────────────────\n",
- "_cores, _mem_gb = get_session_resources()\n",
- "_mem = f\"{_mem_gb} GB\" if _mem_gb is not None else \"unknown\"\n",
- "\n",
- "\n",
- "def _ok(flag, extra=\"\"):\n",
- " tick = '✓'\n",
- " cross = '✗'\n",
- " return (tick if flag else cross) + (\" \" + extra if extra else \"\")\n",
- "\n",
- "\n",
- "_items = [\n",
- " (\"PySCF (calculations)\", _ok(PYSCF_AVAILABLE,\n",
- " \"\" if PYSCF_AVAILABLE else \"— Linux / macOS / WSL required\")),\n",
- " (\"ASE (structure I/O, opt.)\", _ok(ASE_AVAILABLE)),\n",
- " (\"PubChem search\", _ok(PUBCHEM_AVAILABLE)),\n",
- " (\"3D viewer (py3Dmol)\", _ok(VISUALIZATION_AVAILABLE)),\n",
- " (\"CPU cores / Memory\", f\"{_cores} cores / {_mem}\"),\n",
- "]\n",
- "_rows = \"\".join(\n",
- " f'| {k} | '\n",
- " f'{v} |
'\n",
- " for k, v in _items\n",
- ")\n",
- "display(HTML(\n",
- " f''\n",
- " f'
'\n",
- " f\"QuantUI-local {quantui.__version__}\"\n",
- " f'
'\n",
- " f\"
\"\n",
- "))\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "3",
- "metadata": {},
- "outputs": [],
- "source": [
- "import io\n",
- "import re\n",
- "import time\n",
- "\n",
- "from quantui.progress import StepProgress\n",
- "import quantui.calc_log as _calc_log\n",
- "\n",
- "# ── Shared output widgets ────────────────────────────────────────────────────\n",
- "mol_info_html = widgets.HTML(\n",
- " value='No molecule loaded yet.'\n",
- ")\n",
- "mol_summary_compact = widgets.HTML(value=\"\")\n",
- "viz_output = widgets.Output(layout=widgets.Layout(min_height=\"50px\"))\n",
- "run_output = widgets.Output(\n",
- " layout=widgets.Layout(\n",
- " border=\"1px solid #c0ccd8\", min_height=\"80px\", max_height=\"400px\",\n",
- " padding=\"8px\", overflow_y=\"auto\",\n",
- " )\n",
- ")\n",
- "with run_output:\n",
- " display(HTML(\n",
- " ''\n",
- " \"No calculation run yet. PySCF output and any errors will appear here.\"\n",
- " \"
\"\n",
- " ))\n",
- "result_output = widgets.Output()\n",
- "comparison_output = widgets.Output()\n",
- "notes_output = widgets.Output()\n",
- "perf_estimate_html = widgets.HTML()\n",
- "\n",
- "# ── Step indicator ────────────────────────────────────────────────────────────\n",
- "step_progress = StepProgress([\"Choose molecule\", \"Set method\", \"Run\", \"Results\"])\n",
- "step_progress.start(0)\n",
- "\n",
- "# ── Calculation setup (defined here so _set_molecule can update them) ────────\n",
- "method_dd = widgets.Dropdown(\n",
- " options=SUPPORTED_METHODS, value=DEFAULT_METHOD,\n",
- " description=\"Method:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"260px\"),\n",
- ")\n",
- "basis_dd = widgets.Dropdown(\n",
- " options=SUPPORTED_BASIS_SETS, value=DEFAULT_BASIS,\n",
- " description=\"Basis Set:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"260px\"),\n",
- ")\n",
- "charge_si = widgets.BoundedIntText(\n",
- " value=DEFAULT_CHARGE, min=-10, max=10,\n",
- " description=\"Charge:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "mult_si = widgets.BoundedIntText(\n",
- " value=DEFAULT_MULTIPLICITY, min=1, max=10,\n",
- " description=\"Multiplicity:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "preopt_cb = widgets.Checkbox(\n",
- " value=False,\n",
- " description=\"Pre-optimize geometry (fast LJ force-field)\",\n",
- " disabled=not PREOPT_AVAILABLE,\n",
- " layout=widgets.Layout(width=\"400px\"),\n",
- ")\n",
- "\n",
- "# ── Calculation type + extra options ──────────────────────────────────────────\n",
- "calc_type_dd = widgets.Dropdown(\n",
- " options=[\"Single Point\", \"Geometry Opt\", \"Frequency\", \"UV-Vis (TD-DFT)\"],\n",
- " value=\"Single Point\",\n",
- " description=\"Calc. Type:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"310px\"),\n",
- ")\n",
- "fmax_fi = widgets.BoundedFloatText(\n",
- " value=0.05, min=0.001, max=1.0, step=0.005,\n",
- " description=\"Force thr. (eV/Å):\",\n",
- " style={\"description_width\": \"130px\"},\n",
- " layout=widgets.Layout(width=\"270px\"),\n",
- ")\n",
- "max_steps_si = widgets.BoundedIntText(\n",
- " value=200, min=10, max=1000,\n",
- " description=\"Max steps:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"200px\"),\n",
- ")\n",
- "nstates_si = widgets.BoundedIntText(\n",
- " value=10, min=1, max=50,\n",
- " description=\"# states:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"180px\"),\n",
- ")\n",
- "calc_extra_opts = widgets.VBox([])\n",
- "\n",
- "\n",
- "def _on_calc_type_change(change):\n",
- " ct = change[\"new\"]\n",
- " if ct == \"Geometry Opt\":\n",
- " calc_extra_opts.children = [\n",
- " widgets.HBox(\n",
- " [fmax_fi, max_steps_si],\n",
- " layout=widgets.Layout(gap=\"8px\"),\n",
- " ),\n",
- " ]\n",
- " elif ct == \"UV-Vis (TD-DFT)\":\n",
- " calc_extra_opts.children = [\n",
- " nstates_si,\n",
- " widgets.HTML(\n",
- " '⚠ Requires a DFT '\n",
- " \"functional (e.g. B3LYP, PBE0). RHF/UHF will run TDHF (CIS) \"\n",
- " \"instead.\"\n",
- " ),\n",
- " ]\n",
- " else:\n",
- " calc_extra_opts.children = []\n",
- "\n",
- "\n",
- "calc_type_dd.observe(_on_calc_type_change, names=\"value\")\n",
- "\n",
- "# ── Context-help buttons (next to Method and Basis dropdowns) ────────────────\n",
- "method_help_btn = widgets.Button(\n",
- " description=\"?\", button_style=\"\",\n",
- " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
- " tooltip=\"RHF vs UHF — opens Help tab\",\n",
- ")\n",
- "basis_help_btn = widgets.Button(\n",
- " description=\"?\", button_style=\"\",\n",
- " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
- " tooltip=\"Choosing a basis set — opens Help tab\",\n",
- ")\n",
- "\n",
- "# ── Run widgets ──────────────────────────────────────────────────────────────\n",
- "run_btn = widgets.Button(\n",
- " description=\"Run Calculation\", button_style=\"success\", icon=\"play\",\n",
- " disabled=True, layout=widgets.Layout(width=\"200px\", height=\"36px\"),\n",
- ")\n",
- "run_status = widgets.Label()\n",
- "\n",
- "# ── Log clear button ─────────────────────────────────────────────────────────\n",
- "log_clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"\", icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\", height=\"26px\"),\n",
- " tooltip=\"Clear calculation output\",\n",
- ")\n",
- "\n",
- "def _clear_log(btn):\n",
- " run_output.clear_output()\n",
- "\n",
- "log_clear_btn.on_click(_clear_log)\n",
- "\n",
- "# ── Comparison / export widgets ───────────────────────────────────────────────\n",
- "accumulate_btn = widgets.Button(\n",
- " description=\"Add to Comparison\", button_style=\"info\", icon=\"plus\",\n",
- " disabled=True, layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"warning\", icon=\"trash\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "export_btn = widgets.Button(\n",
- " description=\"Export Script\", button_style=\"\", icon=\"download\",\n",
- " disabled=True, layout=widgets.Layout(width=\"160px\"),\n",
- ")\n",
- "export_status = widgets.Label()\n",
- "\n",
- "\n",
- "# ── Thread-safe log capture ───────────────────────────────────────────────────\n",
- "_RE_CYCLE = re.compile(\n",
- " r\"cycle=\\s*(\\d+)\\s+E=\\s*([\\-\\d\\.]+)\\s+delta_E=\\s*([\\-\\d\\.Ee+\\-]+)\"\n",
- ")\n",
- "_RE_CONV = re.compile(r\"converged SCF energy\\s*=\\s*([\\-\\d\\.]+)\")\n",
- "\n",
- "\n",
- "class _LogCapture:\n",
- " '''Write PySCF output to an Output widget and capture it to a buffer.'''\n",
- " def __init__(self, output_widget):\n",
- " self._w = output_widget\n",
- " self._buf = io.StringIO()\n",
- " self._line_buf = \"\"\n",
- "\n",
- " def write(self, text: str) -> None:\n",
- " if not text:\n",
- " return\n",
- " self._w.append_stdout(text)\n",
- " self._buf.write(text)\n",
- " # Scan complete lines for SCF progress and update the status label.\n",
- " self._line_buf += text\n",
- " while \"\\n\" in self._line_buf:\n",
- " line, self._line_buf = self._line_buf.split(\"\\n\", 1)\n",
- " m = _RE_CYCLE.search(line)\n",
- " if m:\n",
- " n, delta = m.group(1), m.group(3)\n",
- " try:\n",
- " run_status.value = f\"SCF cycle {n} · ΔE = {float(delta):.4g} Ha\"\n",
- " except Exception:\n",
- " run_status.value = f\"SCF cycle {n}\"\n",
- " continue\n",
- " m = _RE_CONV.search(line)\n",
- " if m:\n",
- " run_status.value = \"SCF converged ✓\"\n",
- "\n",
- " def flush(self) -> None:\n",
- " pass\n",
- "\n",
- " def getvalue(self) -> str:\n",
- " return self._buf.getvalue()\n",
- "\n",
- "\n",
- "# Placeholders — overwritten by later cells once they execute.\n",
- "_refresh_results_browser = lambda: None # noqa: E731\n",
- "_show_help_topic = lambda topic: None # noqa: E731\n",
- "_populate_compare_list = lambda: None # noqa: E731\n",
- "_update_log_panel = lambda log_text, label=\"\": None # noqa: E731\n",
- "_goto_output_tab = lambda: None # noqa: E731\n",
- "\n",
- "\n",
- "# ── Callbacks ─────────────────────────────────────────────────────────────────\n",
- "\n",
- "def _set_molecule(mol, label=\"\"):\n",
- " '''Update shared state and refresh dependent widgets.'''\n",
- " _state[\"molecule\"] = mol\n",
- " run_btn.disabled = False\n",
- " export_btn.disabled = False\n",
- "\n",
- " try:\n",
- " n_e = mol.get_electron_count()\n",
- " e_str = f\"{n_e} electrons\"\n",
- " except Exception:\n",
- " e_str = \"\"\n",
- "\n",
- " _lbl = f'
{label}' if label else \"\"\n",
- " _summary = (\n",
- " f'{mol.get_formula()}'\n",
- " f' '\n",
- " f\"{len(mol.atoms)} atoms\"\n",
- " + (f\" • {e_str}\" if e_str else \"\")\n",
- " + f\" • charge {mol.charge} • mult {mol.multiplicity}\"\n",
- " + f\"{_lbl}\"\n",
- " )\n",
- " mol_info_html.value = _summary\n",
- " mol_summary_compact.value = (\n",
- " f''\n",
- " f\"{_summary}
\"\n",
- " )\n",
- "\n",
- " charge_si.value = mol.charge\n",
- " mult_si.value = mol.multiplicity\n",
- " if mol.multiplicity > 1 and method_dd.value == \"RHF\":\n",
- " method_dd.value = \"UHF\"\n",
- "\n",
- " viz_output.clear_output(wait=True)\n",
- " if display_molecule is not None:\n",
- " with viz_output:\n",
- " display_molecule(mol)\n",
- "\n",
- " _update_notes()\n",
- "\n",
- " # Advance step indicator — but not during a running calculation (e.g. preopt)\n",
- " if step_progress._states[2] != \"active\":\n",
- " if step_progress._states[2] in (\"done\", \"fail\"):\n",
- " # Fresh molecule after a completed run — reset indicator\n",
- " step_progress.reset()\n",
- " step_progress.complete(0)\n",
- " step_progress.start(1)\n",
- "\n",
- " # Update time estimate for the newly loaded molecule\n",
- " _update_estimate()\n",
- "\n",
- " # Collapse the molecule input panel to the compact summary + 3D viewer\n",
- " mol_input_container.children = [mol_input_collapsed, viz_output]\n",
- "\n",
- "\n",
- "def _format_result(r):\n",
- " _conv = \"Yes\" if r.converged else \"No (treat results with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " _gap = (\n",
- " f\"{r.homo_lumo_gap_ev:.4f} eV\"\n",
- " if r.homo_lumo_gap_ev is not None else \"N/A\"\n",
- " )\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Total energy\",\n",
- " f\"{r.energy_hartree:.8f} Ha ({r.energy_ev:.4f} eV)\", \"#000\"),\n",
- " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
- " (\"SCF converged\", _conv, _cc),\n",
- " (\"SCF iterations\", str(r.n_iterations), \"#000\"),\n",
- " ]\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
{r.formula} — {r.method}/{r.basis}\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_opt_result(r):\n",
- " '''Format an OptimizationResult as an HTML result card.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (max steps reached)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Final energy\", f\"{r.energy_hartree:.8f} Ha\", \"#000\"),\n",
- " (\"Energy change\", f\"{r.energy_change_hartree:+.6f} Ha\", \"#000\"),\n",
- " (\"Opt converged\", _conv, _cc),\n",
- " (\"Steps taken\", str(r.n_steps), \"#000\"),\n",
- " (\"Geometry RMSD\", f\"{r.rmsd_angstrom:.4f} Å\", \"#000\"),\n",
- " ]\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
Geometry Optimisation — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_freq_result(r):\n",
- " '''Format a FreqResult as an HTML result card.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " n_real = r.n_real_modes()\n",
- " n_imag = r.n_imaginary_modes()\n",
- " real_freqs = sorted(f for f in r.frequencies_cm1 if f > 0)[:6]\n",
- " freq_str = \" \".join(f\"{f:.1f}\" for f in real_freqs)\n",
- " if len([f for f in r.frequencies_cm1 if f > 0]) > 6:\n",
- " freq_str += \" …\"\n",
- " imag_note = \"\"\n",
- " if n_imag > 0:\n",
- " imag_note = (\n",
- " f'| Imaginary modes | '\n",
- " f'{n_imag} — geometry may not be a minimum |
'\n",
- " )\n",
- " _rows = (\n",
- " f'| SCF energy | '\n",
- " f'{r.energy_hartree:.8f} Ha |
'\n",
- " f'| SCF converged | '\n",
- " f'{_conv} |
'\n",
- " f'| Real modes | '\n",
- " f'{n_real} |
'\n",
- " + imag_note\n",
- " + (f'| Frequencies (cm⁻¹) | '\n",
- " f'{freq_str or \"none\"} |
'\n",
- " if real_freqs else \"\")\n",
- " + f'| ZPVE | '\n",
- " f'{r.zpve_hartree:.6f} Ha '\n",
- " f'({r.zpve_hartree * 27.211386245988:.4f} eV) |
'\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
Frequency Analysis — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_tddft_result(r):\n",
- " '''Format a TDDFTResult as an HTML result card with excitation table.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " header_rows = (\n",
- " f'| Ground-state energy | '\n",
- " f'{r.energy_hartree:.8f} Ha |
'\n",
- " f'| SCF converged | '\n",
- " f'{_conv} |
'\n",
- " f'| States computed | '\n",
- " f'{len(r.excitation_energies_ev)} |
'\n",
- " )\n",
- " exc_table = \"\"\n",
- " if r.excitation_energies_ev:\n",
- " wl = r.wavelengths_nm()\n",
- " exc_rows = []\n",
- " for i, (e_ev, f_osc) in enumerate(\n",
- " zip(r.excitation_energies_ev[:8], r.oscillator_strengths[:8]), 1\n",
- " ):\n",
- " bold = \"font-weight:bold\" if f_osc > 0.05 else \"\"\n",
- " exc_rows.append(\n",
- " f''\n",
- " f'| S{i} | '\n",
- " f'{e_ev:.3f} eV | '\n",
- " f'{wl[i - 1]:.1f} nm | '\n",
- " f'f = {f_osc:.4f} | '\n",
- " f\"
\"\n",
- " )\n",
- " if len(r.excitation_energies_ev) > 8:\n",
- " exc_rows.append(\n",
- " f'| … '\n",
- " f\"and {len(r.excitation_energies_ev) - 8} more states |
\"\n",
- " )\n",
- " exc_table = (\n",
- " '| '\n",
- " \"Vertical excitations: |
\"\n",
- " ''\n",
- " '| State | '\n",
- " 'Energy | '\n",
- " 'λ | '\n",
- " 'Osc. str. |
'\n",
- " + \"\".join(exc_rows)\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
TD-DFT / UV-Vis — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
'\n",
- " f\"{header_rows}{exc_table}
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _do_run(btn):\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " run_status.value = \"Load a molecule first.\"\n",
- " return\n",
- " run_btn.disabled = True\n",
- " run_status.value = \"Starting...\"\n",
- " run_output.clear_output()\n",
- " result_output.clear_output()\n",
- "\n",
- " # Advance step indicator: method confirmed → running\n",
- " step_progress.complete(1)\n",
- " step_progress.start(2)\n",
- "\n",
- " _calc_log.log_event(\n",
- " \"calc_start\",\n",
- " f\"{method_dd.value}/{basis_dd.value} on {mol.get_formula()}\",\n",
- " n_atoms=len(mol.atoms),\n",
- " )\n",
- " _run_wall_t = time.perf_counter()\n",
- "\n",
- " def _thread():\n",
- " log = _LogCapture(run_output)\n",
- " try:\n",
- " calc_mol = mol\n",
- " if preopt_cb.value and PREOPT_AVAILABLE:\n",
- " run_status.value = \"Pre-optimizing...\"\n",
- " calc_mol, _rmsd = preoptimize(mol)\n",
- " _set_molecule(calc_mol, f\"Geometry pre-optimized (LJ, RMSD={_rmsd:.3f} Å)\")\n",
- "\n",
- " # ── Dispatch to the right backend based on Calc. Type ─────────────\n",
- " ct = calc_type_dd.value\n",
- " if ct == \"Geometry Opt\":\n",
- " run_status.value = \"Optimizing geometry...\"\n",
- " from quantui import optimize_geometry\n",
- " result = optimize_geometry(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " fmax=fmax_fi.value,\n",
- " steps=max_steps_si.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_opt_result(result)\n",
- " save_spectra, save_type = {}, \"geometry_opt\"\n",
- " elif ct == \"Frequency\":\n",
- " run_status.value = \"Computing frequencies (SCF + Hessian)...\"\n",
- " from quantui.freq_calc import run_freq_calc\n",
- " result = run_freq_calc(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_freq_result(result)\n",
- " save_spectra = {\"ir\": {\n",
- " \"frequencies_cm1\": result.frequencies_cm1,\n",
- " \"ir_intensities\": result.ir_intensities,\n",
- " \"zpve_hartree\": result.zpve_hartree,\n",
- " }}\n",
- " save_type = \"frequency\"\n",
- " elif ct == \"UV-Vis (TD-DFT)\":\n",
- " run_status.value = \"Running TD-DFT excited states...\"\n",
- " from quantui.tddft_calc import run_tddft_calc\n",
- " result = run_tddft_calc(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " nstates=nstates_si.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_tddft_result(result)\n",
- " save_spectra = {\"uv_vis\": {\n",
- " \"excitation_energies_ev\": result.excitation_energies_ev,\n",
- " \"oscillator_strengths\": result.oscillator_strengths,\n",
- " \"wavelengths_nm\": result.wavelengths_nm(),\n",
- " }}\n",
- " save_type = \"tddft\"\n",
- " else: # Single Point\n",
- " run_status.value = \"Calculating...\"\n",
- " result = run_in_session(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_result(result)\n",
- " save_spectra, save_type = {}, \"single_point\"\n",
- " _elapsed = time.perf_counter() - _run_wall_t\n",
- "\n",
- " _state[\"last_result\"] = result\n",
- " accumulate_btn.disabled = False\n",
- "\n",
- " result_output.append_display_data(HTML(result_html))\n",
- " run_status.value = f\"Done in {_elapsed:.1f} s.\"\n",
- "\n",
- " # Advance step indicator: run complete → results ready\n",
- " step_progress.complete(2)\n",
- " step_progress.complete(3)\n",
- "\n",
- " # Persist result to disk\n",
- " try:\n",
- " from quantui import save_result\n",
- " save_result(\n",
- " result, pyscf_log=log.getvalue(),\n",
- " calc_type=save_type, spectra=save_spectra,\n",
- " )\n",
- " _refresh_results_browser()\n",
- " _populate_compare_list()\n",
- " _update_log_panel(\n",
- " log.getvalue(),\n",
- " f\"{result.formula} {method_dd.value}/{basis_dd.value}\",\n",
- " )\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- " # Log performance record and refresh the estimate widget\n",
- " try:\n",
- " _calc_log.log_calculation(\n",
- " formula=result.formula,\n",
- " n_atoms=len(calc_mol.atoms),\n",
- " n_electrons=calc_mol.get_electron_count(),\n",
- " method=result.method,\n",
- " basis=result.basis,\n",
- " n_iterations=getattr(result, \"n_iterations\", -1),\n",
- " elapsed_s=_elapsed,\n",
- " converged=result.converged,\n",
- " )\n",
- " _calc_log.log_event(\n",
- " \"calc_done\",\n",
- " f\"{result.method}/{result.basis} on {result.formula}\",\n",
- " elapsed_s=round(_elapsed, 2),\n",
- " converged=result.converged,\n",
- " )\n",
- " _update_estimate()\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- " except ImportError as _import_err:\n",
- " _err_detail = str(_import_err)\n",
- " log.write(\n",
- " f\"Import error: {_err_detail}\\n\\n\"\n",
- " \"A required calculation dependency could not be loaded.\\n\"\n",
- " \"On Windows: use the Apptainer container.\\n\"\n",
- " \" apptainer run quantui-local.sif\\n\"\n",
- " )\n",
- " run_status.value = \"Import error — see output.\"\n",
- " step_progress.fail(2, _err_detail[:60])\n",
- " _calc_log.log_event(\"calc_error\", _err_detail[:200])\n",
- "\n",
- " except Exception as exc:\n",
- " import traceback\n",
- " _elapsed = time.perf_counter() - _run_wall_t\n",
- " log.write(f\"Error: {exc}\\n\\n{traceback.format_exc()}\")\n",
- " run_status.value = \"Error — see Calculation Output below.\"\n",
- " step_progress.fail(2, str(exc)[:60])\n",
- " _calc_log.log_event(\"calc_error\", str(exc)[:200], elapsed_s=round(_elapsed, 2))\n",
- "\n",
- " finally:\n",
- " run_btn.disabled = False\n",
- "\n",
- " threading.Thread(target=_thread, daemon=True).start()\n",
- "\n",
- "run_btn.on_click(_do_run)\n",
- "\n",
- "\n",
- "def _update_notes(change=None):\n",
- " notes_output.clear_output(wait=True)\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " return\n",
- " try:\n",
- " from quantui import PySCFCalculation\n",
- " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
- " notes = calc.get_educational_notes()\n",
- " if notes:\n",
- " safe = (\n",
- " notes\n",
- " .replace(\"**\", \"\", 1)\n",
- " .replace(\"**\", \"\", 1)\n",
- " .replace(\"\\n\\n\", \"
\")\n",
- " )\n",
- " with notes_output:\n",
- " display(HTML(\n",
- " ''\n",
- " + safe + \"
\"\n",
- " ))\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "method_dd.observe(_update_notes, names=\"value\")\n",
- "basis_dd.observe(_update_notes, names=\"value\")\n",
- "\n",
- "\n",
- "def _update_estimate(change=None):\n",
- " '''Refresh the time-estimate label based on current molecule + method/basis.'''\n",
- " mol = _state.get(\"molecule\")\n",
- " if mol is None:\n",
- " perf_estimate_html.value = \"\"\n",
- " return\n",
- " try:\n",
- " est = _calc_log.estimate_time(\n",
- " n_atoms=len(mol.atoms),\n",
- " n_electrons=mol.get_electron_count(),\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " )\n",
- " perf_estimate_html.value = _calc_log.format_estimate(est)\n",
- " except Exception:\n",
- " perf_estimate_html.value = \"\"\n",
- "\n",
- "method_dd.observe(_update_estimate, names=\"value\")\n",
- "basis_dd.observe(_update_estimate, names=\"value\")\n",
- "\n",
- "# Log startup event\n",
- "_calc_log.log_event(\"startup\", f\"QuantUI-local {quantui.__version__} started\")\n",
- "\n",
- "\n",
- "def _do_accumulate(btn):\n",
- " r = _state[\"last_result\"]\n",
- " if r is None:\n",
- " return\n",
- " _state[\"results\"].append(r)\n",
- " _refresh_comparison()\n",
- "\n",
- "accumulate_btn.on_click(_do_accumulate)\n",
- "\n",
- "\n",
- "def _refresh_comparison():\n",
- " from quantui import summary_from_session_result, comparison_table_html\n",
- " comparison_output.clear_output(wait=True)\n",
- " results = _state[\"results\"]\n",
- " if not results:\n",
- " return\n",
- " summaries = [summary_from_session_result(r) for r in results]\n",
- " with comparison_output:\n",
- " display(HTML(comparison_table_html(summaries)))\n",
- " if len(summaries) > 1:\n",
- " try:\n",
- " from quantui import plot_comparison\n",
- " plot_comparison(summaries)\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "\n",
- "def _do_clear(btn):\n",
- " _state[\"results\"].clear()\n",
- " comparison_output.clear_output()\n",
- "\n",
- "clear_btn.on_click(_do_clear)\n",
- "\n",
- "\n",
- "def _do_export(btn):\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " export_status.value = \"Load a molecule first.\"\n",
- " return\n",
- " try:\n",
- " from quantui import PySCFCalculation\n",
- " from pathlib import Path\n",
- " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
- " fname = f\"{mol.get_formula()}_{method_dd.value}_{basis_dd.value}.py\"\n",
- " calc.generate_calculation_script(Path(fname))\n",
- " export_status.value = f\"Saved: {fname}\"\n",
- " except Exception as exc:\n",
- " export_status.value = f\"Error: {exc}\"\n",
- "\n",
- "export_btn.on_click(_do_export)\n",
- "\n",
- "\n",
- "def _on_method_help(btn):\n",
- " _show_help_topic(\"method\")\n",
- "\n",
- "def _on_basis_help(btn):\n",
- " _show_help_topic(\"basis_set\")\n",
- "\n",
- "method_help_btn.on_click(_on_method_help)\n",
- "basis_help_btn.on_click(_on_basis_help)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4",
- "metadata": {},
- "outputs": [],
- "source": [
- "# ── Preset Library ─────────────────────────────────────────────────────────\n",
- "_preset_opts = [\"(select a molecule)\"] + list(MOLECULE_LIBRARY.keys())\n",
- "preset_dd = widgets.Dropdown(\n",
- " options=_preset_opts, value=\"(select a molecule)\",\n",
- " description=\"Molecule:\", style={\"description_width\": \"90px\"},\n",
- " layout=widgets.Layout(width=\"320px\"),\n",
- ")\n",
- "\n",
- "def _load_preset(change):\n",
- " name = change[\"new\"]\n",
- " if name.startswith(\"(\"):\n",
- " return\n",
- " d = MOLECULE_LIBRARY[name]\n",
- " _set_molecule(\n",
- " Molecule(\n",
- " atoms=d[\"atoms\"], coordinates=d[\"coordinates\"],\n",
- " charge=d[\"charge\"], multiplicity=d[\"multiplicity\"],\n",
- " ),\n",
- " d[\"description\"],\n",
- " )\n",
- "\n",
- "preset_dd.observe(_load_preset, names=\"value\")\n",
- "\n",
- "# ── XYZ Input ──────────────────────────────────────────────────────────────\n",
- "xyz_area = widgets.Textarea(\n",
- " placeholder=(\n",
- " \"Paste XYZ coordinates (symbol x y z):\\n\"\n",
- " \"O 0.000 0.000 0.000\\n\"\n",
- " \"H 0.757 0.587 0.000\\n\"\n",
- " \"H -0.757 0.587 0.000\"\n",
- " ),\n",
- " layout=widgets.Layout(width=\"440px\", height=\"130px\"),\n",
- ")\n",
- "xyz_btn = widgets.Button(description=\"Load XYZ\", button_style=\"info\", icon=\"upload\")\n",
- "xyz_msg = widgets.Label()\n",
- "\n",
- "def _load_xyz(btn):\n",
- " try:\n",
- " mol = parse_xyz_input(xyz_area.value.strip())\n",
- " _set_molecule(mol, \"Loaded from XYZ input\")\n",
- " xyz_msg.value = \"\"\n",
- " except Exception as exc:\n",
- " xyz_msg.value = f\"Parse error: {exc}\"\n",
- "\n",
- "xyz_btn.on_click(_load_xyz)\n",
- "\n",
- "# ── PubChem Search ─────────────────────────────────────────────────────────\n",
- "pubchem_txt = widgets.Text(\n",
- " placeholder=\"name or SMILES (e.g. aspirin, caffeine, CC(=O)O)\",\n",
- " layout=widgets.Layout(width=\"380px\"),\n",
- ")\n",
- "pubchem_btn = widgets.Button(\n",
- " description=\"Search\", button_style=\"info\", icon=\"search\",\n",
- " disabled=not PUBCHEM_AVAILABLE,\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "pubchem_msg = widgets.Label(\n",
- " value=\"\" if PUBCHEM_AVAILABLE else \"PubChem unavailable — check internet connection\"\n",
- ")\n",
- "\n",
- "def _search_pubchem(btn):\n",
- " query = pubchem_txt.value.strip()\n",
- " if not query:\n",
- " pubchem_msg.value = \"Enter a molecule name or SMILES.\"\n",
- " return\n",
- " if student_friendly_fetch is None:\n",
- " pubchem_msg.value = \"PubChem module not available.\"\n",
- " return\n",
- " pubchem_msg.value = f'Searching for \"{query}\"...'\n",
- " pubchem_btn.disabled = True\n",
- "\n",
- " def _do():\n",
- " try:\n",
- " mol = student_friendly_fetch(query)\n",
- " _set_molecule(mol, f\"PubChem: {query}\")\n",
- " pubchem_msg.value = f\"Loaded {mol.get_formula()} from PubChem.\"\n",
- " except Exception as exc:\n",
- " pubchem_msg.value = f\"Not found: {exc}\"\n",
- " finally:\n",
- " pubchem_btn.disabled = False\n",
- "\n",
- " threading.Thread(target=_do, daemon=True).start()\n",
- "\n",
- "pubchem_btn.on_click(_search_pubchem)\n",
- "\n",
- "# ── Assemble input tab ─────────────────────────────────────────────────────\n",
- "_hint = ''\n",
- "tab_preset = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Choose from 20+ curated educational molecules.
\"),\n",
- " preset_dd,\n",
- "])\n",
- "tab_xyz = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Paste XYZ coordinates (element x y z, one atom per line).\"),\n",
- " xyz_area,\n",
- " widgets.HBox([xyz_btn, xyz_msg]),\n",
- "])\n",
- "tab_pubchem = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Search by name or SMILES. Requires internet connection.\"),\n",
- " widgets.HBox([pubchem_txt, pubchem_btn]),\n",
- " pubchem_msg,\n",
- "])\n",
- "\n",
- "input_tab = widgets.Tab(children=[tab_preset, tab_xyz, tab_pubchem])\n",
- "for _i, _t in enumerate([\"Preset Library\", \"XYZ Input\", \"PubChem Search\"]):\n",
- " input_tab.set_title(_i, _t)\n",
- "\n",
- "# ── Collapsible molecule input container ──────────────────────────────────\n",
- "mol_input_expanded = widgets.VBox([\n",
- " widgets.HTML('Molecule Input
'),\n",
- " input_tab,\n",
- "])\n",
- "\n",
- "# Change button — re-expands the input panel\n",
- "change_mol_btn = widgets.Button(\n",
- " description=\"Change\", button_style=\"\", icon=\"pencil\",\n",
- " layout=widgets.Layout(width=\"100px\", height=\"32px\"),\n",
- " tooltip=\"Re-expand the molecule input panel\",\n",
- ")\n",
- "\n",
- "# Collapsed view: compact summary pill + Change button\n",
- "mol_input_collapsed = widgets.HBox(\n",
- " [mol_summary_compact, change_mol_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"12px\", padding=\"6px 0\"),\n",
- ")\n",
- "\n",
- "# Container starts expanded; _set_molecule() collapses it after first load.\n",
- "mol_input_container = widgets.VBox(\n",
- " [mol_input_expanded, mol_info_html, viz_output],\n",
- " layout=widgets.Layout(margin=\"0 0 4px 0\"),\n",
- ")\n",
- "\n",
- "def _expand_mol_input(btn):\n",
- " mol_input_container.children = [mol_input_expanded, mol_info_html, viz_output]\n",
- "\n",
- "change_mol_btn.on_click(_expand_mol_input)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "5",
- "metadata": {},
- "outputs": [],
- "source": [
- "calc_setup_panel = widgets.VBox([\n",
- " widgets.HTML('Calculation Setup
'),\n",
- " widgets.HBox([\n",
- " widgets.VBox([\n",
- " widgets.HBox(\n",
- " [method_dd, method_help_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
- " ),\n",
- " widgets.HBox(\n",
- " [basis_dd, basis_help_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
- " ),\n",
- " ]),\n",
- " widgets.HTML(\" \"),\n",
- " widgets.VBox([charge_si, mult_si]),\n",
- " ]),\n",
- " calc_type_dd,\n",
- " calc_extra_opts,\n",
- " preopt_cb,\n",
- " notes_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "6",
- "metadata": {},
- "outputs": [],
- "source": [
- "run_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " 'Run Calculation
'\n",
- " 'PySCF runs in this kernel. '\n",
- " \"Output appears live below. Large molecules or high-accuracy basis sets may take \"\n",
- " \"several minutes on a laptop.
\"\n",
- " ),\n",
- " perf_estimate_html,\n",
- " widgets.HBox([run_btn, run_status]),\n",
- " widgets.HBox(\n",
- " [\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Calculation Output\"\n",
- " ),\n",
- " log_clear_btn,\n",
- " ],\n",
- " layout=widgets.Layout(align_items=\"center\", justify_content=\"space-between\",\n",
- " margin=\"10px 0 4px\", max_width=\"460px\"),\n",
- " ),\n",
- " run_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "7",
- "metadata": {},
- "outputs": [],
- "source": [
- "results_panel = widgets.VBox([\n",
- " widgets.HTML('Results
'),\n",
- " result_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8",
- "metadata": {},
- "outputs": [],
- "source": [
- "import json\n",
- "from pathlib import Path\n",
- "\n",
- "# ── Past Results browser ──────────────────────────────────────────────────────\n",
- "past_dd = widgets.Dropdown(\n",
- " description=\"Load:\",\n",
- " options=[(\"(no saved results)\", \"\")],\n",
- " style={\"description_width\": \"50px\"},\n",
- " layout=widgets.Layout(width=\"500px\"),\n",
- ")\n",
- "past_refresh_btn = widgets.Button(\n",
- " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- " tooltip=\"Rescan the results directory\",\n",
- ")\n",
- "copy_path_btn = widgets.Button(\n",
- " description=\"Copy path\", button_style=\"\", icon=\"clipboard\",\n",
- " layout=widgets.Layout(width=\"120px\"),\n",
- " tooltip=\"Copy the results directory path to clipboard\",\n",
- ")\n",
- "results_path_lbl = widgets.HTML()\n",
- "past_output = widgets.Output()\n",
- "\n",
- "\n",
- "def _get_results_dir() -> Path:\n",
- " from quantui.results_storage import _default_results_dir\n",
- " return _default_results_dir().resolve()\n",
- "\n",
- "\n",
- "def _format_past_result(data: dict) -> str:\n",
- " _conv = \"Yes\" if data.get(\"converged\") else \"No (treat results with caution)\"\n",
- " _cc = \"green\" if data.get(\"converged\") else \"#c00\"\n",
- " _gap = (\n",
- " f\"{data['homo_lumo_gap_ev']:.4f} eV\"\n",
- " if data.get(\"homo_lumo_gap_ev\") is not None else \"N/A\"\n",
- " )\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Total energy\",\n",
- " f\"{data['energy_hartree']:.8f} Ha ({data['energy_ev']:.4f} eV)\", \"#000\"),\n",
- " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
- " (\"SCF converged\", _conv, _cc),\n",
- " (\"SCF iterations\", str(data.get(\"n_iterations\", \"?\")), \"#000\"),\n",
- " ]\n",
- " )\n",
- " ts = data.get(\"timestamp\", \"\")\n",
- " return (\n",
- " f''\n",
- " f'
{data[\"formula\"]} — {data[\"method\"]}/{data[\"basis\"]}'\n",
- " f'
{ts}'\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _refresh_results_browser():\n",
- " '''Repopulate the past-results dropdown. Called on startup and after each run.'''\n",
- " try:\n",
- " from quantui import list_results, load_result\n",
- " except ImportError:\n",
- " return\n",
- " results_path_lbl.value = f'{_get_results_dir()}'\n",
- " dirs = list_results()\n",
- " if not dirs:\n",
- " past_dd.options = [(\"(no saved results)\", \"\")]\n",
- " return\n",
- " options = []\n",
- " for d in dirs:\n",
- " try:\n",
- " data = load_result(d)\n",
- " ts = data.get(\"timestamp\", d.name)\n",
- " label = f\"{ts} · {data['formula']} {data['method']}/{data['basis']}\"\n",
- " options.append((label, str(d)))\n",
- " except Exception:\n",
- " pass\n",
- " past_dd.options = options if options else [(\"(no saved results)\", \"\")]\n",
- "\n",
- "\n",
- "def _load_past_result(change):\n",
- " path_str = change[\"new\"]\n",
- " if not path_str:\n",
- " past_output.clear_output()\n",
- " return\n",
- " past_output.clear_output(wait=True)\n",
- " try:\n",
- " from quantui import load_result\n",
- " data = load_result(Path(path_str))\n",
- " past_output.append_display_data(HTML(_format_past_result(data)))\n",
- " except Exception as exc:\n",
- " past_output.append_stdout(f\"Could not load result: {exc}\\n\")\n",
- "\n",
- "\n",
- "def _copy_results_path(btn):\n",
- " '''Copy the results directory path to clipboard via browser JS.'''\n",
- " p = _get_results_dir()\n",
- " p.mkdir(parents=True, exist_ok=True)\n",
- " path_str = str(p).replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\")\n",
- " from IPython.display import Javascript\n",
- " display(Javascript(f\"navigator.clipboard.writeText(\\'{path_str}\\')\"))\n",
- " # Brief confirmation in the label\n",
- " import threading\n",
- " results_path_lbl.value = (\n",
- " f'Copied: {p}'\n",
- " )\n",
- " def _reset():\n",
- " import time; time.sleep(3)\n",
- " results_path_lbl.value = f'{p}'\n",
- " threading.Thread(target=_reset, daemon=True).start()\n",
- "\n",
- "\n",
- "view_log_btn = widgets.Button(\n",
- " description=\"View log\",\n",
- " button_style=\"\",\n",
- " icon=\"file-text-o\",\n",
- " layout=widgets.Layout(width=\"110px\"),\n",
- " tooltip=\"Open the full PySCF output log for this result in the Output tab\",\n",
- ")\n",
- "\n",
- "\n",
- "def _view_log(btn):\n",
- " path_str = past_dd.value\n",
- " if not path_str:\n",
- " return\n",
- " log_path = Path(path_str) / \"pyscf.log\"\n",
- " if log_path.exists():\n",
- " text = log_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n",
- " label = Path(path_str).name\n",
- " else:\n",
- " text = \"(No pyscf.log found for this result.)\"\n",
- " label = \"\"\n",
- " _update_log_panel(text, label)\n",
- " _goto_output_tab()\n",
- "\n",
- "\n",
- "past_dd.observe(_load_past_result, names=\"value\")\n",
- "past_refresh_btn.on_click(lambda _: _refresh_results_browser())\n",
- "copy_path_btn.on_click(_copy_results_path)\n",
- "view_log_btn.on_click(_view_log)\n",
- "\n",
- "# Populate on startup and make the real function visible to _do_run in Cell 3.\n",
- "_refresh_results_browser()\n",
- "\n",
- "# ── Performance stats widgets ───────────────────────────────────────────────────────────────────\n",
- "_perf_stats_html = widgets.HTML()\n",
- "_perf_events_html = widgets.HTML()\n",
- "\n",
- "_reset_btn = widgets.Button(\n",
- " description=\"Reset performance database\",\n",
- " button_style=\"danger\",\n",
- " icon=\"trash\",\n",
- " layout=widgets.Layout(width=\"230px\"),\n",
- ")\n",
- "_reset_confirm_html = widgets.HTML(\n",
- " ''\n",
- " \"Warning: This will permanently delete all performance records. \"\n",
- " \"Time estimates will reset to “no data”.\"\n",
- ")\n",
- "_reset_confirm_yes = widgets.Button(\n",
- " description=\"Yes, delete all records\",\n",
- " button_style=\"danger\",\n",
- " icon=\"check\",\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "_reset_confirm_no = widgets.Button(\n",
- " description=\"Cancel\",\n",
- " button_style=\"\",\n",
- " icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\"),\n",
- ")\n",
- "_reset_confirm_box = widgets.VBox(\n",
- " [\n",
- " _reset_confirm_html,\n",
- " widgets.HBox(\n",
- " [_reset_confirm_yes, _reset_confirm_no],\n",
- " layout=widgets.Layout(gap=\"8px\", margin=\"4px 0 0\"),\n",
- " ),\n",
- " ],\n",
- " layout=widgets.Layout(\n",
- " display=\"none\",\n",
- " border=\"1px solid #fca5a5\",\n",
- " padding=\"8px 10px\",\n",
- " margin=\"6px 0 0\",\n",
- " ),\n",
- ")\n",
- "\n",
- "\n",
- "def _build_perf_stats_html() -> str:\n",
- " from quantui.calc_log import get_perf_history\n",
- " records = get_perf_history()\n",
- " if not records:\n",
- " return (\n",
- " ''\n",
- " \"No performance data recorded yet.\"\n",
- " )\n",
- " groups: dict = {}\n",
- " for r in records:\n",
- " key = (r.get(\"method\", \"?\"), r.get(\"basis\", \"?\"))\n",
- " groups.setdefault(key, []).append(r)\n",
- " rows = \"\"\n",
- " for (meth, bas), recs in sorted(groups.items()):\n",
- " times = [r[\"elapsed_s\"] for r in recs if \"elapsed_s\" in r]\n",
- " n = len(recs)\n",
- " if times:\n",
- " avg = sum(times) / len(times)\n",
- " rows += (\n",
- " \"\"\n",
- " f'| {meth} | '\n",
- " f'{bas} | '\n",
- " f'{n} | '\n",
- " f'{avg:.1f} s | '\n",
- " f'{min(times):.1f} s | '\n",
- " f'{max(times):.1f} s | '\n",
- " \"
\"\n",
- " )\n",
- " header = (\n",
- " \"\"\n",
- " '| Method | '\n",
- " 'Basis | '\n",
- " 'Runs | '\n",
- " 'Avg | '\n",
- " 'Min | '\n",
- " 'Max | '\n",
- " \"
\"\n",
- " )\n",
- " return (\n",
- " ''\n",
- " f\"{header}{rows}
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _build_events_html() -> str:\n",
- " from quantui.calc_log import get_recent_events\n",
- " events = get_recent_events(20)\n",
- " if not events:\n",
- " return (\n",
- " ''\n",
- " \"No events recorded yet.\"\n",
- " )\n",
- " rows = \"\"\n",
- " for e in reversed(events):\n",
- " ts = e.get(\"timestamp\", \"\")[:19].replace(\"T\", \" \")\n",
- " evt = e.get(\"event\", \"\")\n",
- " msg = e.get(\"message\", \"\")\n",
- " rows += (\n",
- " \"\"\n",
- " f'| {ts} | '\n",
- " f'{evt} | '\n",
- " f'{msg} | '\n",
- " \"
\"\n",
- " )\n",
- " return (\n",
- " '\"\n",
- " )\n",
- "\n",
- "\n",
- "def _refresh_perf_stats():\n",
- " _perf_stats_html.value = _build_perf_stats_html()\n",
- " _perf_events_html.value = _build_events_html()\n",
- "\n",
- "\n",
- "def _on_reset_click(btn):\n",
- " _reset_confirm_box.layout.display = \"\"\n",
- "\n",
- "\n",
- "def _on_confirm_yes(btn):\n",
- " from quantui.calc_log import reset_perf_log\n",
- " reset_perf_log()\n",
- " _reset_confirm_box.layout.display = \"none\"\n",
- " _refresh_perf_stats()\n",
- "\n",
- "\n",
- "def _on_confirm_no(btn):\n",
- " _reset_confirm_box.layout.display = \"none\"\n",
- "\n",
- "\n",
- "_reset_btn.on_click(_on_reset_click)\n",
- "_reset_confirm_yes.on_click(_on_confirm_yes)\n",
- "_reset_confirm_no.on_click(_on_confirm_no)\n",
- "_refresh_perf_stats()\n",
- "\n",
- "_perf_stats_panel = widgets.VBox([\n",
- " _perf_stats_html,\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Recent events (last 20)
\"\n",
- " ),\n",
- " _perf_events_html,\n",
- " widgets.HBox(\n",
- " [_reset_btn],\n",
- " layout=widgets.Layout(margin=\"14px 0 4px\"),\n",
- " ),\n",
- " _reset_confirm_box,\n",
- "])\n",
- "\n",
- "_perf_accordion = widgets.Accordion(children=[_perf_stats_panel], selected_index=None)\n",
- "_perf_accordion.set_title(0, \"Performance stats\")\n",
- "\n",
- "# ── History panel (assembled widget for the History tab) ─────────────────\n",
- "history_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Calculations are saved automatically. Select one below to view its results.
\"\n",
- " ),\n",
- " widgets.HBox(\n",
- " [past_dd, past_refresh_btn, copy_path_btn, view_log_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"8px\"),\n",
- " ),\n",
- " results_path_lbl,\n",
- " past_output,\n",
- " _perf_accordion,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9",
- "metadata": {},
- "outputs": [],
- "source": [
- "from pathlib import Path\n",
- "\n",
- "# ── Compare tab widgets ────────────────────────────────────────────────────────\n",
- "compare_select = widgets.SelectMultiple(\n",
- " options=[(\"(no saved results)\", \"\")],\n",
- " rows=8,\n",
- " description=\"\",\n",
- " layout=widgets.Layout(width=\"100%\"),\n",
- ")\n",
- "compare_refresh_btn = widgets.Button(\n",
- " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "compare_btn = widgets.Button(\n",
- " description=\"Compare selected\", button_style=\"primary\", icon=\"bar-chart\",\n",
- " disabled=True,\n",
- " layout=widgets.Layout(width=\"180px\"),\n",
- ")\n",
- "compare_clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"warning\", icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\"),\n",
- ")\n",
- "compare_output = widgets.Output()\n",
- "\n",
- "\n",
- "def _populate_compare_list():\n",
- " '''Repopulate compare_select with all saved results.'''\n",
- " from quantui.results_storage import list_results, load_result\n",
- " dirs = list_results()\n",
- " if not dirs:\n",
- " compare_select.options = [(\"(no saved results)\", \"\")]\n",
- " compare_btn.disabled = True\n",
- " return\n",
- " options = []\n",
- " for d in dirs:\n",
- " try:\n",
- " data = load_result(d)\n",
- " ts = data.get(\"timestamp\", d.name[:19])\n",
- " label = f\"{ts} {data['formula']} {data['method']}/{data['basis']}\"\n",
- " options.append((label, str(d)))\n",
- " except Exception:\n",
- " options.append((d.name, str(d)))\n",
- " compare_select.options = options\n",
- " compare_btn.disabled = False\n",
- "\n",
- "\n",
- "def _on_compare_refresh(btn):\n",
- " _populate_compare_list()\n",
- "\n",
- "\n",
- "def _on_compare(btn):\n",
- " selected = compare_select.value # tuple of selected path strings\n",
- " if not selected or selected == (\"\",):\n",
- " return\n",
- " compare_output.clear_output(wait=True)\n",
- " from quantui.results_storage import load_result\n",
- " from quantui import summary_from_saved_result, comparison_table_html, plot_comparison\n",
- " summaries = []\n",
- " for path_str in selected:\n",
- " if not path_str:\n",
- " continue\n",
- " try:\n",
- " data = load_result(Path(path_str))\n",
- " summaries.append(summary_from_saved_result(data))\n",
- " except Exception as exc:\n",
- " with compare_output:\n",
- " display(HTML(f'Error loading result: {exc}
'))\n",
- " if not summaries:\n",
- " return\n",
- " with compare_output:\n",
- " display(HTML(comparison_table_html(summaries)))\n",
- " if len(summaries) > 1:\n",
- " try:\n",
- " import matplotlib.pyplot as plt\n",
- " fig = plot_comparison(summaries)\n",
- " display(fig)\n",
- " plt.close(fig)\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "\n",
- "def _on_compare_clear(btn):\n",
- " compare_select.value = ()\n",
- " compare_output.clear_output()\n",
- "\n",
- "\n",
- "compare_refresh_btn.on_click(_on_compare_refresh)\n",
- "compare_btn.on_click(_on_compare)\n",
- "compare_clear_btn.on_click(_on_compare_clear)\n",
- "\n",
- "# Populate on load; expose to Cell-3 placeholder so _do_run refreshes it.\n",
- "_populate_compare_list()\n",
- "globals()[\"_populate_compare_list\"] = _populate_compare_list\n",
- "\n",
- "# ── Compare panel (assembled widget for the Compare tab) ──────────────────────\n",
- "compare_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " 'Compare Calculations
'\n",
- " ''\n",
- " \"Select two or more saved calculations to compare side-by-side. \"\n",
- " \"Hold Ctrl (or ⌘) to select multiple entries.
\"\n",
- " ),\n",
- " widgets.HBox([compare_refresh_btn]),\n",
- " compare_select,\n",
- " widgets.HBox(\n",
- " [compare_btn, compare_clear_btn],\n",
- " layout=widgets.Layout(gap=\"8px\", margin=\"6px 0\"),\n",
- " ),\n",
- " compare_output,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Advanced accordion (Export only) ──────────────────────────────────────────\n",
- "_export_content = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Download a self-contained PySCF script you can study or run outside the notebook.
\"\n",
- " ),\n",
- " widgets.HBox([export_btn, export_status]),\n",
- "])\n",
- "advanced_accordion = widgets.Accordion(children=[_export_content])\n",
- "advanced_accordion.set_title(0, \"Export Script\")\n",
- "advanced_accordion.selected_index = None # collapsed by default\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "10",
- "metadata": {},
- "outputs": [],
- "source": [
- "from quantui.help_content import HELP_TOPICS\n",
- "\n",
- "# ── Help tab content ──────────────────────────────────────────────────────────\n",
- "_help_keys = list(HELP_TOPICS.keys())\n",
- "_help_labels = [HELP_TOPICS[k][\"title\"] for k in _help_keys]\n",
- "\n",
- "help_topic_dd = widgets.Dropdown(\n",
- " options=list(zip(_help_labels, _help_keys)),\n",
- " description=\"Topic:\",\n",
- " style={\"description_width\": \"60px\"},\n",
- " layout=widgets.Layout(width=\"460px\"),\n",
- ")\n",
- "help_content_html = widgets.HTML()\n",
- "\n",
- "def _render_help_topic(change=None):\n",
- " key = help_topic_dd.value\n",
- " if key and key in HELP_TOPICS:\n",
- " entry = HELP_TOPICS[key]\n",
- " help_content_html.value = (\n",
- " f''\n",
- " f'
'\n",
- " f'{entry[\"title\"]}
'\n",
- " f'
'\n",
- " f'{entry[\"body\"]}
'\n",
- " f'
'\n",
- " )\n",
- "\n",
- "help_topic_dd.observe(_render_help_topic, names=\"value\")\n",
- "_render_help_topic() # render first topic on load\n",
- "\n",
- "help_tab_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Browse help topics below. Click ? next to the Method or Basis Set \"\n",
- " \"dropdown in the Calculate tab to jump directly to a relevant topic.
\"\n",
- " ),\n",
- " help_topic_dd,\n",
- " help_content_html,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Assemble root tab ─────────────────────────────────────────────────────────\n",
- "_calculate_content = widgets.VBox([\n",
- " step_progress.widget,\n",
- " mol_input_container,\n",
- " calc_setup_panel,\n",
- " run_panel,\n",
- " results_panel,\n",
- " advanced_accordion,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Output log tab ────────────────────────────────────────────────────────────────\n",
- "_log_output_html = widgets.HTML(\n",
- " ''\n",
- " \"No log yet — run a calculation first, or use \"\n",
- " \"View log in the History tab.\"\n",
- ")\n",
- "_log_source_lbl = widgets.HTML()\n",
- "_log_clear_btn = widgets.Button(\n",
- " description=\"Clear\",\n",
- " button_style=\"\",\n",
- " icon=\"times\",\n",
- " layout=widgets.Layout(width=\"80px\"),\n",
- ")\n",
- "\n",
- "\n",
- "def _render_log(text: str, source_label: str = \"\") -> None:\n",
- " # Render text as syntax-highlighted HTML in the log panel.\n",
- " import html as _html_mod\n",
- " lines = text.splitlines()\n",
- " rows = []\n",
- " for line in lines:\n",
- " esc = _html_mod.escape(line)\n",
- " if \"converged SCF energy\" in line or \"SCF converged\" in line:\n",
- " style = \"color:#16a34a;font-weight:600\"\n",
- " elif \"cycle=\" in line and \"E=\" in line:\n",
- " style = \"color:#475569\"\n",
- " elif \"HOMO\" in line or \"LUMO\" in line:\n",
- " style = \"color:#2563eb\"\n",
- " elif \"Warning\" in line or \"warning\" in line:\n",
- " style = \"color:#d97706\"\n",
- " elif \"Error\" in line or \"error\" in line or \"failed\" in line:\n",
- " style = \"color:#dc2626\"\n",
- " else:\n",
- " style = \"color:#1e293b\"\n",
- " rows.append(f'{esc}
')\n",
- " _log_output_html.value = (\n",
- " ''\n",
- " + \"\".join(rows)\n",
- " + \"
\"\n",
- " )\n",
- " _log_source_lbl.value = (\n",
- " f'Source: {source_label}'\n",
- " if source_label\n",
- " else \"\"\n",
- " )\n",
- "\n",
- "\n",
- "def _update_log_panel(log_text: str, label: str = \"\") -> None:\n",
- " _render_log(log_text, label)\n",
- "\n",
- "\n",
- "def _goto_output_tab() -> None:\n",
- " root_tab.selected_index = 3\n",
- "\n",
- "\n",
- "def _clear_log(_):\n",
- " _log_output_html.value = (\n",
- " 'Log cleared.'\n",
- " )\n",
- " _log_source_lbl.value = \"\"\n",
- "\n",
- "\n",
- "_log_clear_btn.on_click(_clear_log)\n",
- "\n",
- "log_tab_panel = widgets.VBox(\n",
- " [\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Full PySCF output for the most recent calculation. \"\n",
- " \"Use View log in the History tab to load a saved result's log.
\"\n",
- " ),\n",
- " widgets.HBox(\n",
- " [_log_clear_btn],\n",
- " layout=widgets.Layout(margin=\"0 0 8px\"),\n",
- " ),\n",
- " _log_source_lbl,\n",
- " _log_output_html,\n",
- " ],\n",
- " layout=widgets.Layout(padding=\"8px 0\"),\n",
- ")\n",
- "\n",
- "# Overwrite Cell-3 placeholders so later widgets can be reached.\n",
- "globals()[\"_update_log_panel\"] = _update_log_panel\n",
- "globals()[\"_goto_output_tab\"] = _goto_output_tab\n",
- "\n",
- "root_tab = widgets.Tab(children=[\n",
- " _calculate_content,\n",
- " history_panel,\n",
- " compare_panel,\n",
- " log_tab_panel,\n",
- " help_tab_panel,\n",
- "])\n",
- "root_tab.set_title(0, \"Calculate\")\n",
- "root_tab.set_title(1, \"History\")\n",
- "root_tab.set_title(2, \"Compare\")\n",
- "root_tab.set_title(3, \"Output\")\n",
- "root_tab.set_title(4, \"Help\")\n",
- "\n",
- "# ── \"?\" button callbacks: jump to Help tab with specific topic ────────────────\n",
- "def _show_help_topic(topic: str) -> None:\n",
- " if topic in HELP_TOPICS:\n",
- " help_topic_dd.value = topic\n",
- " root_tab.selected_index = 4\n",
- "\n",
- "# Overwrite Cell-3 placeholder so \"?\" buttons find the real function.\n",
- "globals()[\"_show_help_topic\"] = _show_help_topic\n",
- "\n",
- "# ── Single root display call ──────────────────────────────────────────────────\n",
- "display(root_tab)\n"
+ "from quantui.app import QuantUIApp\n",
+ "QuantUIApp().display()"
]
}
],
diff --git a/notebooks/molecule_computations.pre-fr012.ipynb b/notebooks/molecule_computations.pre-fr012.ipynb
new file mode 100644
index 0000000..6140e0d
--- /dev/null
+++ b/notebooks/molecule_computations.pre-fr012.ipynb
@@ -0,0 +1,1791 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# QuantUI-local — Quantum Chemistry Calculator\n",
+ "\n",
+ "Run PySCF quantum chemistry calculations directly in this notebook.\n",
+ "\n",
+ "**How to use:**\n",
+ "1. Select or enter a molecule in **Molecule Input**\n",
+ "2. Choose a method and basis set in **Calculation Setup**\n",
+ "3. Click **Run Calculation** — results appear below\n",
+ "4. Optionally add results to **Compare** or **Export** a standalone script\n",
+ "\n",
+ "> **Platform note:** PySCF requires Linux, macOS, or WSL. \\\n",
+ "> Windows users: `apptainer run quantui-local.sif`\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1",
+ "metadata": {
+ "tags": [
+ "skip-execution",
+ "remove-input"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "# Environment check — verifies correct conda environment.\n",
+ "# Tagged skip-execution and remove-input so it is hidden in Voilà.\n",
+ "import sys as _sys\n",
+ "_env = _sys.prefix\n",
+ "if \"quantui\" not in _env.lower():\n",
+ " print(f\"Warning: active environment may not be quantui-local\")\n",
+ " print(f\"Active: {_env}\")\n",
+ " print(\"Run: conda activate quantui-local\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import threading\n",
+ "import ipywidgets as widgets\n",
+ "from IPython.display import display, HTML\n",
+ "\n",
+ "import quantui\n",
+ "from quantui import (\n",
+ " Molecule, parse_xyz_input,\n",
+ " MOLECULE_LIBRARY, SUPPORTED_METHODS, SUPPORTED_BASIS_SETS,\n",
+ " DEFAULT_METHOD, DEFAULT_BASIS, DEFAULT_CHARGE, DEFAULT_MULTIPLICITY,\n",
+ " session_can_handle, get_session_resources,\n",
+ " PUBCHEM_AVAILABLE, VISUALIZATION_AVAILABLE, ASE_AVAILABLE,\n",
+ " QUICK_START_TEMPLATES,\n",
+ ")\n",
+ "\n",
+ "# Optional — degrade gracefully if unavailable\n",
+ "try:\n",
+ " from quantui import run_in_session, SessionResult\n",
+ " PYSCF_AVAILABLE = True\n",
+ "except (ImportError, AttributeError):\n",
+ " PYSCF_AVAILABLE = False\n",
+ "\n",
+ "try:\n",
+ " from quantui import student_friendly_fetch\n",
+ "except (ImportError, AttributeError):\n",
+ " student_friendly_fetch = None\n",
+ "\n",
+ "try:\n",
+ " from quantui import display_molecule\n",
+ "except (ImportError, AttributeError):\n",
+ " display_molecule = None\n",
+ "\n",
+ "try:\n",
+ " from quantui import preoptimize\n",
+ " PREOPT_AVAILABLE = True\n",
+ "except (ImportError, AttributeError):\n",
+ " PREOPT_AVAILABLE = False\n",
+ "\n",
+ "# Mutable session state shared across all callbacks\n",
+ "_state = {\"molecule\": None, \"last_result\": None, \"results\": []}\n",
+ "\n",
+ "# ── Global app styles ─────────────────────────────────────────────────────────\n",
+ "# Permanent — not toggled by dark mode (dark mode uses filter inversion on top).\n",
+ "display(HTML(\"\"\"\"\"\"))\n",
+ "\n",
+ "# ── Dark mode toggle ─────────────────────────────────────────────────────────\n",
+ "# Uses CSS filter invert+hue-rotate on the html element so it works with all\n",
+ "# inline-styled elements. canvas/img/iframe are re-inverted to keep their\n",
+ "# original appearance (e.g. the py3Dmol 3D viewer).\n",
+ "# Map theme name → hue-rotate angle. Light uses no filter.\n",
+ "_THEME_HUE = {\"Dark\": 180, \"Dark Blue\": 200, \"Dark Maroon\": 160}\n",
+ "\n",
+ "\n",
+ "def _theme_css(theme: str) -> str:\n",
+ " \"\"\"Return the CSS filter block for *theme*, or '' for Light.\"\"\"\n",
+ " if theme not in _THEME_HUE:\n",
+ " return \"\"\n",
+ " deg = _THEME_HUE[theme]\n",
+ " return (\n",
+ " \"\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "_theme_style = widgets.Output(\n",
+ " layout=widgets.Layout(height=\"0px\", overflow=\"hidden\", margin=\"0\", padding=\"0\")\n",
+ ")\n",
+ "\n",
+ "theme_btn = widgets.ToggleButtons(\n",
+ " options=[\"Light\", \"Dark\", \"Dark Blue\", \"Dark Maroon\"],\n",
+ " value=\"Dark\",\n",
+ " description=\"Theme:\",\n",
+ " style={\n",
+ " \"description_width\": \"48px\",\n",
+ " \"button_width\": \"90px\",\n",
+ " },\n",
+ " layout=widgets.Layout(margin=\"0\"),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def _toggle_theme(change):\n",
+ " _theme_style.clear_output()\n",
+ " css = _theme_css(change[\"new\"])\n",
+ " if css:\n",
+ " with _theme_style:\n",
+ " display(HTML(css))\n",
+ "\n",
+ "\n",
+ "theme_btn.observe(_toggle_theme, names=\"value\")\n",
+ "\n",
+ "# Apply Dark theme on startup\n",
+ "with _theme_style:\n",
+ " display(HTML(_theme_css(\"Dark\")))\n",
+ "\n",
+ "display(widgets.HBox(\n",
+ " [theme_btn],\n",
+ " layout=widgets.Layout(justify_content=\"flex-end\", margin=\"0 0 4px\"),\n",
+ "))\n",
+ "display(_theme_style)\n",
+ "\n",
+ "# ── Status panel ────────────────────────────────────────────────────────────\n",
+ "_cores, _mem_gb = get_session_resources()\n",
+ "_mem = f\"{_mem_gb} GB\" if _mem_gb is not None else \"unknown\"\n",
+ "\n",
+ "\n",
+ "def _ok(flag, extra=\"\"):\n",
+ " tick = '✓'\n",
+ " cross = '✗'\n",
+ " return (tick if flag else cross) + (\" \" + extra if extra else \"\")\n",
+ "\n",
+ "\n",
+ "_items = [\n",
+ " (\"PySCF (calculations)\", _ok(PYSCF_AVAILABLE,\n",
+ " \"\" if PYSCF_AVAILABLE else \"— Linux / macOS / WSL required\")),\n",
+ " (\"ASE (structure I/O, opt.)\", _ok(ASE_AVAILABLE)),\n",
+ " (\"PubChem search\", _ok(PUBCHEM_AVAILABLE)),\n",
+ " (\"3D viewer (py3Dmol)\", _ok(VISUALIZATION_AVAILABLE)),\n",
+ " (\"CPU cores / Memory\", f\"{_cores} cores / {_mem}\"),\n",
+ "]\n",
+ "_rows = \"\".join(\n",
+ " f'| {k} | '\n",
+ " f'{v} |
'\n",
+ " for k, v in _items\n",
+ ")\n",
+ "display(HTML(\n",
+ " f''\n",
+ " f'
'\n",
+ " f\"QuantUI-local {quantui.__version__}\"\n",
+ " f'
'\n",
+ " f\"
\"\n",
+ "))\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import io\n",
+ "import re\n",
+ "import time\n",
+ "\n",
+ "from quantui.progress import StepProgress\n",
+ "import quantui.calc_log as _calc_log\n",
+ "\n",
+ "# ── Shared output widgets ────────────────────────────────────────────────────\n",
+ "mol_info_html = widgets.HTML(\n",
+ " value='No molecule loaded yet.'\n",
+ ")\n",
+ "mol_summary_compact = widgets.HTML(value=\"\")\n",
+ "viz_output = widgets.Output(layout=widgets.Layout(min_height=\"50px\"))\n",
+ "run_output = widgets.Output(\n",
+ " layout=widgets.Layout(\n",
+ " border=\"1px solid #c0ccd8\", min_height=\"80px\", max_height=\"400px\",\n",
+ " padding=\"8px\", overflow_y=\"auto\",\n",
+ " )\n",
+ ")\n",
+ "with run_output:\n",
+ " display(HTML(\n",
+ " ''\n",
+ " \"No calculation run yet. PySCF output and any errors will appear here.\"\n",
+ " \"
\"\n",
+ " ))\n",
+ "result_output = widgets.Output()\n",
+ "comparison_output = widgets.Output()\n",
+ "notes_output = widgets.Output()\n",
+ "perf_estimate_html = widgets.HTML()\n",
+ "\n",
+ "# ── Step indicator ────────────────────────────────────────────────────────────\n",
+ "step_progress = StepProgress([\"Choose molecule\", \"Set method\", \"Run\", \"Results\"])\n",
+ "step_progress.start(0)\n",
+ "\n",
+ "# ── Calculation setup (defined here so _set_molecule can update them) ────────\n",
+ "method_dd = widgets.Dropdown(\n",
+ " options=SUPPORTED_METHODS, value=DEFAULT_METHOD,\n",
+ " description=\"Method:\", style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"260px\"),\n",
+ ")\n",
+ "basis_dd = widgets.Dropdown(\n",
+ " options=SUPPORTED_BASIS_SETS, value=DEFAULT_BASIS,\n",
+ " description=\"Basis Set:\", style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"260px\"),\n",
+ ")\n",
+ "charge_si = widgets.BoundedIntText(\n",
+ " value=DEFAULT_CHARGE, min=-10, max=10,\n",
+ " description=\"Charge:\", style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"190px\"),\n",
+ ")\n",
+ "mult_si = widgets.BoundedIntText(\n",
+ " value=DEFAULT_MULTIPLICITY, min=1, max=10,\n",
+ " description=\"Multiplicity:\", style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"190px\"),\n",
+ ")\n",
+ "preopt_cb = widgets.Checkbox(\n",
+ " value=False,\n",
+ " description=\"Pre-optimize geometry (fast LJ force-field)\",\n",
+ " disabled=not PREOPT_AVAILABLE,\n",
+ " layout=widgets.Layout(width=\"400px\"),\n",
+ ")\n",
+ "\n",
+ "# ── Calculation type + extra options ──────────────────────────────────────────\n",
+ "calc_type_dd = widgets.Dropdown(\n",
+ " options=[\"Single Point\", \"Geometry Opt\", \"Frequency\", \"UV-Vis (TD-DFT)\"],\n",
+ " value=\"Single Point\",\n",
+ " description=\"Calc. Type:\",\n",
+ " style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"310px\"),\n",
+ ")\n",
+ "fmax_fi = widgets.BoundedFloatText(\n",
+ " value=0.05, min=0.001, max=1.0, step=0.005,\n",
+ " description=\"Force thr. (eV/Å):\",\n",
+ " style={\"description_width\": \"130px\"},\n",
+ " layout=widgets.Layout(width=\"270px\"),\n",
+ ")\n",
+ "max_steps_si = widgets.BoundedIntText(\n",
+ " value=200, min=10, max=1000,\n",
+ " description=\"Max steps:\",\n",
+ " style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"200px\"),\n",
+ ")\n",
+ "nstates_si = widgets.BoundedIntText(\n",
+ " value=10, min=1, max=50,\n",
+ " description=\"# states:\",\n",
+ " style={\"description_width\": \"100px\"},\n",
+ " layout=widgets.Layout(width=\"180px\"),\n",
+ ")\n",
+ "calc_extra_opts = widgets.VBox([])\n",
+ "\n",
+ "\n",
+ "def _on_calc_type_change(change):\n",
+ " ct = change[\"new\"]\n",
+ " if ct == \"Geometry Opt\":\n",
+ " calc_extra_opts.children = [\n",
+ " widgets.HBox(\n",
+ " [fmax_fi, max_steps_si],\n",
+ " layout=widgets.Layout(gap=\"8px\"),\n",
+ " ),\n",
+ " ]\n",
+ " elif ct == \"UV-Vis (TD-DFT)\":\n",
+ " calc_extra_opts.children = [\n",
+ " nstates_si,\n",
+ " widgets.HTML(\n",
+ " '⚠ Requires a DFT '\n",
+ " \"functional (e.g. B3LYP, PBE0). RHF/UHF will run TDHF (CIS) \"\n",
+ " \"instead.\"\n",
+ " ),\n",
+ " ]\n",
+ " else:\n",
+ " calc_extra_opts.children = []\n",
+ "\n",
+ "\n",
+ "calc_type_dd.observe(_on_calc_type_change, names=\"value\")\n",
+ "\n",
+ "# ── Context-help buttons (next to Method and Basis dropdowns) ────────────────\n",
+ "method_help_btn = widgets.Button(\n",
+ " description=\"?\", button_style=\"\",\n",
+ " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
+ " tooltip=\"RHF vs UHF — opens Help tab\",\n",
+ ")\n",
+ "basis_help_btn = widgets.Button(\n",
+ " description=\"?\", button_style=\"\",\n",
+ " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
+ " tooltip=\"Choosing a basis set — opens Help tab\",\n",
+ ")\n",
+ "\n",
+ "# ── Run widgets ──────────────────────────────────────────────────────────────\n",
+ "run_btn = widgets.Button(\n",
+ " description=\"Run Calculation\", button_style=\"success\", icon=\"play\",\n",
+ " disabled=True, layout=widgets.Layout(width=\"200px\", height=\"36px\"),\n",
+ ")\n",
+ "run_status = widgets.Label()\n",
+ "\n",
+ "# ── Log clear button ─────────────────────────────────────────────────────────\n",
+ "log_clear_btn = widgets.Button(\n",
+ " description=\"Clear\", button_style=\"\", icon=\"times\",\n",
+ " layout=widgets.Layout(width=\"90px\", height=\"26px\"),\n",
+ " tooltip=\"Clear calculation output\",\n",
+ ")\n",
+ "\n",
+ "def _clear_log(btn):\n",
+ " run_output.clear_output()\n",
+ "\n",
+ "log_clear_btn.on_click(_clear_log)\n",
+ "\n",
+ "# ── Comparison / export widgets ───────────────────────────────────────────────\n",
+ "accumulate_btn = widgets.Button(\n",
+ " description=\"Add to Comparison\", button_style=\"info\", icon=\"plus\",\n",
+ " disabled=True, layout=widgets.Layout(width=\"190px\"),\n",
+ ")\n",
+ "clear_btn = widgets.Button(\n",
+ " description=\"Clear\", button_style=\"warning\", icon=\"trash\",\n",
+ " layout=widgets.Layout(width=\"100px\"),\n",
+ ")\n",
+ "export_btn = widgets.Button(\n",
+ " description=\"Export Script\", button_style=\"\", icon=\"download\",\n",
+ " disabled=True, layout=widgets.Layout(width=\"160px\"),\n",
+ ")\n",
+ "export_status = widgets.Label()\n",
+ "\n",
+ "\n",
+ "# ── Thread-safe log capture ───────────────────────────────────────────────────\n",
+ "_RE_CYCLE = re.compile(\n",
+ " r\"cycle=\\s*(\\d+)\\s+E=\\s*([\\-\\d\\.]+)\\s+delta_E=\\s*([\\-\\d\\.Ee+\\-]+)\"\n",
+ ")\n",
+ "_RE_CONV = re.compile(r\"converged SCF energy\\s*=\\s*([\\-\\d\\.]+)\")\n",
+ "\n",
+ "\n",
+ "class _LogCapture:\n",
+ " '''Write PySCF output to an Output widget and capture it to a buffer.'''\n",
+ " def __init__(self, output_widget):\n",
+ " self._w = output_widget\n",
+ " self._buf = io.StringIO()\n",
+ " self._line_buf = \"\"\n",
+ "\n",
+ " def write(self, text: str) -> None:\n",
+ " if not text:\n",
+ " return\n",
+ " self._w.append_stdout(text)\n",
+ " self._buf.write(text)\n",
+ " # Scan complete lines for SCF progress and update the status label.\n",
+ " self._line_buf += text\n",
+ " while \"\\n\" in self._line_buf:\n",
+ " line, self._line_buf = self._line_buf.split(\"\\n\", 1)\n",
+ " m = _RE_CYCLE.search(line)\n",
+ " if m:\n",
+ " n, delta = m.group(1), m.group(3)\n",
+ " try:\n",
+ " run_status.value = f\"SCF cycle {n} · ΔE = {float(delta):.4g} Ha\"\n",
+ " except Exception:\n",
+ " run_status.value = f\"SCF cycle {n}\"\n",
+ " continue\n",
+ " m = _RE_CONV.search(line)\n",
+ " if m:\n",
+ " run_status.value = \"SCF converged ✓\"\n",
+ "\n",
+ " def flush(self) -> None:\n",
+ " pass\n",
+ "\n",
+ " def getvalue(self) -> str:\n",
+ " return self._buf.getvalue()\n",
+ "\n",
+ "\n",
+ "# Placeholders — overwritten by later cells once they execute.\n",
+ "_refresh_results_browser = lambda: None # noqa: E731\n",
+ "_show_help_topic = lambda topic: None # noqa: E731\n",
+ "_populate_compare_list = lambda: None # noqa: E731\n",
+ "_update_log_panel = lambda log_text, label=\"\": None # noqa: E731\n",
+ "_goto_output_tab = lambda: None # noqa: E731\n",
+ "\n",
+ "\n",
+ "# ── Callbacks ─────────────────────────────────────────────────────────────────\n",
+ "\n",
+ "def _set_molecule(mol, label=\"\"):\n",
+ " '''Update shared state and refresh dependent widgets.'''\n",
+ " _state[\"molecule\"] = mol\n",
+ " run_btn.disabled = False\n",
+ " export_btn.disabled = False\n",
+ "\n",
+ " try:\n",
+ " n_e = mol.get_electron_count()\n",
+ " e_str = f\"{n_e} electrons\"\n",
+ " except Exception:\n",
+ " e_str = \"\"\n",
+ "\n",
+ " _lbl = f'
{label}' if label else \"\"\n",
+ " _summary = (\n",
+ " f'{mol.get_formula()}'\n",
+ " f' '\n",
+ " f\"{len(mol.atoms)} atoms\"\n",
+ " + (f\" • {e_str}\" if e_str else \"\")\n",
+ " + f\" • charge {mol.charge} • mult {mol.multiplicity}\"\n",
+ " + f\"{_lbl}\"\n",
+ " )\n",
+ " mol_info_html.value = _summary\n",
+ " mol_summary_compact.value = (\n",
+ " f''\n",
+ " f\"{_summary}
\"\n",
+ " )\n",
+ "\n",
+ " charge_si.value = mol.charge\n",
+ " mult_si.value = mol.multiplicity\n",
+ " if mol.multiplicity > 1 and method_dd.value == \"RHF\":\n",
+ " method_dd.value = \"UHF\"\n",
+ "\n",
+ " viz_output.clear_output(wait=True)\n",
+ " if display_molecule is not None:\n",
+ " with viz_output:\n",
+ " display_molecule(mol)\n",
+ "\n",
+ " _update_notes()\n",
+ "\n",
+ " # Advance step indicator — but not during a running calculation (e.g. preopt)\n",
+ " if step_progress._states[2] != \"active\":\n",
+ " if step_progress._states[2] in (\"done\", \"fail\"):\n",
+ " # Fresh molecule after a completed run — reset indicator\n",
+ " step_progress.reset()\n",
+ " step_progress.complete(0)\n",
+ " step_progress.start(1)\n",
+ "\n",
+ " # Update time estimate for the newly loaded molecule\n",
+ " _update_estimate()\n",
+ "\n",
+ " # Collapse the molecule input panel to the compact summary + 3D viewer\n",
+ " mol_input_container.children = [mol_input_collapsed, viz_output]\n",
+ "\n",
+ "\n",
+ "def _format_result(r):\n",
+ " _conv = \"Yes\" if r.converged else \"No (treat results with caution)\"\n",
+ " _cc = \"green\" if r.converged else \"#c00\"\n",
+ " _gap = (\n",
+ " f\"{r.homo_lumo_gap_ev:.4f} eV\"\n",
+ " if r.homo_lumo_gap_ev is not None else \"N/A\"\n",
+ " )\n",
+ " _rows = \"\".join(\n",
+ " f''\n",
+ " f'| {k} | '\n",
+ " f'{v} | '\n",
+ " f\"
\"\n",
+ " for k, v, vc in [\n",
+ " (\"Total energy\",\n",
+ " f\"{r.energy_hartree:.8f} Ha ({r.energy_ev:.4f} eV)\", \"#000\"),\n",
+ " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
+ " (\"SCF converged\", _conv, _cc),\n",
+ " (\"SCF iterations\", str(r.n_iterations), \"#000\"),\n",
+ " ]\n",
+ " )\n",
+ " return (\n",
+ " f''\n",
+ " f\"
{r.formula} — {r.method}/{r.basis}\"\n",
+ " f'
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _format_opt_result(r):\n",
+ " '''Format an OptimizationResult as an HTML result card.'''\n",
+ " _conv = \"Yes\" if r.converged else \"No (max steps reached)\"\n",
+ " _cc = \"green\" if r.converged else \"#c00\"\n",
+ " _rows = \"\".join(\n",
+ " f''\n",
+ " f'| {k} | '\n",
+ " f'{v} | '\n",
+ " f\"
\"\n",
+ " for k, v, vc in [\n",
+ " (\"Final energy\", f\"{r.energy_hartree:.8f} Ha\", \"#000\"),\n",
+ " (\"Energy change\", f\"{r.energy_change_hartree:+.6f} Ha\", \"#000\"),\n",
+ " (\"Opt converged\", _conv, _cc),\n",
+ " (\"Steps taken\", str(r.n_steps), \"#000\"),\n",
+ " (\"Geometry RMSD\", f\"{r.rmsd_angstrom:.4f} Å\", \"#000\"),\n",
+ " ]\n",
+ " )\n",
+ " return (\n",
+ " f''\n",
+ " f\"
Geometry Optimisation — {r.formula} ({r.method}/{r.basis})\"\n",
+ " f'
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _format_freq_result(r):\n",
+ " '''Format a FreqResult as an HTML result card.'''\n",
+ " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
+ " _cc = \"green\" if r.converged else \"#c00\"\n",
+ " n_real = r.n_real_modes()\n",
+ " n_imag = r.n_imaginary_modes()\n",
+ " real_freqs = sorted(f for f in r.frequencies_cm1 if f > 0)[:6]\n",
+ " freq_str = \" \".join(f\"{f:.1f}\" for f in real_freqs)\n",
+ " if len([f for f in r.frequencies_cm1 if f > 0]) > 6:\n",
+ " freq_str += \" …\"\n",
+ " imag_note = \"\"\n",
+ " if n_imag > 0:\n",
+ " imag_note = (\n",
+ " f'| Imaginary modes | '\n",
+ " f'{n_imag} — geometry may not be a minimum |
'\n",
+ " )\n",
+ " _rows = (\n",
+ " f'| SCF energy | '\n",
+ " f'{r.energy_hartree:.8f} Ha |
'\n",
+ " f'| SCF converged | '\n",
+ " f'{_conv} |
'\n",
+ " f'| Real modes | '\n",
+ " f'{n_real} |
'\n",
+ " + imag_note\n",
+ " + (f'| Frequencies (cm⁻¹) | '\n",
+ " f'{freq_str or \"none\"} |
'\n",
+ " if real_freqs else \"\")\n",
+ " + f'| ZPVE | '\n",
+ " f'{r.zpve_hartree:.6f} Ha '\n",
+ " f'({r.zpve_hartree * 27.211386245988:.4f} eV) |
'\n",
+ " )\n",
+ " return (\n",
+ " f''\n",
+ " f\"
Frequency Analysis — {r.formula} ({r.method}/{r.basis})\"\n",
+ " f'
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _format_tddft_result(r):\n",
+ " '''Format a TDDFTResult as an HTML result card with excitation table.'''\n",
+ " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
+ " _cc = \"green\" if r.converged else \"#c00\"\n",
+ " header_rows = (\n",
+ " f'| Ground-state energy | '\n",
+ " f'{r.energy_hartree:.8f} Ha |
'\n",
+ " f'| SCF converged | '\n",
+ " f'{_conv} |
'\n",
+ " f'| States computed | '\n",
+ " f'{len(r.excitation_energies_ev)} |
'\n",
+ " )\n",
+ " exc_table = \"\"\n",
+ " if r.excitation_energies_ev:\n",
+ " wl = r.wavelengths_nm()\n",
+ " exc_rows = []\n",
+ " for i, (e_ev, f_osc) in enumerate(\n",
+ " zip(r.excitation_energies_ev[:8], r.oscillator_strengths[:8]), 1\n",
+ " ):\n",
+ " bold = \"font-weight:bold\" if f_osc > 0.05 else \"\"\n",
+ " exc_rows.append(\n",
+ " f''\n",
+ " f'| S{i} | '\n",
+ " f'{e_ev:.3f} eV | '\n",
+ " f'{wl[i - 1]:.1f} nm | '\n",
+ " f'f = {f_osc:.4f} | '\n",
+ " f\"
\"\n",
+ " )\n",
+ " if len(r.excitation_energies_ev) > 8:\n",
+ " exc_rows.append(\n",
+ " f'| … '\n",
+ " f\"and {len(r.excitation_energies_ev) - 8} more states |
\"\n",
+ " )\n",
+ " exc_table = (\n",
+ " '| '\n",
+ " \"Vertical excitations: |
\"\n",
+ " ''\n",
+ " '| State | '\n",
+ " 'Energy | '\n",
+ " 'λ | '\n",
+ " 'Osc. str. |
'\n",
+ " + \"\".join(exc_rows)\n",
+ " )\n",
+ " return (\n",
+ " f''\n",
+ " f\"
TD-DFT / UV-Vis — {r.formula} ({r.method}/{r.basis})\"\n",
+ " f'
'\n",
+ " f\"{header_rows}{exc_table}
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _do_run(btn):\n",
+ " mol = _state[\"molecule\"]\n",
+ " if mol is None:\n",
+ " run_status.value = \"Load a molecule first.\"\n",
+ " return\n",
+ " run_btn.disabled = True\n",
+ " run_status.value = \"Starting...\"\n",
+ " run_output.clear_output()\n",
+ " result_output.clear_output()\n",
+ "\n",
+ " # Advance step indicator: method confirmed → running\n",
+ " step_progress.complete(1)\n",
+ " step_progress.start(2)\n",
+ "\n",
+ " _calc_log.log_event(\n",
+ " \"calc_start\",\n",
+ " f\"{method_dd.value}/{basis_dd.value} on {mol.get_formula()}\",\n",
+ " n_atoms=len(mol.atoms),\n",
+ " )\n",
+ " _run_wall_t = time.perf_counter()\n",
+ "\n",
+ " def _thread():\n",
+ " log = _LogCapture(run_output)\n",
+ " try:\n",
+ " calc_mol = mol\n",
+ " if preopt_cb.value and PREOPT_AVAILABLE:\n",
+ " run_status.value = \"Pre-optimizing...\"\n",
+ " calc_mol, _rmsd = preoptimize(mol)\n",
+ " _set_molecule(calc_mol, f\"Geometry pre-optimized (LJ, RMSD={_rmsd:.3f} Å)\")\n",
+ "\n",
+ " # ── Dispatch to the right backend based on Calc. Type ─────────────\n",
+ " ct = calc_type_dd.value\n",
+ " if ct == \"Geometry Opt\":\n",
+ " run_status.value = \"Optimizing geometry...\"\n",
+ " from quantui import optimize_geometry\n",
+ " result = optimize_geometry(\n",
+ " molecule=calc_mol,\n",
+ " method=method_dd.value,\n",
+ " basis=basis_dd.value,\n",
+ " fmax=fmax_fi.value,\n",
+ " steps=max_steps_si.value,\n",
+ " progress_stream=log,\n",
+ " )\n",
+ " result_html = _format_opt_result(result)\n",
+ " save_spectra, save_type = {}, \"geometry_opt\"\n",
+ " elif ct == \"Frequency\":\n",
+ " run_status.value = \"Computing frequencies (SCF + Hessian)...\"\n",
+ " from quantui.freq_calc import run_freq_calc\n",
+ " result = run_freq_calc(\n",
+ " molecule=calc_mol,\n",
+ " method=method_dd.value,\n",
+ " basis=basis_dd.value,\n",
+ " progress_stream=log,\n",
+ " )\n",
+ " result_html = _format_freq_result(result)\n",
+ " save_spectra = {\"ir\": {\n",
+ " \"frequencies_cm1\": result.frequencies_cm1,\n",
+ " \"ir_intensities\": result.ir_intensities,\n",
+ " \"zpve_hartree\": result.zpve_hartree,\n",
+ " }}\n",
+ " save_type = \"frequency\"\n",
+ " elif ct == \"UV-Vis (TD-DFT)\":\n",
+ " run_status.value = \"Running TD-DFT excited states...\"\n",
+ " from quantui.tddft_calc import run_tddft_calc\n",
+ " result = run_tddft_calc(\n",
+ " molecule=calc_mol,\n",
+ " method=method_dd.value,\n",
+ " basis=basis_dd.value,\n",
+ " nstates=nstates_si.value,\n",
+ " progress_stream=log,\n",
+ " )\n",
+ " result_html = _format_tddft_result(result)\n",
+ " save_spectra = {\"uv_vis\": {\n",
+ " \"excitation_energies_ev\": result.excitation_energies_ev,\n",
+ " \"oscillator_strengths\": result.oscillator_strengths,\n",
+ " \"wavelengths_nm\": result.wavelengths_nm(),\n",
+ " }}\n",
+ " save_type = \"tddft\"\n",
+ " else: # Single Point\n",
+ " run_status.value = \"Calculating...\"\n",
+ " result = run_in_session(\n",
+ " molecule=calc_mol,\n",
+ " method=method_dd.value,\n",
+ " basis=basis_dd.value,\n",
+ " progress_stream=log,\n",
+ " )\n",
+ " result_html = _format_result(result)\n",
+ " save_spectra, save_type = {}, \"single_point\"\n",
+ " _elapsed = time.perf_counter() - _run_wall_t\n",
+ "\n",
+ " _state[\"last_result\"] = result\n",
+ " accumulate_btn.disabled = False\n",
+ "\n",
+ " result_output.append_display_data(HTML(result_html))\n",
+ " run_status.value = f\"Done in {_elapsed:.1f} s.\"\n",
+ "\n",
+ " # Advance step indicator: run complete → results ready\n",
+ " step_progress.complete(2)\n",
+ " step_progress.complete(3)\n",
+ "\n",
+ " # Persist result to disk\n",
+ " try:\n",
+ " from quantui import save_result\n",
+ " save_result(\n",
+ " result, pyscf_log=log.getvalue(),\n",
+ " calc_type=save_type, spectra=save_spectra,\n",
+ " )\n",
+ " _refresh_results_browser()\n",
+ " _populate_compare_list()\n",
+ " _update_log_panel(\n",
+ " log.getvalue(),\n",
+ " f\"{result.formula} {method_dd.value}/{basis_dd.value}\",\n",
+ " )\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ " # Log performance record and refresh the estimate widget\n",
+ " try:\n",
+ " _calc_log.log_calculation(\n",
+ " formula=result.formula,\n",
+ " n_atoms=len(calc_mol.atoms),\n",
+ " n_electrons=calc_mol.get_electron_count(),\n",
+ " method=result.method,\n",
+ " basis=result.basis,\n",
+ " n_iterations=getattr(result, \"n_iterations\", -1),\n",
+ " elapsed_s=_elapsed,\n",
+ " converged=result.converged,\n",
+ " )\n",
+ " _calc_log.log_event(\n",
+ " \"calc_done\",\n",
+ " f\"{result.method}/{result.basis} on {result.formula}\",\n",
+ " elapsed_s=round(_elapsed, 2),\n",
+ " converged=result.converged,\n",
+ " )\n",
+ " _update_estimate()\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ " except ImportError as _import_err:\n",
+ " _err_detail = str(_import_err)\n",
+ " log.write(\n",
+ " f\"Import error: {_err_detail}\\n\\n\"\n",
+ " \"A required calculation dependency could not be loaded.\\n\"\n",
+ " \"On Windows: use the Apptainer container.\\n\"\n",
+ " \" apptainer run quantui-local.sif\\n\"\n",
+ " )\n",
+ " run_status.value = \"Import error — see output.\"\n",
+ " step_progress.fail(2, _err_detail[:60])\n",
+ " _calc_log.log_event(\"calc_error\", _err_detail[:200])\n",
+ "\n",
+ " except Exception as exc:\n",
+ " import traceback\n",
+ " _elapsed = time.perf_counter() - _run_wall_t\n",
+ " log.write(f\"Error: {exc}\\n\\n{traceback.format_exc()}\")\n",
+ " run_status.value = \"Error — see Calculation Output below.\"\n",
+ " step_progress.fail(2, str(exc)[:60])\n",
+ " _calc_log.log_event(\"calc_error\", str(exc)[:200], elapsed_s=round(_elapsed, 2))\n",
+ "\n",
+ " finally:\n",
+ " run_btn.disabled = False\n",
+ "\n",
+ " threading.Thread(target=_thread, daemon=True).start()\n",
+ "\n",
+ "run_btn.on_click(_do_run)\n",
+ "\n",
+ "\n",
+ "def _update_notes(change=None):\n",
+ " notes_output.clear_output(wait=True)\n",
+ " mol = _state[\"molecule\"]\n",
+ " if mol is None:\n",
+ " return\n",
+ " try:\n",
+ " from quantui import PySCFCalculation\n",
+ " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
+ " notes = calc.get_educational_notes()\n",
+ " if notes:\n",
+ " safe = (\n",
+ " notes\n",
+ " .replace(\"**\", \"\", 1)\n",
+ " .replace(\"**\", \"\", 1)\n",
+ " .replace(\"\\n\\n\", \"
\")\n",
+ " )\n",
+ " with notes_output:\n",
+ " display(HTML(\n",
+ " ''\n",
+ " + safe + \"
\"\n",
+ " ))\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ "method_dd.observe(_update_notes, names=\"value\")\n",
+ "basis_dd.observe(_update_notes, names=\"value\")\n",
+ "\n",
+ "\n",
+ "def _update_estimate(change=None):\n",
+ " '''Refresh the time-estimate label based on current molecule + method/basis.'''\n",
+ " mol = _state.get(\"molecule\")\n",
+ " if mol is None:\n",
+ " perf_estimate_html.value = \"\"\n",
+ " return\n",
+ " try:\n",
+ " est = _calc_log.estimate_time(\n",
+ " n_atoms=len(mol.atoms),\n",
+ " n_electrons=mol.get_electron_count(),\n",
+ " method=method_dd.value,\n",
+ " basis=basis_dd.value,\n",
+ " )\n",
+ " perf_estimate_html.value = _calc_log.format_estimate(est)\n",
+ " except Exception:\n",
+ " perf_estimate_html.value = \"\"\n",
+ "\n",
+ "method_dd.observe(_update_estimate, names=\"value\")\n",
+ "basis_dd.observe(_update_estimate, names=\"value\")\n",
+ "\n",
+ "# Log startup event\n",
+ "_calc_log.log_event(\"startup\", f\"QuantUI-local {quantui.__version__} started\")\n",
+ "\n",
+ "\n",
+ "def _do_accumulate(btn):\n",
+ " r = _state[\"last_result\"]\n",
+ " if r is None:\n",
+ " return\n",
+ " _state[\"results\"].append(r)\n",
+ " _refresh_comparison()\n",
+ "\n",
+ "accumulate_btn.on_click(_do_accumulate)\n",
+ "\n",
+ "\n",
+ "def _refresh_comparison():\n",
+ " from quantui import summary_from_session_result, comparison_table_html\n",
+ " comparison_output.clear_output(wait=True)\n",
+ " results = _state[\"results\"]\n",
+ " if not results:\n",
+ " return\n",
+ " summaries = [summary_from_session_result(r) for r in results]\n",
+ " with comparison_output:\n",
+ " display(HTML(comparison_table_html(summaries)))\n",
+ " if len(summaries) > 1:\n",
+ " try:\n",
+ " from quantui import plot_comparison\n",
+ " plot_comparison(summaries)\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "def _do_clear(btn):\n",
+ " _state[\"results\"].clear()\n",
+ " comparison_output.clear_output()\n",
+ "\n",
+ "clear_btn.on_click(_do_clear)\n",
+ "\n",
+ "\n",
+ "def _do_export(btn):\n",
+ " mol = _state[\"molecule\"]\n",
+ " if mol is None:\n",
+ " export_status.value = \"Load a molecule first.\"\n",
+ " return\n",
+ " try:\n",
+ " from quantui import PySCFCalculation\n",
+ " from pathlib import Path\n",
+ " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
+ " fname = f\"{mol.get_formula()}_{method_dd.value}_{basis_dd.value}.py\"\n",
+ " calc.generate_calculation_script(Path(fname))\n",
+ " export_status.value = f\"Saved: {fname}\"\n",
+ " except Exception as exc:\n",
+ " export_status.value = f\"Error: {exc}\"\n",
+ "\n",
+ "export_btn.on_click(_do_export)\n",
+ "\n",
+ "\n",
+ "def _on_method_help(btn):\n",
+ " _show_help_topic(\"method\")\n",
+ "\n",
+ "def _on_basis_help(btn):\n",
+ " _show_help_topic(\"basis_set\")\n",
+ "\n",
+ "method_help_btn.on_click(_on_method_help)\n",
+ "basis_help_btn.on_click(_on_basis_help)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# ── Preset Library ─────────────────────────────────────────────────────────\n",
+ "_preset_opts = [\"(select a molecule)\"] + list(MOLECULE_LIBRARY.keys())\n",
+ "preset_dd = widgets.Dropdown(\n",
+ " options=_preset_opts, value=\"(select a molecule)\",\n",
+ " description=\"Molecule:\", style={\"description_width\": \"90px\"},\n",
+ " layout=widgets.Layout(width=\"320px\"),\n",
+ ")\n",
+ "\n",
+ "def _load_preset(change):\n",
+ " name = change[\"new\"]\n",
+ " if name.startswith(\"(\"):\n",
+ " return\n",
+ " d = MOLECULE_LIBRARY[name]\n",
+ " _set_molecule(\n",
+ " Molecule(\n",
+ " atoms=d[\"atoms\"], coordinates=d[\"coordinates\"],\n",
+ " charge=d[\"charge\"], multiplicity=d[\"multiplicity\"],\n",
+ " ),\n",
+ " d[\"description\"],\n",
+ " )\n",
+ "\n",
+ "preset_dd.observe(_load_preset, names=\"value\")\n",
+ "\n",
+ "# ── XYZ Input ──────────────────────────────────────────────────────────────\n",
+ "xyz_area = widgets.Textarea(\n",
+ " placeholder=(\n",
+ " \"Paste XYZ coordinates (symbol x y z):\\n\"\n",
+ " \"O 0.000 0.000 0.000\\n\"\n",
+ " \"H 0.757 0.587 0.000\\n\"\n",
+ " \"H -0.757 0.587 0.000\"\n",
+ " ),\n",
+ " layout=widgets.Layout(width=\"440px\", height=\"130px\"),\n",
+ ")\n",
+ "xyz_btn = widgets.Button(description=\"Load XYZ\", button_style=\"info\", icon=\"upload\")\n",
+ "xyz_msg = widgets.Label()\n",
+ "\n",
+ "def _load_xyz(btn):\n",
+ " try:\n",
+ " mol = parse_xyz_input(xyz_area.value.strip())\n",
+ " _set_molecule(mol, \"Loaded from XYZ input\")\n",
+ " xyz_msg.value = \"\"\n",
+ " except Exception as exc:\n",
+ " xyz_msg.value = f\"Parse error: {exc}\"\n",
+ "\n",
+ "xyz_btn.on_click(_load_xyz)\n",
+ "\n",
+ "# ── PubChem Search ─────────────────────────────────────────────────────────\n",
+ "pubchem_txt = widgets.Text(\n",
+ " placeholder=\"name or SMILES (e.g. aspirin, caffeine, CC(=O)O)\",\n",
+ " layout=widgets.Layout(width=\"380px\"),\n",
+ ")\n",
+ "pubchem_btn = widgets.Button(\n",
+ " description=\"Search\", button_style=\"info\", icon=\"search\",\n",
+ " disabled=not PUBCHEM_AVAILABLE,\n",
+ " layout=widgets.Layout(width=\"100px\"),\n",
+ ")\n",
+ "pubchem_msg = widgets.Label(\n",
+ " value=\"\" if PUBCHEM_AVAILABLE else \"PubChem unavailable — check internet connection\"\n",
+ ")\n",
+ "\n",
+ "def _search_pubchem(btn):\n",
+ " query = pubchem_txt.value.strip()\n",
+ " if not query:\n",
+ " pubchem_msg.value = \"Enter a molecule name or SMILES.\"\n",
+ " return\n",
+ " if student_friendly_fetch is None:\n",
+ " pubchem_msg.value = \"PubChem module not available.\"\n",
+ " return\n",
+ " pubchem_msg.value = f'Searching for \"{query}\"...'\n",
+ " pubchem_btn.disabled = True\n",
+ "\n",
+ " def _do():\n",
+ " try:\n",
+ " mol = student_friendly_fetch(query)\n",
+ " _set_molecule(mol, f\"PubChem: {query}\")\n",
+ " pubchem_msg.value = f\"Loaded {mol.get_formula()} from PubChem.\"\n",
+ " except Exception as exc:\n",
+ " pubchem_msg.value = f\"Not found: {exc}\"\n",
+ " finally:\n",
+ " pubchem_btn.disabled = False\n",
+ "\n",
+ " threading.Thread(target=_do, daemon=True).start()\n",
+ "\n",
+ "pubchem_btn.on_click(_search_pubchem)\n",
+ "\n",
+ "# ── Assemble input tab ─────────────────────────────────────────────────────\n",
+ "_hint = ''\n",
+ "tab_preset = widgets.VBox([\n",
+ " widgets.HTML(_hint + \"Choose from 20+ curated educational molecules.
\"),\n",
+ " preset_dd,\n",
+ "])\n",
+ "tab_xyz = widgets.VBox([\n",
+ " widgets.HTML(_hint + \"Paste XYZ coordinates (element x y z, one atom per line).\"),\n",
+ " xyz_area,\n",
+ " widgets.HBox([xyz_btn, xyz_msg]),\n",
+ "])\n",
+ "tab_pubchem = widgets.VBox([\n",
+ " widgets.HTML(_hint + \"Search by name or SMILES. Requires internet connection.\"),\n",
+ " widgets.HBox([pubchem_txt, pubchem_btn]),\n",
+ " pubchem_msg,\n",
+ "])\n",
+ "\n",
+ "input_tab = widgets.Tab(children=[tab_preset, tab_xyz, tab_pubchem])\n",
+ "for _i, _t in enumerate([\"Preset Library\", \"XYZ Input\", \"PubChem Search\"]):\n",
+ " input_tab.set_title(_i, _t)\n",
+ "\n",
+ "# ── Collapsible molecule input container ──────────────────────────────────\n",
+ "mol_input_expanded = widgets.VBox([\n",
+ " widgets.HTML('Molecule Input
'),\n",
+ " input_tab,\n",
+ "])\n",
+ "\n",
+ "# Change button — re-expands the input panel\n",
+ "change_mol_btn = widgets.Button(\n",
+ " description=\"Change\", button_style=\"\", icon=\"pencil\",\n",
+ " layout=widgets.Layout(width=\"100px\", height=\"32px\"),\n",
+ " tooltip=\"Re-expand the molecule input panel\",\n",
+ ")\n",
+ "\n",
+ "# Collapsed view: compact summary pill + Change button\n",
+ "mol_input_collapsed = widgets.HBox(\n",
+ " [mol_summary_compact, change_mol_btn],\n",
+ " layout=widgets.Layout(align_items=\"center\", gap=\"12px\", padding=\"6px 0\"),\n",
+ ")\n",
+ "\n",
+ "# Container starts expanded; _set_molecule() collapses it after first load.\n",
+ "mol_input_container = widgets.VBox(\n",
+ " [mol_input_expanded, mol_info_html, viz_output],\n",
+ " layout=widgets.Layout(margin=\"0 0 4px 0\"),\n",
+ ")\n",
+ "\n",
+ "def _expand_mol_input(btn):\n",
+ " mol_input_container.children = [mol_input_expanded, mol_info_html, viz_output]\n",
+ "\n",
+ "change_mol_btn.on_click(_expand_mol_input)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "calc_setup_panel = widgets.VBox([\n",
+ " widgets.HTML('Calculation Setup
'),\n",
+ " widgets.HBox([\n",
+ " widgets.VBox([\n",
+ " widgets.HBox(\n",
+ " [method_dd, method_help_btn],\n",
+ " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
+ " ),\n",
+ " widgets.HBox(\n",
+ " [basis_dd, basis_help_btn],\n",
+ " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
+ " ),\n",
+ " ]),\n",
+ " widgets.HTML(\" \"),\n",
+ " widgets.VBox([charge_si, mult_si]),\n",
+ " ]),\n",
+ " calc_type_dd,\n",
+ " calc_extra_opts,\n",
+ " preopt_cb,\n",
+ " notes_output,\n",
+ "])\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "run_panel = widgets.VBox([\n",
+ " widgets.HTML(\n",
+ " 'Run Calculation
'\n",
+ " 'PySCF runs in this kernel. '\n",
+ " \"Output appears live below. Large molecules or high-accuracy basis sets may take \"\n",
+ " \"several minutes on a laptop.
\"\n",
+ " ),\n",
+ " perf_estimate_html,\n",
+ " widgets.HBox([run_btn, run_status]),\n",
+ " widgets.HBox(\n",
+ " [\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Calculation Output\"\n",
+ " ),\n",
+ " log_clear_btn,\n",
+ " ],\n",
+ " layout=widgets.Layout(align_items=\"center\", justify_content=\"space-between\",\n",
+ " margin=\"10px 0 4px\", max_width=\"460px\"),\n",
+ " ),\n",
+ " run_output,\n",
+ "])\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "results_panel = widgets.VBox([\n",
+ " widgets.HTML('Results
'),\n",
+ " result_output,\n",
+ "])\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import json\n",
+ "from pathlib import Path\n",
+ "\n",
+ "# ── Past Results browser ──────────────────────────────────────────────────────\n",
+ "past_dd = widgets.Dropdown(\n",
+ " description=\"Load:\",\n",
+ " options=[(\"(no saved results)\", \"\")],\n",
+ " style={\"description_width\": \"50px\"},\n",
+ " layout=widgets.Layout(width=\"500px\"),\n",
+ ")\n",
+ "past_refresh_btn = widgets.Button(\n",
+ " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
+ " layout=widgets.Layout(width=\"100px\"),\n",
+ " tooltip=\"Rescan the results directory\",\n",
+ ")\n",
+ "copy_path_btn = widgets.Button(\n",
+ " description=\"Copy path\", button_style=\"\", icon=\"clipboard\",\n",
+ " layout=widgets.Layout(width=\"120px\"),\n",
+ " tooltip=\"Copy the results directory path to clipboard\",\n",
+ ")\n",
+ "results_path_lbl = widgets.HTML()\n",
+ "past_output = widgets.Output()\n",
+ "\n",
+ "\n",
+ "def _get_results_dir() -> Path:\n",
+ " from quantui.results_storage import _default_results_dir\n",
+ " return _default_results_dir().resolve()\n",
+ "\n",
+ "\n",
+ "def _format_past_result(data: dict) -> str:\n",
+ " _conv = \"Yes\" if data.get(\"converged\") else \"No (treat results with caution)\"\n",
+ " _cc = \"green\" if data.get(\"converged\") else \"#c00\"\n",
+ " _gap = (\n",
+ " f\"{data['homo_lumo_gap_ev']:.4f} eV\"\n",
+ " if data.get(\"homo_lumo_gap_ev\") is not None else \"N/A\"\n",
+ " )\n",
+ " _rows = \"\".join(\n",
+ " f''\n",
+ " f'| {k} | '\n",
+ " f'{v} | '\n",
+ " f\"
\"\n",
+ " for k, v, vc in [\n",
+ " (\"Total energy\",\n",
+ " f\"{data['energy_hartree']:.8f} Ha ({data['energy_ev']:.4f} eV)\", \"#000\"),\n",
+ " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
+ " (\"SCF converged\", _conv, _cc),\n",
+ " (\"SCF iterations\", str(data.get(\"n_iterations\", \"?\")), \"#000\"),\n",
+ " ]\n",
+ " )\n",
+ " ts = data.get(\"timestamp\", \"\")\n",
+ " return (\n",
+ " f''\n",
+ " f'
{data[\"formula\"]} — {data[\"method\"]}/{data[\"basis\"]}'\n",
+ " f'
{ts}'\n",
+ " f'
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _refresh_results_browser():\n",
+ " '''Repopulate the past-results dropdown. Called on startup and after each run.'''\n",
+ " try:\n",
+ " from quantui import list_results, load_result\n",
+ " except ImportError:\n",
+ " return\n",
+ " results_path_lbl.value = f'{_get_results_dir()}'\n",
+ " dirs = list_results()\n",
+ " if not dirs:\n",
+ " past_dd.options = [(\"(no saved results)\", \"\")]\n",
+ " return\n",
+ " options = []\n",
+ " for d in dirs:\n",
+ " try:\n",
+ " data = load_result(d)\n",
+ " ts = data.get(\"timestamp\", d.name)\n",
+ " label = f\"{ts} · {data['formula']} {data['method']}/{data['basis']}\"\n",
+ " options.append((label, str(d)))\n",
+ " except Exception:\n",
+ " pass\n",
+ " past_dd.options = options if options else [(\"(no saved results)\", \"\")]\n",
+ "\n",
+ "\n",
+ "def _load_past_result(change):\n",
+ " path_str = change[\"new\"]\n",
+ " if not path_str:\n",
+ " past_output.clear_output()\n",
+ " return\n",
+ " past_output.clear_output(wait=True)\n",
+ " try:\n",
+ " from quantui import load_result\n",
+ " data = load_result(Path(path_str))\n",
+ " past_output.append_display_data(HTML(_format_past_result(data)))\n",
+ " except Exception as exc:\n",
+ " past_output.append_stdout(f\"Could not load result: {exc}\\n\")\n",
+ "\n",
+ "\n",
+ "def _copy_results_path(btn):\n",
+ " '''Copy the results directory path to clipboard via browser JS.'''\n",
+ " p = _get_results_dir()\n",
+ " p.mkdir(parents=True, exist_ok=True)\n",
+ " path_str = str(p).replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\")\n",
+ " from IPython.display import Javascript\n",
+ " display(Javascript(f\"navigator.clipboard.writeText(\\'{path_str}\\')\"))\n",
+ " # Brief confirmation in the label\n",
+ " import threading\n",
+ " results_path_lbl.value = (\n",
+ " f'Copied: {p}'\n",
+ " )\n",
+ " def _reset():\n",
+ " import time; time.sleep(3)\n",
+ " results_path_lbl.value = f'{p}'\n",
+ " threading.Thread(target=_reset, daemon=True).start()\n",
+ "\n",
+ "\n",
+ "view_log_btn = widgets.Button(\n",
+ " description=\"View log\",\n",
+ " button_style=\"\",\n",
+ " icon=\"file-text-o\",\n",
+ " layout=widgets.Layout(width=\"110px\"),\n",
+ " tooltip=\"Open the full PySCF output log for this result in the Output tab\",\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def _view_log(btn):\n",
+ " path_str = past_dd.value\n",
+ " if not path_str:\n",
+ " return\n",
+ " log_path = Path(path_str) / \"pyscf.log\"\n",
+ " if log_path.exists():\n",
+ " text = log_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n",
+ " label = Path(path_str).name\n",
+ " else:\n",
+ " text = \"(No pyscf.log found for this result.)\"\n",
+ " label = \"\"\n",
+ " _update_log_panel(text, label)\n",
+ " _goto_output_tab()\n",
+ "\n",
+ "\n",
+ "past_dd.observe(_load_past_result, names=\"value\")\n",
+ "past_refresh_btn.on_click(lambda _: _refresh_results_browser())\n",
+ "copy_path_btn.on_click(_copy_results_path)\n",
+ "view_log_btn.on_click(_view_log)\n",
+ "\n",
+ "# Populate on startup and make the real function visible to _do_run in Cell 3.\n",
+ "_refresh_results_browser()\n",
+ "\n",
+ "# ── Performance stats widgets ───────────────────────────────────────────────────────────────────\n",
+ "_perf_stats_html = widgets.HTML()\n",
+ "_perf_events_html = widgets.HTML()\n",
+ "\n",
+ "_reset_btn = widgets.Button(\n",
+ " description=\"Reset performance database\",\n",
+ " button_style=\"danger\",\n",
+ " icon=\"trash\",\n",
+ " layout=widgets.Layout(width=\"230px\"),\n",
+ ")\n",
+ "_reset_confirm_html = widgets.HTML(\n",
+ " ''\n",
+ " \"Warning: This will permanently delete all performance records. \"\n",
+ " \"Time estimates will reset to “no data”.\"\n",
+ ")\n",
+ "_reset_confirm_yes = widgets.Button(\n",
+ " description=\"Yes, delete all records\",\n",
+ " button_style=\"danger\",\n",
+ " icon=\"check\",\n",
+ " layout=widgets.Layout(width=\"190px\"),\n",
+ ")\n",
+ "_reset_confirm_no = widgets.Button(\n",
+ " description=\"Cancel\",\n",
+ " button_style=\"\",\n",
+ " icon=\"times\",\n",
+ " layout=widgets.Layout(width=\"90px\"),\n",
+ ")\n",
+ "_reset_confirm_box = widgets.VBox(\n",
+ " [\n",
+ " _reset_confirm_html,\n",
+ " widgets.HBox(\n",
+ " [_reset_confirm_yes, _reset_confirm_no],\n",
+ " layout=widgets.Layout(gap=\"8px\", margin=\"4px 0 0\"),\n",
+ " ),\n",
+ " ],\n",
+ " layout=widgets.Layout(\n",
+ " display=\"none\",\n",
+ " border=\"1px solid #fca5a5\",\n",
+ " padding=\"8px 10px\",\n",
+ " margin=\"6px 0 0\",\n",
+ " ),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def _build_perf_stats_html() -> str:\n",
+ " from quantui.calc_log import get_perf_history\n",
+ " records = get_perf_history()\n",
+ " if not records:\n",
+ " return (\n",
+ " ''\n",
+ " \"No performance data recorded yet.\"\n",
+ " )\n",
+ " groups: dict = {}\n",
+ " for r in records:\n",
+ " key = (r.get(\"method\", \"?\"), r.get(\"basis\", \"?\"))\n",
+ " groups.setdefault(key, []).append(r)\n",
+ " rows = \"\"\n",
+ " for (meth, bas), recs in sorted(groups.items()):\n",
+ " times = [r[\"elapsed_s\"] for r in recs if \"elapsed_s\" in r]\n",
+ " n = len(recs)\n",
+ " if times:\n",
+ " avg = sum(times) / len(times)\n",
+ " rows += (\n",
+ " \"\"\n",
+ " f'| {meth} | '\n",
+ " f'{bas} | '\n",
+ " f'{n} | '\n",
+ " f'{avg:.1f} s | '\n",
+ " f'{min(times):.1f} s | '\n",
+ " f'{max(times):.1f} s | '\n",
+ " \"
\"\n",
+ " )\n",
+ " header = (\n",
+ " \"\"\n",
+ " '| Method | '\n",
+ " 'Basis | '\n",
+ " 'Runs | '\n",
+ " 'Avg | '\n",
+ " 'Min | '\n",
+ " 'Max | '\n",
+ " \"
\"\n",
+ " )\n",
+ " return (\n",
+ " ''\n",
+ " f\"{header}{rows}
\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _build_events_html() -> str:\n",
+ " from quantui.calc_log import get_recent_events\n",
+ " events = get_recent_events(20)\n",
+ " if not events:\n",
+ " return (\n",
+ " ''\n",
+ " \"No events recorded yet.\"\n",
+ " )\n",
+ " rows = \"\"\n",
+ " for e in reversed(events):\n",
+ " ts = e.get(\"timestamp\", \"\")[:19].replace(\"T\", \" \")\n",
+ " evt = e.get(\"event\", \"\")\n",
+ " msg = e.get(\"message\", \"\")\n",
+ " rows += (\n",
+ " \"\"\n",
+ " f'| {ts} | '\n",
+ " f'{evt} | '\n",
+ " f'{msg} | '\n",
+ " \"
\"\n",
+ " )\n",
+ " return (\n",
+ " '\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _refresh_perf_stats():\n",
+ " _perf_stats_html.value = _build_perf_stats_html()\n",
+ " _perf_events_html.value = _build_events_html()\n",
+ "\n",
+ "\n",
+ "def _on_reset_click(btn):\n",
+ " _reset_confirm_box.layout.display = \"\"\n",
+ "\n",
+ "\n",
+ "def _on_confirm_yes(btn):\n",
+ " from quantui.calc_log import reset_perf_log\n",
+ " reset_perf_log()\n",
+ " _reset_confirm_box.layout.display = \"none\"\n",
+ " _refresh_perf_stats()\n",
+ "\n",
+ "\n",
+ "def _on_confirm_no(btn):\n",
+ " _reset_confirm_box.layout.display = \"none\"\n",
+ "\n",
+ "\n",
+ "_reset_btn.on_click(_on_reset_click)\n",
+ "_reset_confirm_yes.on_click(_on_confirm_yes)\n",
+ "_reset_confirm_no.on_click(_on_confirm_no)\n",
+ "_refresh_perf_stats()\n",
+ "\n",
+ "_perf_stats_panel = widgets.VBox([\n",
+ " _perf_stats_html,\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Recent events (last 20)
\"\n",
+ " ),\n",
+ " _perf_events_html,\n",
+ " widgets.HBox(\n",
+ " [_reset_btn],\n",
+ " layout=widgets.Layout(margin=\"14px 0 4px\"),\n",
+ " ),\n",
+ " _reset_confirm_box,\n",
+ "])\n",
+ "\n",
+ "_perf_accordion = widgets.Accordion(children=[_perf_stats_panel], selected_index=None)\n",
+ "_perf_accordion.set_title(0, \"Performance stats\")\n",
+ "\n",
+ "# ── History panel (assembled widget for the History tab) ─────────────────\n",
+ "history_panel = widgets.VBox([\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Calculations are saved automatically. Select one below to view its results.
\"\n",
+ " ),\n",
+ " widgets.HBox(\n",
+ " [past_dd, past_refresh_btn, copy_path_btn, view_log_btn],\n",
+ " layout=widgets.Layout(align_items=\"center\", gap=\"8px\"),\n",
+ " ),\n",
+ " results_path_lbl,\n",
+ " past_output,\n",
+ " _perf_accordion,\n",
+ "])\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pathlib import Path\n",
+ "\n",
+ "# ── Compare tab widgets ────────────────────────────────────────────────────────\n",
+ "compare_select = widgets.SelectMultiple(\n",
+ " options=[(\"(no saved results)\", \"\")],\n",
+ " rows=8,\n",
+ " description=\"\",\n",
+ " layout=widgets.Layout(width=\"100%\"),\n",
+ ")\n",
+ "compare_refresh_btn = widgets.Button(\n",
+ " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
+ " layout=widgets.Layout(width=\"100px\"),\n",
+ ")\n",
+ "compare_btn = widgets.Button(\n",
+ " description=\"Compare selected\", button_style=\"primary\", icon=\"bar-chart\",\n",
+ " disabled=True,\n",
+ " layout=widgets.Layout(width=\"180px\"),\n",
+ ")\n",
+ "compare_clear_btn = widgets.Button(\n",
+ " description=\"Clear\", button_style=\"warning\", icon=\"times\",\n",
+ " layout=widgets.Layout(width=\"90px\"),\n",
+ ")\n",
+ "compare_output = widgets.Output()\n",
+ "\n",
+ "\n",
+ "def _populate_compare_list():\n",
+ " '''Repopulate compare_select with all saved results.'''\n",
+ " from quantui.results_storage import list_results, load_result\n",
+ " dirs = list_results()\n",
+ " if not dirs:\n",
+ " compare_select.options = [(\"(no saved results)\", \"\")]\n",
+ " compare_btn.disabled = True\n",
+ " return\n",
+ " options = []\n",
+ " for d in dirs:\n",
+ " try:\n",
+ " data = load_result(d)\n",
+ " ts = data.get(\"timestamp\", d.name[:19])\n",
+ " label = f\"{ts} {data['formula']} {data['method']}/{data['basis']}\"\n",
+ " options.append((label, str(d)))\n",
+ " except Exception:\n",
+ " options.append((d.name, str(d)))\n",
+ " compare_select.options = options\n",
+ " compare_btn.disabled = False\n",
+ "\n",
+ "\n",
+ "def _on_compare_refresh(btn):\n",
+ " _populate_compare_list()\n",
+ "\n",
+ "\n",
+ "def _on_compare(btn):\n",
+ " selected = compare_select.value # tuple of selected path strings\n",
+ " if not selected or selected == (\"\",):\n",
+ " return\n",
+ " compare_output.clear_output(wait=True)\n",
+ " from quantui.results_storage import load_result\n",
+ " from quantui import summary_from_saved_result, comparison_table_html, plot_comparison\n",
+ " summaries = []\n",
+ " for path_str in selected:\n",
+ " if not path_str:\n",
+ " continue\n",
+ " try:\n",
+ " data = load_result(Path(path_str))\n",
+ " summaries.append(summary_from_saved_result(data))\n",
+ " except Exception as exc:\n",
+ " with compare_output:\n",
+ " display(HTML(f'Error loading result: {exc}
'))\n",
+ " if not summaries:\n",
+ " return\n",
+ " with compare_output:\n",
+ " display(HTML(comparison_table_html(summaries)))\n",
+ " if len(summaries) > 1:\n",
+ " try:\n",
+ " import matplotlib.pyplot as plt\n",
+ " fig = plot_comparison(summaries)\n",
+ " display(fig)\n",
+ " plt.close(fig)\n",
+ " except Exception:\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "def _on_compare_clear(btn):\n",
+ " compare_select.value = ()\n",
+ " compare_output.clear_output()\n",
+ "\n",
+ "\n",
+ "compare_refresh_btn.on_click(_on_compare_refresh)\n",
+ "compare_btn.on_click(_on_compare)\n",
+ "compare_clear_btn.on_click(_on_compare_clear)\n",
+ "\n",
+ "# Populate on load; expose to Cell-3 placeholder so _do_run refreshes it.\n",
+ "_populate_compare_list()\n",
+ "globals()[\"_populate_compare_list\"] = _populate_compare_list\n",
+ "\n",
+ "# ── Compare panel (assembled widget for the Compare tab) ──────────────────────\n",
+ "compare_panel = widgets.VBox([\n",
+ " widgets.HTML(\n",
+ " 'Compare Calculations
'\n",
+ " ''\n",
+ " \"Select two or more saved calculations to compare side-by-side. \"\n",
+ " \"Hold Ctrl (or ⌘) to select multiple entries.
\"\n",
+ " ),\n",
+ " widgets.HBox([compare_refresh_btn]),\n",
+ " compare_select,\n",
+ " widgets.HBox(\n",
+ " [compare_btn, compare_clear_btn],\n",
+ " layout=widgets.Layout(gap=\"8px\", margin=\"6px 0\"),\n",
+ " ),\n",
+ " compare_output,\n",
+ "], layout=widgets.Layout(padding=\"8px 0\"))\n",
+ "\n",
+ "# ── Advanced accordion (Export only) ──────────────────────────────────────────\n",
+ "_export_content = widgets.VBox([\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Download a self-contained PySCF script you can study or run outside the notebook.
\"\n",
+ " ),\n",
+ " widgets.HBox([export_btn, export_status]),\n",
+ "])\n",
+ "advanced_accordion = widgets.Accordion(children=[_export_content])\n",
+ "advanced_accordion.set_title(0, \"Export Script\")\n",
+ "advanced_accordion.selected_index = None # collapsed by default\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from quantui.help_content import HELP_TOPICS\n",
+ "\n",
+ "# ── Help tab content ──────────────────────────────────────────────────────────\n",
+ "_help_keys = list(HELP_TOPICS.keys())\n",
+ "_help_labels = [HELP_TOPICS[k][\"title\"] for k in _help_keys]\n",
+ "\n",
+ "help_topic_dd = widgets.Dropdown(\n",
+ " options=list(zip(_help_labels, _help_keys)),\n",
+ " description=\"Topic:\",\n",
+ " style={\"description_width\": \"60px\"},\n",
+ " layout=widgets.Layout(width=\"460px\"),\n",
+ ")\n",
+ "help_content_html = widgets.HTML()\n",
+ "\n",
+ "def _render_help_topic(change=None):\n",
+ " key = help_topic_dd.value\n",
+ " if key and key in HELP_TOPICS:\n",
+ " entry = HELP_TOPICS[key]\n",
+ " help_content_html.value = (\n",
+ " f''\n",
+ " f'
'\n",
+ " f'{entry[\"title\"]}
'\n",
+ " f'
'\n",
+ " f'{entry[\"body\"]}
'\n",
+ " f'
'\n",
+ " )\n",
+ "\n",
+ "help_topic_dd.observe(_render_help_topic, names=\"value\")\n",
+ "_render_help_topic() # render first topic on load\n",
+ "\n",
+ "help_tab_panel = widgets.VBox([\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Browse help topics below. Click ? next to the Method or Basis Set \"\n",
+ " \"dropdown in the Calculate tab to jump directly to a relevant topic.
\"\n",
+ " ),\n",
+ " help_topic_dd,\n",
+ " help_content_html,\n",
+ "], layout=widgets.Layout(padding=\"8px 0\"))\n",
+ "\n",
+ "# ── Assemble root tab ─────────────────────────────────────────────────────────\n",
+ "_calculate_content = widgets.VBox([\n",
+ " step_progress.widget,\n",
+ " mol_input_container,\n",
+ " calc_setup_panel,\n",
+ " run_panel,\n",
+ " results_panel,\n",
+ " advanced_accordion,\n",
+ "], layout=widgets.Layout(padding=\"8px 0\"))\n",
+ "\n",
+ "# ── Output log tab ────────────────────────────────────────────────────────────────\n",
+ "_log_output_html = widgets.HTML(\n",
+ " ''\n",
+ " \"No log yet — run a calculation first, or use \"\n",
+ " \"View log in the History tab.\"\n",
+ ")\n",
+ "_log_source_lbl = widgets.HTML()\n",
+ "_log_clear_btn = widgets.Button(\n",
+ " description=\"Clear\",\n",
+ " button_style=\"\",\n",
+ " icon=\"times\",\n",
+ " layout=widgets.Layout(width=\"80px\"),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def _render_log(text: str, source_label: str = \"\") -> None:\n",
+ " # Render text as syntax-highlighted HTML in the log panel.\n",
+ " import html as _html_mod\n",
+ " lines = text.splitlines()\n",
+ " rows = []\n",
+ " for line in lines:\n",
+ " esc = _html_mod.escape(line)\n",
+ " if \"converged SCF energy\" in line or \"SCF converged\" in line:\n",
+ " style = \"color:#16a34a;font-weight:600\"\n",
+ " elif \"cycle=\" in line and \"E=\" in line:\n",
+ " style = \"color:#475569\"\n",
+ " elif \"HOMO\" in line or \"LUMO\" in line:\n",
+ " style = \"color:#2563eb\"\n",
+ " elif \"Warning\" in line or \"warning\" in line:\n",
+ " style = \"color:#d97706\"\n",
+ " elif \"Error\" in line or \"error\" in line or \"failed\" in line:\n",
+ " style = \"color:#dc2626\"\n",
+ " else:\n",
+ " style = \"color:#1e293b\"\n",
+ " rows.append(f'{esc}
')\n",
+ " _log_output_html.value = (\n",
+ " ''\n",
+ " + \"\".join(rows)\n",
+ " + \"
\"\n",
+ " )\n",
+ " _log_source_lbl.value = (\n",
+ " f'Source: {source_label}'\n",
+ " if source_label\n",
+ " else \"\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _update_log_panel(log_text: str, label: str = \"\") -> None:\n",
+ " _render_log(log_text, label)\n",
+ "\n",
+ "\n",
+ "def _goto_output_tab() -> None:\n",
+ " root_tab.selected_index = 3\n",
+ "\n",
+ "\n",
+ "def _clear_log(_):\n",
+ " _log_output_html.value = (\n",
+ " 'Log cleared.'\n",
+ " )\n",
+ " _log_source_lbl.value = \"\"\n",
+ "\n",
+ "\n",
+ "_log_clear_btn.on_click(_clear_log)\n",
+ "\n",
+ "log_tab_panel = widgets.VBox(\n",
+ " [\n",
+ " widgets.HTML(\n",
+ " ''\n",
+ " \"Full PySCF output for the most recent calculation. \"\n",
+ " \"Use View log in the History tab to load a saved result's log.
\"\n",
+ " ),\n",
+ " widgets.HBox(\n",
+ " [_log_clear_btn],\n",
+ " layout=widgets.Layout(margin=\"0 0 8px\"),\n",
+ " ),\n",
+ " _log_source_lbl,\n",
+ " _log_output_html,\n",
+ " ],\n",
+ " layout=widgets.Layout(padding=\"8px 0\"),\n",
+ ")\n",
+ "\n",
+ "# Overwrite Cell-3 placeholders so later widgets can be reached.\n",
+ "globals()[\"_update_log_panel\"] = _update_log_panel\n",
+ "globals()[\"_goto_output_tab\"] = _goto_output_tab\n",
+ "\n",
+ "root_tab = widgets.Tab(children=[\n",
+ " _calculate_content,\n",
+ " history_panel,\n",
+ " compare_panel,\n",
+ " log_tab_panel,\n",
+ " help_tab_panel,\n",
+ "])\n",
+ "root_tab.set_title(0, \"Calculate\")\n",
+ "root_tab.set_title(1, \"History\")\n",
+ "root_tab.set_title(2, \"Compare\")\n",
+ "root_tab.set_title(3, \"Output\")\n",
+ "root_tab.set_title(4, \"Help\")\n",
+ "\n",
+ "# ── \"?\" button callbacks: jump to Help tab with specific topic ────────────────\n",
+ "def _show_help_topic(topic: str) -> None:\n",
+ " if topic in HELP_TOPICS:\n",
+ " help_topic_dd.value = topic\n",
+ " root_tab.selected_index = 4\n",
+ "\n",
+ "# Overwrite Cell-3 placeholder so \"?\" buttons find the real function.\n",
+ "globals()[\"_show_help_topic\"] = _show_help_topic\n",
+ "\n",
+ "# ── Single root display call ──────────────────────────────────────────────────\n",
+ "display(root_tab)\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.11.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..cb8e253
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,338 @@
+"""
+Tests for quantui.app.QuantUIApp — FR-012 Phase 4.
+
+All tests instantiate QuantUIApp() without calling .display(), which is safe
+on any platform (display() requires an active IPython kernel; construction does
+not). PySCF is unavailable on Windows; calculations are skipped accordingly.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock, patch
+
+import ipywidgets as widgets
+import pytest
+
+from quantui.app import _RE_CONV, _RE_CYCLE, QuantUIApp, _LogCapture
+from quantui.molecule import Molecule
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _water() -> Molecule:
+ """Return a minimal water molecule for testing."""
+ return Molecule(
+ atoms=["O", "H", "H"],
+ coordinates=[[0.0, 0.0, 0.0], [0.757, 0.587, 0.0], [-0.757, 0.587, 0.0]],
+ )
+
+
+# ---------------------------------------------------------------------------
+# Instantiation
+# ---------------------------------------------------------------------------
+
+
+class TestInstantiation:
+ """QuantUIApp constructs successfully without calling display()."""
+
+ def test_instantiates(self):
+ app = QuantUIApp()
+ assert app is not None
+
+ def test_root_tab_is_widget(self):
+ app = QuantUIApp()
+ assert isinstance(app.root_tab, widgets.Tab)
+
+ def test_widget_property_returns_root_tab(self):
+ app = QuantUIApp()
+ assert app.widget is app.root_tab
+
+ def test_initial_state(self):
+ app = QuantUIApp()
+ assert app._molecule is None
+ assert app._last_result is None
+ assert app._results == []
+
+ def test_initial_molecule_is_none(self):
+ app = QuantUIApp()
+ assert app._molecule is None
+
+ def test_run_btn_initially_disabled(self):
+ app = QuantUIApp()
+ assert app.run_btn.disabled is True
+
+ def test_export_btn_initially_disabled(self):
+ app = QuantUIApp()
+ assert app.export_btn.disabled is True
+
+
+# ---------------------------------------------------------------------------
+# Default widget values
+# ---------------------------------------------------------------------------
+
+
+class TestDefaultWidgetValues:
+ """Widget dropdowns and inputs have expected defaults."""
+
+ def test_method_default(self):
+ from quantui.config import DEFAULT_METHOD
+
+ app = QuantUIApp()
+ assert app.method_dd.value == DEFAULT_METHOD
+
+ def test_basis_default(self):
+ from quantui.config import DEFAULT_BASIS
+
+ app = QuantUIApp()
+ assert app.basis_dd.value == DEFAULT_BASIS
+
+ def test_calc_type_default(self):
+ app = QuantUIApp()
+ assert app.calc_type_dd.value == "Single Point"
+
+ def test_theme_default(self):
+ app = QuantUIApp()
+ assert app.theme_btn.value == "Dark"
+
+ def test_charge_default(self):
+ from quantui.config import DEFAULT_CHARGE
+
+ app = QuantUIApp()
+ assert app.charge_si.value == DEFAULT_CHARGE
+
+ def test_multiplicity_default(self):
+ from quantui.config import DEFAULT_MULTIPLICITY
+
+ app = QuantUIApp()
+ assert app.mult_si.value == DEFAULT_MULTIPLICITY
+
+
+# ---------------------------------------------------------------------------
+# Tab structure
+# ---------------------------------------------------------------------------
+
+
+class TestTabStructure:
+ """root_tab has the correct number and titles of tabs."""
+
+ def test_five_tabs(self):
+ app = QuantUIApp()
+ assert len(app.root_tab.children) == 5
+
+ def test_tab_titles(self):
+ app = QuantUIApp()
+ expected = ["Calculate", "History", "Compare", "Output", "Help"]
+ for i, title in enumerate(expected):
+ assert app.root_tab.get_title(i) == title
+
+
+# ---------------------------------------------------------------------------
+# Molecule input — collapse / expand pattern
+# ---------------------------------------------------------------------------
+
+
+class TestMoleculeInputCollapse:
+ """mol_input_container switches between expanded and collapsed views."""
+
+ def test_initially_expanded(self):
+ app = QuantUIApp()
+ # Expanded: first child is mol_input_expanded
+ assert app.mol_input_container.children[0] is app.mol_input_expanded
+
+ def test_collapses_after_set_molecule(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ # Collapsed: first child is mol_input_collapsed
+ assert app.mol_input_container.children[0] is app.mol_input_collapsed
+
+ def test_molecule_stored_after_set_molecule(self):
+ app = QuantUIApp()
+ mol = _water()
+ app._set_molecule(mol)
+ assert app._molecule is mol
+
+ def test_run_btn_enabled_after_set_molecule(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ assert app.run_btn.disabled is False
+
+ def test_export_btn_enabled_after_set_molecule(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ assert app.export_btn.disabled is False
+
+ def test_mol_info_html_updated(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ assert "H2O" in app.mol_info_html.value
+
+ def test_expand_restores_expanded_view(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ # Simulate clicking "Change molecule"
+ app._on_expand_mol_input(None)
+ assert app.mol_input_container.children[0] is app.mol_input_expanded
+
+ def test_multiplicity_above_one_switches_to_uhf(self):
+ app = QuantUIApp()
+ app.method_dd.value = "RHF"
+ radical = Molecule(
+ atoms=["H"],
+ coordinates=[[0.0, 0.0, 0.0]],
+ multiplicity=2,
+ )
+ app._set_molecule(radical)
+ assert app.method_dd.value == "UHF"
+
+ def test_rhf_kept_for_singlet(self):
+ app = QuantUIApp()
+ app.method_dd.value = "RHF"
+ app._set_molecule(_water())
+ assert app.method_dd.value == "RHF"
+
+
+# ---------------------------------------------------------------------------
+# Step progress
+# ---------------------------------------------------------------------------
+
+
+class TestStepProgress:
+ """Step indicator advances correctly through the workflow."""
+
+ def test_step_0_active_initially(self):
+ app = QuantUIApp()
+ # Step 0 ("Choose molecule") should be active at start
+ assert app.step_progress._states[0] == "active"
+
+ def test_step_advances_after_set_molecule(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ # Step 0 done, step 1 should be active
+ assert app.step_progress._states[0] == "done"
+ assert app.step_progress._states[1] == "active"
+
+
+# ---------------------------------------------------------------------------
+# _LogCapture
+# ---------------------------------------------------------------------------
+
+
+class TestLogCapture:
+ """_LogCapture parses SCF cycle lines and updates the status label."""
+
+ def _make_capture(self):
+ out = widgets.Output()
+ status = widgets.Label()
+ return _LogCapture(out, status), status
+
+ def test_write_buffers_text(self):
+ cap, _ = self._make_capture()
+ cap.write("hello world\n")
+ assert "hello world" in cap.getvalue()
+
+ def test_cycle_regex_parses_line(self):
+ line = "cycle= 3 E= -76.031234 delta_E= -0.0042"
+ m = _RE_CYCLE.search(line)
+ assert m is not None
+ assert m.group(1) == "3"
+
+ def test_conv_regex_parses_line(self):
+ line = "converged SCF energy = -76.031234"
+ m = _RE_CONV.search(line)
+ assert m is not None
+
+ def test_status_label_updated_on_cycle(self):
+ cap, status = self._make_capture()
+ cap.write("cycle= 2 E= -76.031234 delta_E= -0.0042\n")
+ assert "SCF cycle 2" in status.value
+
+ def test_status_label_updated_on_convergence(self):
+ cap, status = self._make_capture()
+ cap.write("converged SCF energy = -76.031234\n")
+ assert "converged" in status.value.lower()
+
+ def test_flush_is_noop(self):
+ cap, _ = self._make_capture()
+ cap.flush() # Must not raise
+
+ def test_empty_write_is_noop(self):
+ cap, _ = self._make_capture()
+ cap.write("")
+ assert cap.getvalue() == ""
+
+
+# ---------------------------------------------------------------------------
+# _do_run dispatch
+# ---------------------------------------------------------------------------
+
+
+class TestDoRunDispatch:
+ """_do_run dispatches to the correct calculation function."""
+
+ @pytest.fixture
+ def app_with_molecule(self):
+ app = QuantUIApp()
+ app._set_molecule(_water())
+ return app
+
+ def test_single_point_dispatch(self, app_with_molecule):
+ app = app_with_molecule
+ app.calc_type_dd.value = "Single Point"
+ mock_result = MagicMock()
+ mock_result.energy_hartree = -75.0
+ mock_result.homo_lumo_gap_ev = 12.3
+ mock_result.converged = True
+ mock_result.n_iterations = 10
+ mock_result.formula = "H2O"
+ mock_result.method = "RHF"
+ mock_result.basis = "STO-3G"
+ with patch("quantui.run_in_session", return_value=mock_result) as mock_run:
+ with patch("quantui.save_result"):
+ app._do_run()
+ mock_run.assert_called_once()
+
+ def test_geo_opt_dispatch(self, app_with_molecule):
+ app = app_with_molecule
+ app.calc_type_dd.value = "Geometry Opt"
+ mock_result = MagicMock()
+ mock_result.energy_hartree = -75.0
+ mock_result.converged = True
+ mock_result.n_iterations = 5
+ mock_result.trajectory = []
+ mock_result.formula = "H2O"
+ mock_result.method = "RHF"
+ mock_result.basis = "STO-3G"
+ mock_result.final_molecule = _water()
+ with patch("quantui.optimize_geometry", return_value=mock_result) as mock_opt:
+ with patch("quantui.save_result"):
+ app._do_run()
+ mock_opt.assert_called_once()
+
+ def test_pyscf_unavailable_shows_error(self, app_with_molecule):
+ app = app_with_molecule
+ app.calc_type_dd.value = "Single Point"
+ with patch("quantui.app._PYSCF_AVAILABLE", False):
+ app._do_run()
+ # Should not raise; error message should be in run_output
+ # (output widget content is opaque, so just verify no exception)
+
+
+# ---------------------------------------------------------------------------
+# Availability flags on the instance
+# ---------------------------------------------------------------------------
+
+
+class TestAvailabilityFlags:
+ def test_pyscf_flag_mirrors_module_level(self):
+ from quantui.app import _PYSCF_AVAILABLE
+
+ app = QuantUIApp()
+ assert app._pyscf_available == _PYSCF_AVAILABLE
+
+ def test_preopt_flag_mirrors_module_level(self):
+ from quantui.app import _PREOPT_AVAILABLE
+
+ app = QuantUIApp()
+ assert app._preopt_available == _PREOPT_AVAILABLE
diff --git a/tests/test_pubchem.py b/tests/test_pubchem.py
index 8a75141..75a033d 100644
--- a/tests/test_pubchem.py
+++ b/tests/test_pubchem.py
@@ -320,6 +320,7 @@ def test_real_search_water(self):
except PubChemAPIError:
pytest.skip("PubChem not accessible")
+ @rdkit_only
def test_real_fetch_caffeine(self):
"""Test real fetch of caffeine molecule."""
try:
From 90267d2c11f8c33689800ade3de6707896d5bf73 Mon Sep 17 00:00:00 2001
From: NCCU-Schultz-Lab
Date: Thu, 16 Apr 2026 18:38:35 -0400
Subject: [PATCH 03/13] Implement FR-012 Phases 2-4; fix error display, RDKit
imports, themes
- app.py: QuantUIApp with all widget logic (FR-012 Phase 1-4)
- app.py: Errors now show clean card in Calculate tab; full traceback
goes to Output tab and ~/.quantui/logs/error_log.txt
- app.py: clear_output fires synchronously on Run click (not in thread)
- app.py: Remove Dark Blue/Maroon themes (imperceptible hue shift)
- pubchem.py: Fix missing 'from rdkit.Chem import Descriptors' import
- environment.yml: Add rdkit>=2022.03.1
- tests/test_app.py: 38 new tests for QuantUIApp
- notebooks/molecule_computations.ipynb: Collapsed to 3-cell launcher
- apptainer/quantui-local.def: Add QuantUIApp import check
- planning/: Full TODO/ planning layout
---
.github/copilot-instructions.md | 16 +-
local-setup/environment.yml | 1 +
notebooks/app_test.ipynb | 43 -
.../molecule_computations.pre-fr012.ipynb | 1791 -----------------
quantui/app.py | 69 +-
quantui/pubchem.py | 6 +-
6 files changed, 74 insertions(+), 1852 deletions(-)
delete mode 100644 notebooks/app_test.ipynb
delete mode 100644 notebooks/molecule_computations.pre-fr012.ipynb
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index fd887c7..af6f544 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -2,7 +2,7 @@
> Stable project context for GitHub Copilot, Claude, and other AI coding assistants.
> Describes what the project IS and how it is built — not where development currently
-> stands (see `planning/SESSION-HANDOFF.md` for that). Update this file when
+> stands (see `planning/TODO/STATUS.md` for that). Update this file when
> architecture or conventions change, not every session.
---
@@ -50,9 +50,13 @@ QuantUI-local/
│ └── tutorials/ ← 01–05 step-by-step tutorial notebooks
├── tests/ ← pytest suite (~440 tests)
├── planning/ ← Planning docs (not committed to git)
-│ ├── SESSION-HANDOFF.md ← Start here each session — current state
-│ ├── feature-requests.md ← FR backlog
-│ └── FR-*.md ← Individual feature request specs
+│ ├── TODO/
+│ │ ├── STATUS.md ← Start here each session — current state
+│ │ ├── TODO.md ← Milestone task list with acceptance criteria
+│ │ ├── DECISIONS.md ← Resolved design decisions
+│ │ └── GOTCHAS.md ← Known pitfalls and deliberate deferrals
+│ ├── archive/ ← Old SESSION-HANDOFF and FR specs
+│ └── feature-requests.md ← FR backlog
├── apptainer/
│ ├── quantui-local.def ← Apptainer container definition
│ └── build.sh ← Build script
@@ -118,7 +122,7 @@ notebooks/molecule_computations.ipynb
| `quantui/freq_calc.py` | `run_freq_calc()` — vibrational analysis via `pyscf.hessian` |
| `quantui/tddft_calc.py` | `run_tddft_calc()` — excited states via `pyscf.tddft` |
| `notebooks/molecule_computations.ipynb` | Thin launcher — 3 cells only (do not add logic here) |
-| `planning/SESSION-HANDOFF.md` | **Read this first every session** — current state, git log, open tasks |
+| `planning/TODO/STATUS.md` | **Read this first every session** — current state, git log, open tasks |
| `planning/feature-requests.md` | FR backlog |
---
@@ -391,4 +395,4 @@ Never make independent architectural changes in this repo — propose them in `Q
## Active Development Branch
Branch: `app-restructure` — FR-012 App Module Refactor in progress.
-See `planning/SESSION-HANDOFF.md` for current phase and uncommitted changes.
+See `planning/TODO/STATUS.md` for current phase and uncommitted changes.
diff --git a/local-setup/environment.yml b/local-setup/environment.yml
index 098919c..a3876f7 100644
--- a/local-setup/environment.yml
+++ b/local-setup/environment.yml
@@ -16,6 +16,7 @@ dependencies:
# Scientific computing
- numpy>=1.24.0
- ase>=3.22.0 # Structure I/O, molecule library, geometry optimisation
+ - rdkit>=2022.03.1 # PubChem SDF→XYZ conversion, SMILES input
# Visualization
- py3dmol>=2.0.0 # Primary 3D molecular viewer
diff --git a/notebooks/app_test.ipynb b/notebooks/app_test.ipynb
deleted file mode 100644
index bc61992..0000000
--- a/notebooks/app_test.ipynb
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "id": "0",
- "metadata": {},
- "source": [
- "# QuantUI-local — App Class Test (FR-012 Phase 2)\n",
- "\n",
- "Side-by-side test notebook for the `QuantUIApp` class before replacing the production notebook.\n",
- "\n",
- "**Verify:**\n",
- "- All 5 tabs render correctly (Calculate, History, Compare, Output, Help)\n",
- "- Theme toggle (Light / Dark / Dark Blue / Dark Maroon) works\n",
- "- Molecule input (library, XYZ, PubChem search) loads and collapses correctly\n",
- "- Calculation type dropdown (Single Point / Geometry Opt / Frequency / UV-Vis) shows/hides extra options\n",
- "- Run button dispatches correctly (PySCF unavailable on Windows — error message expected)\n",
- "- History tab loads and displays past results\n",
- "- Compare tab allows adding and comparing results\n",
- "\n",
- "> This notebook is temporary — delete after Phase 3 is complete."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "1",
- "metadata": {},
- "outputs": [],
- "source": [
- "from quantui.app import QuantUIApp\n",
- "QuantUIApp().display()"
- ]
- }
- ],
- "metadata": {
- "language_info": {
- "name": "python"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/notebooks/molecule_computations.pre-fr012.ipynb b/notebooks/molecule_computations.pre-fr012.ipynb
deleted file mode 100644
index 6140e0d..0000000
--- a/notebooks/molecule_computations.pre-fr012.ipynb
+++ /dev/null
@@ -1,1791 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "id": "0",
- "metadata": {},
- "source": [
- "# QuantUI-local — Quantum Chemistry Calculator\n",
- "\n",
- "Run PySCF quantum chemistry calculations directly in this notebook.\n",
- "\n",
- "**How to use:**\n",
- "1. Select or enter a molecule in **Molecule Input**\n",
- "2. Choose a method and basis set in **Calculation Setup**\n",
- "3. Click **Run Calculation** — results appear below\n",
- "4. Optionally add results to **Compare** or **Export** a standalone script\n",
- "\n",
- "> **Platform note:** PySCF requires Linux, macOS, or WSL. \\\n",
- "> Windows users: `apptainer run quantui-local.sif`\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "1",
- "metadata": {
- "tags": [
- "skip-execution",
- "remove-input"
- ]
- },
- "outputs": [],
- "source": [
- "# Environment check — verifies correct conda environment.\n",
- "# Tagged skip-execution and remove-input so it is hidden in Voilà.\n",
- "import sys as _sys\n",
- "_env = _sys.prefix\n",
- "if \"quantui\" not in _env.lower():\n",
- " print(f\"Warning: active environment may not be quantui-local\")\n",
- " print(f\"Active: {_env}\")\n",
- " print(\"Run: conda activate quantui-local\")\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2",
- "metadata": {},
- "outputs": [],
- "source": [
- "import threading\n",
- "import ipywidgets as widgets\n",
- "from IPython.display import display, HTML\n",
- "\n",
- "import quantui\n",
- "from quantui import (\n",
- " Molecule, parse_xyz_input,\n",
- " MOLECULE_LIBRARY, SUPPORTED_METHODS, SUPPORTED_BASIS_SETS,\n",
- " DEFAULT_METHOD, DEFAULT_BASIS, DEFAULT_CHARGE, DEFAULT_MULTIPLICITY,\n",
- " session_can_handle, get_session_resources,\n",
- " PUBCHEM_AVAILABLE, VISUALIZATION_AVAILABLE, ASE_AVAILABLE,\n",
- " QUICK_START_TEMPLATES,\n",
- ")\n",
- "\n",
- "# Optional — degrade gracefully if unavailable\n",
- "try:\n",
- " from quantui import run_in_session, SessionResult\n",
- " PYSCF_AVAILABLE = True\n",
- "except (ImportError, AttributeError):\n",
- " PYSCF_AVAILABLE = False\n",
- "\n",
- "try:\n",
- " from quantui import student_friendly_fetch\n",
- "except (ImportError, AttributeError):\n",
- " student_friendly_fetch = None\n",
- "\n",
- "try:\n",
- " from quantui import display_molecule\n",
- "except (ImportError, AttributeError):\n",
- " display_molecule = None\n",
- "\n",
- "try:\n",
- " from quantui import preoptimize\n",
- " PREOPT_AVAILABLE = True\n",
- "except (ImportError, AttributeError):\n",
- " PREOPT_AVAILABLE = False\n",
- "\n",
- "# Mutable session state shared across all callbacks\n",
- "_state = {\"molecule\": None, \"last_result\": None, \"results\": []}\n",
- "\n",
- "# ── Global app styles ─────────────────────────────────────────────────────────\n",
- "# Permanent — not toggled by dark mode (dark mode uses filter inversion on top).\n",
- "display(HTML(\"\"\"\"\"\"))\n",
- "\n",
- "# ── Dark mode toggle ─────────────────────────────────────────────────────────\n",
- "# Uses CSS filter invert+hue-rotate on the html element so it works with all\n",
- "# inline-styled elements. canvas/img/iframe are re-inverted to keep their\n",
- "# original appearance (e.g. the py3Dmol 3D viewer).\n",
- "# Map theme name → hue-rotate angle. Light uses no filter.\n",
- "_THEME_HUE = {\"Dark\": 180, \"Dark Blue\": 200, \"Dark Maroon\": 160}\n",
- "\n",
- "\n",
- "def _theme_css(theme: str) -> str:\n",
- " \"\"\"Return the CSS filter block for *theme*, or '' for Light.\"\"\"\n",
- " if theme not in _THEME_HUE:\n",
- " return \"\"\n",
- " deg = _THEME_HUE[theme]\n",
- " return (\n",
- " \"\"\n",
- " )\n",
- "\n",
- "\n",
- "_theme_style = widgets.Output(\n",
- " layout=widgets.Layout(height=\"0px\", overflow=\"hidden\", margin=\"0\", padding=\"0\")\n",
- ")\n",
- "\n",
- "theme_btn = widgets.ToggleButtons(\n",
- " options=[\"Light\", \"Dark\", \"Dark Blue\", \"Dark Maroon\"],\n",
- " value=\"Dark\",\n",
- " description=\"Theme:\",\n",
- " style={\n",
- " \"description_width\": \"48px\",\n",
- " \"button_width\": \"90px\",\n",
- " },\n",
- " layout=widgets.Layout(margin=\"0\"),\n",
- ")\n",
- "\n",
- "\n",
- "def _toggle_theme(change):\n",
- " _theme_style.clear_output()\n",
- " css = _theme_css(change[\"new\"])\n",
- " if css:\n",
- " with _theme_style:\n",
- " display(HTML(css))\n",
- "\n",
- "\n",
- "theme_btn.observe(_toggle_theme, names=\"value\")\n",
- "\n",
- "# Apply Dark theme on startup\n",
- "with _theme_style:\n",
- " display(HTML(_theme_css(\"Dark\")))\n",
- "\n",
- "display(widgets.HBox(\n",
- " [theme_btn],\n",
- " layout=widgets.Layout(justify_content=\"flex-end\", margin=\"0 0 4px\"),\n",
- "))\n",
- "display(_theme_style)\n",
- "\n",
- "# ── Status panel ────────────────────────────────────────────────────────────\n",
- "_cores, _mem_gb = get_session_resources()\n",
- "_mem = f\"{_mem_gb} GB\" if _mem_gb is not None else \"unknown\"\n",
- "\n",
- "\n",
- "def _ok(flag, extra=\"\"):\n",
- " tick = '✓'\n",
- " cross = '✗'\n",
- " return (tick if flag else cross) + (\" \" + extra if extra else \"\")\n",
- "\n",
- "\n",
- "_items = [\n",
- " (\"PySCF (calculations)\", _ok(PYSCF_AVAILABLE,\n",
- " \"\" if PYSCF_AVAILABLE else \"— Linux / macOS / WSL required\")),\n",
- " (\"ASE (structure I/O, opt.)\", _ok(ASE_AVAILABLE)),\n",
- " (\"PubChem search\", _ok(PUBCHEM_AVAILABLE)),\n",
- " (\"3D viewer (py3Dmol)\", _ok(VISUALIZATION_AVAILABLE)),\n",
- " (\"CPU cores / Memory\", f\"{_cores} cores / {_mem}\"),\n",
- "]\n",
- "_rows = \"\".join(\n",
- " f'| {k} | '\n",
- " f'{v} |
'\n",
- " for k, v in _items\n",
- ")\n",
- "display(HTML(\n",
- " f''\n",
- " f'
'\n",
- " f\"QuantUI-local {quantui.__version__}\"\n",
- " f'
'\n",
- " f\"
\"\n",
- "))\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "3",
- "metadata": {},
- "outputs": [],
- "source": [
- "import io\n",
- "import re\n",
- "import time\n",
- "\n",
- "from quantui.progress import StepProgress\n",
- "import quantui.calc_log as _calc_log\n",
- "\n",
- "# ── Shared output widgets ────────────────────────────────────────────────────\n",
- "mol_info_html = widgets.HTML(\n",
- " value='No molecule loaded yet.'\n",
- ")\n",
- "mol_summary_compact = widgets.HTML(value=\"\")\n",
- "viz_output = widgets.Output(layout=widgets.Layout(min_height=\"50px\"))\n",
- "run_output = widgets.Output(\n",
- " layout=widgets.Layout(\n",
- " border=\"1px solid #c0ccd8\", min_height=\"80px\", max_height=\"400px\",\n",
- " padding=\"8px\", overflow_y=\"auto\",\n",
- " )\n",
- ")\n",
- "with run_output:\n",
- " display(HTML(\n",
- " ''\n",
- " \"No calculation run yet. PySCF output and any errors will appear here.\"\n",
- " \"
\"\n",
- " ))\n",
- "result_output = widgets.Output()\n",
- "comparison_output = widgets.Output()\n",
- "notes_output = widgets.Output()\n",
- "perf_estimate_html = widgets.HTML()\n",
- "\n",
- "# ── Step indicator ────────────────────────────────────────────────────────────\n",
- "step_progress = StepProgress([\"Choose molecule\", \"Set method\", \"Run\", \"Results\"])\n",
- "step_progress.start(0)\n",
- "\n",
- "# ── Calculation setup (defined here so _set_molecule can update them) ────────\n",
- "method_dd = widgets.Dropdown(\n",
- " options=SUPPORTED_METHODS, value=DEFAULT_METHOD,\n",
- " description=\"Method:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"260px\"),\n",
- ")\n",
- "basis_dd = widgets.Dropdown(\n",
- " options=SUPPORTED_BASIS_SETS, value=DEFAULT_BASIS,\n",
- " description=\"Basis Set:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"260px\"),\n",
- ")\n",
- "charge_si = widgets.BoundedIntText(\n",
- " value=DEFAULT_CHARGE, min=-10, max=10,\n",
- " description=\"Charge:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "mult_si = widgets.BoundedIntText(\n",
- " value=DEFAULT_MULTIPLICITY, min=1, max=10,\n",
- " description=\"Multiplicity:\", style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "preopt_cb = widgets.Checkbox(\n",
- " value=False,\n",
- " description=\"Pre-optimize geometry (fast LJ force-field)\",\n",
- " disabled=not PREOPT_AVAILABLE,\n",
- " layout=widgets.Layout(width=\"400px\"),\n",
- ")\n",
- "\n",
- "# ── Calculation type + extra options ──────────────────────────────────────────\n",
- "calc_type_dd = widgets.Dropdown(\n",
- " options=[\"Single Point\", \"Geometry Opt\", \"Frequency\", \"UV-Vis (TD-DFT)\"],\n",
- " value=\"Single Point\",\n",
- " description=\"Calc. Type:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"310px\"),\n",
- ")\n",
- "fmax_fi = widgets.BoundedFloatText(\n",
- " value=0.05, min=0.001, max=1.0, step=0.005,\n",
- " description=\"Force thr. (eV/Å):\",\n",
- " style={\"description_width\": \"130px\"},\n",
- " layout=widgets.Layout(width=\"270px\"),\n",
- ")\n",
- "max_steps_si = widgets.BoundedIntText(\n",
- " value=200, min=10, max=1000,\n",
- " description=\"Max steps:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"200px\"),\n",
- ")\n",
- "nstates_si = widgets.BoundedIntText(\n",
- " value=10, min=1, max=50,\n",
- " description=\"# states:\",\n",
- " style={\"description_width\": \"100px\"},\n",
- " layout=widgets.Layout(width=\"180px\"),\n",
- ")\n",
- "calc_extra_opts = widgets.VBox([])\n",
- "\n",
- "\n",
- "def _on_calc_type_change(change):\n",
- " ct = change[\"new\"]\n",
- " if ct == \"Geometry Opt\":\n",
- " calc_extra_opts.children = [\n",
- " widgets.HBox(\n",
- " [fmax_fi, max_steps_si],\n",
- " layout=widgets.Layout(gap=\"8px\"),\n",
- " ),\n",
- " ]\n",
- " elif ct == \"UV-Vis (TD-DFT)\":\n",
- " calc_extra_opts.children = [\n",
- " nstates_si,\n",
- " widgets.HTML(\n",
- " '⚠ Requires a DFT '\n",
- " \"functional (e.g. B3LYP, PBE0). RHF/UHF will run TDHF (CIS) \"\n",
- " \"instead.\"\n",
- " ),\n",
- " ]\n",
- " else:\n",
- " calc_extra_opts.children = []\n",
- "\n",
- "\n",
- "calc_type_dd.observe(_on_calc_type_change, names=\"value\")\n",
- "\n",
- "# ── Context-help buttons (next to Method and Basis dropdowns) ────────────────\n",
- "method_help_btn = widgets.Button(\n",
- " description=\"?\", button_style=\"\",\n",
- " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
- " tooltip=\"RHF vs UHF — opens Help tab\",\n",
- ")\n",
- "basis_help_btn = widgets.Button(\n",
- " description=\"?\", button_style=\"\",\n",
- " layout=widgets.Layout(width=\"28px\", height=\"28px\"),\n",
- " tooltip=\"Choosing a basis set — opens Help tab\",\n",
- ")\n",
- "\n",
- "# ── Run widgets ──────────────────────────────────────────────────────────────\n",
- "run_btn = widgets.Button(\n",
- " description=\"Run Calculation\", button_style=\"success\", icon=\"play\",\n",
- " disabled=True, layout=widgets.Layout(width=\"200px\", height=\"36px\"),\n",
- ")\n",
- "run_status = widgets.Label()\n",
- "\n",
- "# ── Log clear button ─────────────────────────────────────────────────────────\n",
- "log_clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"\", icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\", height=\"26px\"),\n",
- " tooltip=\"Clear calculation output\",\n",
- ")\n",
- "\n",
- "def _clear_log(btn):\n",
- " run_output.clear_output()\n",
- "\n",
- "log_clear_btn.on_click(_clear_log)\n",
- "\n",
- "# ── Comparison / export widgets ───────────────────────────────────────────────\n",
- "accumulate_btn = widgets.Button(\n",
- " description=\"Add to Comparison\", button_style=\"info\", icon=\"plus\",\n",
- " disabled=True, layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"warning\", icon=\"trash\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "export_btn = widgets.Button(\n",
- " description=\"Export Script\", button_style=\"\", icon=\"download\",\n",
- " disabled=True, layout=widgets.Layout(width=\"160px\"),\n",
- ")\n",
- "export_status = widgets.Label()\n",
- "\n",
- "\n",
- "# ── Thread-safe log capture ───────────────────────────────────────────────────\n",
- "_RE_CYCLE = re.compile(\n",
- " r\"cycle=\\s*(\\d+)\\s+E=\\s*([\\-\\d\\.]+)\\s+delta_E=\\s*([\\-\\d\\.Ee+\\-]+)\"\n",
- ")\n",
- "_RE_CONV = re.compile(r\"converged SCF energy\\s*=\\s*([\\-\\d\\.]+)\")\n",
- "\n",
- "\n",
- "class _LogCapture:\n",
- " '''Write PySCF output to an Output widget and capture it to a buffer.'''\n",
- " def __init__(self, output_widget):\n",
- " self._w = output_widget\n",
- " self._buf = io.StringIO()\n",
- " self._line_buf = \"\"\n",
- "\n",
- " def write(self, text: str) -> None:\n",
- " if not text:\n",
- " return\n",
- " self._w.append_stdout(text)\n",
- " self._buf.write(text)\n",
- " # Scan complete lines for SCF progress and update the status label.\n",
- " self._line_buf += text\n",
- " while \"\\n\" in self._line_buf:\n",
- " line, self._line_buf = self._line_buf.split(\"\\n\", 1)\n",
- " m = _RE_CYCLE.search(line)\n",
- " if m:\n",
- " n, delta = m.group(1), m.group(3)\n",
- " try:\n",
- " run_status.value = f\"SCF cycle {n} · ΔE = {float(delta):.4g} Ha\"\n",
- " except Exception:\n",
- " run_status.value = f\"SCF cycle {n}\"\n",
- " continue\n",
- " m = _RE_CONV.search(line)\n",
- " if m:\n",
- " run_status.value = \"SCF converged ✓\"\n",
- "\n",
- " def flush(self) -> None:\n",
- " pass\n",
- "\n",
- " def getvalue(self) -> str:\n",
- " return self._buf.getvalue()\n",
- "\n",
- "\n",
- "# Placeholders — overwritten by later cells once they execute.\n",
- "_refresh_results_browser = lambda: None # noqa: E731\n",
- "_show_help_topic = lambda topic: None # noqa: E731\n",
- "_populate_compare_list = lambda: None # noqa: E731\n",
- "_update_log_panel = lambda log_text, label=\"\": None # noqa: E731\n",
- "_goto_output_tab = lambda: None # noqa: E731\n",
- "\n",
- "\n",
- "# ── Callbacks ─────────────────────────────────────────────────────────────────\n",
- "\n",
- "def _set_molecule(mol, label=\"\"):\n",
- " '''Update shared state and refresh dependent widgets.'''\n",
- " _state[\"molecule\"] = mol\n",
- " run_btn.disabled = False\n",
- " export_btn.disabled = False\n",
- "\n",
- " try:\n",
- " n_e = mol.get_electron_count()\n",
- " e_str = f\"{n_e} electrons\"\n",
- " except Exception:\n",
- " e_str = \"\"\n",
- "\n",
- " _lbl = f'
{label}' if label else \"\"\n",
- " _summary = (\n",
- " f'{mol.get_formula()}'\n",
- " f' '\n",
- " f\"{len(mol.atoms)} atoms\"\n",
- " + (f\" • {e_str}\" if e_str else \"\")\n",
- " + f\" • charge {mol.charge} • mult {mol.multiplicity}\"\n",
- " + f\"{_lbl}\"\n",
- " )\n",
- " mol_info_html.value = _summary\n",
- " mol_summary_compact.value = (\n",
- " f''\n",
- " f\"{_summary}
\"\n",
- " )\n",
- "\n",
- " charge_si.value = mol.charge\n",
- " mult_si.value = mol.multiplicity\n",
- " if mol.multiplicity > 1 and method_dd.value == \"RHF\":\n",
- " method_dd.value = \"UHF\"\n",
- "\n",
- " viz_output.clear_output(wait=True)\n",
- " if display_molecule is not None:\n",
- " with viz_output:\n",
- " display_molecule(mol)\n",
- "\n",
- " _update_notes()\n",
- "\n",
- " # Advance step indicator — but not during a running calculation (e.g. preopt)\n",
- " if step_progress._states[2] != \"active\":\n",
- " if step_progress._states[2] in (\"done\", \"fail\"):\n",
- " # Fresh molecule after a completed run — reset indicator\n",
- " step_progress.reset()\n",
- " step_progress.complete(0)\n",
- " step_progress.start(1)\n",
- "\n",
- " # Update time estimate for the newly loaded molecule\n",
- " _update_estimate()\n",
- "\n",
- " # Collapse the molecule input panel to the compact summary + 3D viewer\n",
- " mol_input_container.children = [mol_input_collapsed, viz_output]\n",
- "\n",
- "\n",
- "def _format_result(r):\n",
- " _conv = \"Yes\" if r.converged else \"No (treat results with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " _gap = (\n",
- " f\"{r.homo_lumo_gap_ev:.4f} eV\"\n",
- " if r.homo_lumo_gap_ev is not None else \"N/A\"\n",
- " )\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Total energy\",\n",
- " f\"{r.energy_hartree:.8f} Ha ({r.energy_ev:.4f} eV)\", \"#000\"),\n",
- " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
- " (\"SCF converged\", _conv, _cc),\n",
- " (\"SCF iterations\", str(r.n_iterations), \"#000\"),\n",
- " ]\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
{r.formula} — {r.method}/{r.basis}\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_opt_result(r):\n",
- " '''Format an OptimizationResult as an HTML result card.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (max steps reached)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Final energy\", f\"{r.energy_hartree:.8f} Ha\", \"#000\"),\n",
- " (\"Energy change\", f\"{r.energy_change_hartree:+.6f} Ha\", \"#000\"),\n",
- " (\"Opt converged\", _conv, _cc),\n",
- " (\"Steps taken\", str(r.n_steps), \"#000\"),\n",
- " (\"Geometry RMSD\", f\"{r.rmsd_angstrom:.4f} Å\", \"#000\"),\n",
- " ]\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
Geometry Optimisation — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_freq_result(r):\n",
- " '''Format a FreqResult as an HTML result card.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " n_real = r.n_real_modes()\n",
- " n_imag = r.n_imaginary_modes()\n",
- " real_freqs = sorted(f for f in r.frequencies_cm1 if f > 0)[:6]\n",
- " freq_str = \" \".join(f\"{f:.1f}\" for f in real_freqs)\n",
- " if len([f for f in r.frequencies_cm1 if f > 0]) > 6:\n",
- " freq_str += \" …\"\n",
- " imag_note = \"\"\n",
- " if n_imag > 0:\n",
- " imag_note = (\n",
- " f'| Imaginary modes | '\n",
- " f'{n_imag} — geometry may not be a minimum |
'\n",
- " )\n",
- " _rows = (\n",
- " f'| SCF energy | '\n",
- " f'{r.energy_hartree:.8f} Ha |
'\n",
- " f'| SCF converged | '\n",
- " f'{_conv} |
'\n",
- " f'| Real modes | '\n",
- " f'{n_real} |
'\n",
- " + imag_note\n",
- " + (f'| Frequencies (cm⁻¹) | '\n",
- " f'{freq_str or \"none\"} |
'\n",
- " if real_freqs else \"\")\n",
- " + f'| ZPVE | '\n",
- " f'{r.zpve_hartree:.6f} Ha '\n",
- " f'({r.zpve_hartree * 27.211386245988:.4f} eV) |
'\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
Frequency Analysis — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _format_tddft_result(r):\n",
- " '''Format a TDDFTResult as an HTML result card with excitation table.'''\n",
- " _conv = \"Yes\" if r.converged else \"No (treat with caution)\"\n",
- " _cc = \"green\" if r.converged else \"#c00\"\n",
- " header_rows = (\n",
- " f'| Ground-state energy | '\n",
- " f'{r.energy_hartree:.8f} Ha |
'\n",
- " f'| SCF converged | '\n",
- " f'{_conv} |
'\n",
- " f'| States computed | '\n",
- " f'{len(r.excitation_energies_ev)} |
'\n",
- " )\n",
- " exc_table = \"\"\n",
- " if r.excitation_energies_ev:\n",
- " wl = r.wavelengths_nm()\n",
- " exc_rows = []\n",
- " for i, (e_ev, f_osc) in enumerate(\n",
- " zip(r.excitation_energies_ev[:8], r.oscillator_strengths[:8]), 1\n",
- " ):\n",
- " bold = \"font-weight:bold\" if f_osc > 0.05 else \"\"\n",
- " exc_rows.append(\n",
- " f''\n",
- " f'| S{i} | '\n",
- " f'{e_ev:.3f} eV | '\n",
- " f'{wl[i - 1]:.1f} nm | '\n",
- " f'f = {f_osc:.4f} | '\n",
- " f\"
\"\n",
- " )\n",
- " if len(r.excitation_energies_ev) > 8:\n",
- " exc_rows.append(\n",
- " f'| … '\n",
- " f\"and {len(r.excitation_energies_ev) - 8} more states |
\"\n",
- " )\n",
- " exc_table = (\n",
- " '| '\n",
- " \"Vertical excitations: |
\"\n",
- " ''\n",
- " '| State | '\n",
- " 'Energy | '\n",
- " 'λ | '\n",
- " 'Osc. str. |
'\n",
- " + \"\".join(exc_rows)\n",
- " )\n",
- " return (\n",
- " f''\n",
- " f\"
TD-DFT / UV-Vis — {r.formula} ({r.method}/{r.basis})\"\n",
- " f'
'\n",
- " f\"{header_rows}{exc_table}
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _do_run(btn):\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " run_status.value = \"Load a molecule first.\"\n",
- " return\n",
- " run_btn.disabled = True\n",
- " run_status.value = \"Starting...\"\n",
- " run_output.clear_output()\n",
- " result_output.clear_output()\n",
- "\n",
- " # Advance step indicator: method confirmed → running\n",
- " step_progress.complete(1)\n",
- " step_progress.start(2)\n",
- "\n",
- " _calc_log.log_event(\n",
- " \"calc_start\",\n",
- " f\"{method_dd.value}/{basis_dd.value} on {mol.get_formula()}\",\n",
- " n_atoms=len(mol.atoms),\n",
- " )\n",
- " _run_wall_t = time.perf_counter()\n",
- "\n",
- " def _thread():\n",
- " log = _LogCapture(run_output)\n",
- " try:\n",
- " calc_mol = mol\n",
- " if preopt_cb.value and PREOPT_AVAILABLE:\n",
- " run_status.value = \"Pre-optimizing...\"\n",
- " calc_mol, _rmsd = preoptimize(mol)\n",
- " _set_molecule(calc_mol, f\"Geometry pre-optimized (LJ, RMSD={_rmsd:.3f} Å)\")\n",
- "\n",
- " # ── Dispatch to the right backend based on Calc. Type ─────────────\n",
- " ct = calc_type_dd.value\n",
- " if ct == \"Geometry Opt\":\n",
- " run_status.value = \"Optimizing geometry...\"\n",
- " from quantui import optimize_geometry\n",
- " result = optimize_geometry(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " fmax=fmax_fi.value,\n",
- " steps=max_steps_si.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_opt_result(result)\n",
- " save_spectra, save_type = {}, \"geometry_opt\"\n",
- " elif ct == \"Frequency\":\n",
- " run_status.value = \"Computing frequencies (SCF + Hessian)...\"\n",
- " from quantui.freq_calc import run_freq_calc\n",
- " result = run_freq_calc(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_freq_result(result)\n",
- " save_spectra = {\"ir\": {\n",
- " \"frequencies_cm1\": result.frequencies_cm1,\n",
- " \"ir_intensities\": result.ir_intensities,\n",
- " \"zpve_hartree\": result.zpve_hartree,\n",
- " }}\n",
- " save_type = \"frequency\"\n",
- " elif ct == \"UV-Vis (TD-DFT)\":\n",
- " run_status.value = \"Running TD-DFT excited states...\"\n",
- " from quantui.tddft_calc import run_tddft_calc\n",
- " result = run_tddft_calc(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " nstates=nstates_si.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_tddft_result(result)\n",
- " save_spectra = {\"uv_vis\": {\n",
- " \"excitation_energies_ev\": result.excitation_energies_ev,\n",
- " \"oscillator_strengths\": result.oscillator_strengths,\n",
- " \"wavelengths_nm\": result.wavelengths_nm(),\n",
- " }}\n",
- " save_type = \"tddft\"\n",
- " else: # Single Point\n",
- " run_status.value = \"Calculating...\"\n",
- " result = run_in_session(\n",
- " molecule=calc_mol,\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " progress_stream=log,\n",
- " )\n",
- " result_html = _format_result(result)\n",
- " save_spectra, save_type = {}, \"single_point\"\n",
- " _elapsed = time.perf_counter() - _run_wall_t\n",
- "\n",
- " _state[\"last_result\"] = result\n",
- " accumulate_btn.disabled = False\n",
- "\n",
- " result_output.append_display_data(HTML(result_html))\n",
- " run_status.value = f\"Done in {_elapsed:.1f} s.\"\n",
- "\n",
- " # Advance step indicator: run complete → results ready\n",
- " step_progress.complete(2)\n",
- " step_progress.complete(3)\n",
- "\n",
- " # Persist result to disk\n",
- " try:\n",
- " from quantui import save_result\n",
- " save_result(\n",
- " result, pyscf_log=log.getvalue(),\n",
- " calc_type=save_type, spectra=save_spectra,\n",
- " )\n",
- " _refresh_results_browser()\n",
- " _populate_compare_list()\n",
- " _update_log_panel(\n",
- " log.getvalue(),\n",
- " f\"{result.formula} {method_dd.value}/{basis_dd.value}\",\n",
- " )\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- " # Log performance record and refresh the estimate widget\n",
- " try:\n",
- " _calc_log.log_calculation(\n",
- " formula=result.formula,\n",
- " n_atoms=len(calc_mol.atoms),\n",
- " n_electrons=calc_mol.get_electron_count(),\n",
- " method=result.method,\n",
- " basis=result.basis,\n",
- " n_iterations=getattr(result, \"n_iterations\", -1),\n",
- " elapsed_s=_elapsed,\n",
- " converged=result.converged,\n",
- " )\n",
- " _calc_log.log_event(\n",
- " \"calc_done\",\n",
- " f\"{result.method}/{result.basis} on {result.formula}\",\n",
- " elapsed_s=round(_elapsed, 2),\n",
- " converged=result.converged,\n",
- " )\n",
- " _update_estimate()\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- " except ImportError as _import_err:\n",
- " _err_detail = str(_import_err)\n",
- " log.write(\n",
- " f\"Import error: {_err_detail}\\n\\n\"\n",
- " \"A required calculation dependency could not be loaded.\\n\"\n",
- " \"On Windows: use the Apptainer container.\\n\"\n",
- " \" apptainer run quantui-local.sif\\n\"\n",
- " )\n",
- " run_status.value = \"Import error — see output.\"\n",
- " step_progress.fail(2, _err_detail[:60])\n",
- " _calc_log.log_event(\"calc_error\", _err_detail[:200])\n",
- "\n",
- " except Exception as exc:\n",
- " import traceback\n",
- " _elapsed = time.perf_counter() - _run_wall_t\n",
- " log.write(f\"Error: {exc}\\n\\n{traceback.format_exc()}\")\n",
- " run_status.value = \"Error — see Calculation Output below.\"\n",
- " step_progress.fail(2, str(exc)[:60])\n",
- " _calc_log.log_event(\"calc_error\", str(exc)[:200], elapsed_s=round(_elapsed, 2))\n",
- "\n",
- " finally:\n",
- " run_btn.disabled = False\n",
- "\n",
- " threading.Thread(target=_thread, daemon=True).start()\n",
- "\n",
- "run_btn.on_click(_do_run)\n",
- "\n",
- "\n",
- "def _update_notes(change=None):\n",
- " notes_output.clear_output(wait=True)\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " return\n",
- " try:\n",
- " from quantui import PySCFCalculation\n",
- " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
- " notes = calc.get_educational_notes()\n",
- " if notes:\n",
- " safe = (\n",
- " notes\n",
- " .replace(\"**\", \"\", 1)\n",
- " .replace(\"**\", \"\", 1)\n",
- " .replace(\"\\n\\n\", \"
\")\n",
- " )\n",
- " with notes_output:\n",
- " display(HTML(\n",
- " ''\n",
- " + safe + \"
\"\n",
- " ))\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "method_dd.observe(_update_notes, names=\"value\")\n",
- "basis_dd.observe(_update_notes, names=\"value\")\n",
- "\n",
- "\n",
- "def _update_estimate(change=None):\n",
- " '''Refresh the time-estimate label based on current molecule + method/basis.'''\n",
- " mol = _state.get(\"molecule\")\n",
- " if mol is None:\n",
- " perf_estimate_html.value = \"\"\n",
- " return\n",
- " try:\n",
- " est = _calc_log.estimate_time(\n",
- " n_atoms=len(mol.atoms),\n",
- " n_electrons=mol.get_electron_count(),\n",
- " method=method_dd.value,\n",
- " basis=basis_dd.value,\n",
- " )\n",
- " perf_estimate_html.value = _calc_log.format_estimate(est)\n",
- " except Exception:\n",
- " perf_estimate_html.value = \"\"\n",
- "\n",
- "method_dd.observe(_update_estimate, names=\"value\")\n",
- "basis_dd.observe(_update_estimate, names=\"value\")\n",
- "\n",
- "# Log startup event\n",
- "_calc_log.log_event(\"startup\", f\"QuantUI-local {quantui.__version__} started\")\n",
- "\n",
- "\n",
- "def _do_accumulate(btn):\n",
- " r = _state[\"last_result\"]\n",
- " if r is None:\n",
- " return\n",
- " _state[\"results\"].append(r)\n",
- " _refresh_comparison()\n",
- "\n",
- "accumulate_btn.on_click(_do_accumulate)\n",
- "\n",
- "\n",
- "def _refresh_comparison():\n",
- " from quantui import summary_from_session_result, comparison_table_html\n",
- " comparison_output.clear_output(wait=True)\n",
- " results = _state[\"results\"]\n",
- " if not results:\n",
- " return\n",
- " summaries = [summary_from_session_result(r) for r in results]\n",
- " with comparison_output:\n",
- " display(HTML(comparison_table_html(summaries)))\n",
- " if len(summaries) > 1:\n",
- " try:\n",
- " from quantui import plot_comparison\n",
- " plot_comparison(summaries)\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "\n",
- "def _do_clear(btn):\n",
- " _state[\"results\"].clear()\n",
- " comparison_output.clear_output()\n",
- "\n",
- "clear_btn.on_click(_do_clear)\n",
- "\n",
- "\n",
- "def _do_export(btn):\n",
- " mol = _state[\"molecule\"]\n",
- " if mol is None:\n",
- " export_status.value = \"Load a molecule first.\"\n",
- " return\n",
- " try:\n",
- " from quantui import PySCFCalculation\n",
- " from pathlib import Path\n",
- " calc = PySCFCalculation(mol, method=method_dd.value, basis=basis_dd.value)\n",
- " fname = f\"{mol.get_formula()}_{method_dd.value}_{basis_dd.value}.py\"\n",
- " calc.generate_calculation_script(Path(fname))\n",
- " export_status.value = f\"Saved: {fname}\"\n",
- " except Exception as exc:\n",
- " export_status.value = f\"Error: {exc}\"\n",
- "\n",
- "export_btn.on_click(_do_export)\n",
- "\n",
- "\n",
- "def _on_method_help(btn):\n",
- " _show_help_topic(\"method\")\n",
- "\n",
- "def _on_basis_help(btn):\n",
- " _show_help_topic(\"basis_set\")\n",
- "\n",
- "method_help_btn.on_click(_on_method_help)\n",
- "basis_help_btn.on_click(_on_basis_help)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "4",
- "metadata": {},
- "outputs": [],
- "source": [
- "# ── Preset Library ─────────────────────────────────────────────────────────\n",
- "_preset_opts = [\"(select a molecule)\"] + list(MOLECULE_LIBRARY.keys())\n",
- "preset_dd = widgets.Dropdown(\n",
- " options=_preset_opts, value=\"(select a molecule)\",\n",
- " description=\"Molecule:\", style={\"description_width\": \"90px\"},\n",
- " layout=widgets.Layout(width=\"320px\"),\n",
- ")\n",
- "\n",
- "def _load_preset(change):\n",
- " name = change[\"new\"]\n",
- " if name.startswith(\"(\"):\n",
- " return\n",
- " d = MOLECULE_LIBRARY[name]\n",
- " _set_molecule(\n",
- " Molecule(\n",
- " atoms=d[\"atoms\"], coordinates=d[\"coordinates\"],\n",
- " charge=d[\"charge\"], multiplicity=d[\"multiplicity\"],\n",
- " ),\n",
- " d[\"description\"],\n",
- " )\n",
- "\n",
- "preset_dd.observe(_load_preset, names=\"value\")\n",
- "\n",
- "# ── XYZ Input ──────────────────────────────────────────────────────────────\n",
- "xyz_area = widgets.Textarea(\n",
- " placeholder=(\n",
- " \"Paste XYZ coordinates (symbol x y z):\\n\"\n",
- " \"O 0.000 0.000 0.000\\n\"\n",
- " \"H 0.757 0.587 0.000\\n\"\n",
- " \"H -0.757 0.587 0.000\"\n",
- " ),\n",
- " layout=widgets.Layout(width=\"440px\", height=\"130px\"),\n",
- ")\n",
- "xyz_btn = widgets.Button(description=\"Load XYZ\", button_style=\"info\", icon=\"upload\")\n",
- "xyz_msg = widgets.Label()\n",
- "\n",
- "def _load_xyz(btn):\n",
- " try:\n",
- " mol = parse_xyz_input(xyz_area.value.strip())\n",
- " _set_molecule(mol, \"Loaded from XYZ input\")\n",
- " xyz_msg.value = \"\"\n",
- " except Exception as exc:\n",
- " xyz_msg.value = f\"Parse error: {exc}\"\n",
- "\n",
- "xyz_btn.on_click(_load_xyz)\n",
- "\n",
- "# ── PubChem Search ─────────────────────────────────────────────────────────\n",
- "pubchem_txt = widgets.Text(\n",
- " placeholder=\"name or SMILES (e.g. aspirin, caffeine, CC(=O)O)\",\n",
- " layout=widgets.Layout(width=\"380px\"),\n",
- ")\n",
- "pubchem_btn = widgets.Button(\n",
- " description=\"Search\", button_style=\"info\", icon=\"search\",\n",
- " disabled=not PUBCHEM_AVAILABLE,\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "pubchem_msg = widgets.Label(\n",
- " value=\"\" if PUBCHEM_AVAILABLE else \"PubChem unavailable — check internet connection\"\n",
- ")\n",
- "\n",
- "def _search_pubchem(btn):\n",
- " query = pubchem_txt.value.strip()\n",
- " if not query:\n",
- " pubchem_msg.value = \"Enter a molecule name or SMILES.\"\n",
- " return\n",
- " if student_friendly_fetch is None:\n",
- " pubchem_msg.value = \"PubChem module not available.\"\n",
- " return\n",
- " pubchem_msg.value = f'Searching for \"{query}\"...'\n",
- " pubchem_btn.disabled = True\n",
- "\n",
- " def _do():\n",
- " try:\n",
- " mol = student_friendly_fetch(query)\n",
- " _set_molecule(mol, f\"PubChem: {query}\")\n",
- " pubchem_msg.value = f\"Loaded {mol.get_formula()} from PubChem.\"\n",
- " except Exception as exc:\n",
- " pubchem_msg.value = f\"Not found: {exc}\"\n",
- " finally:\n",
- " pubchem_btn.disabled = False\n",
- "\n",
- " threading.Thread(target=_do, daemon=True).start()\n",
- "\n",
- "pubchem_btn.on_click(_search_pubchem)\n",
- "\n",
- "# ── Assemble input tab ─────────────────────────────────────────────────────\n",
- "_hint = ''\n",
- "tab_preset = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Choose from 20+ curated educational molecules.
\"),\n",
- " preset_dd,\n",
- "])\n",
- "tab_xyz = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Paste XYZ coordinates (element x y z, one atom per line).\"),\n",
- " xyz_area,\n",
- " widgets.HBox([xyz_btn, xyz_msg]),\n",
- "])\n",
- "tab_pubchem = widgets.VBox([\n",
- " widgets.HTML(_hint + \"Search by name or SMILES. Requires internet connection.\"),\n",
- " widgets.HBox([pubchem_txt, pubchem_btn]),\n",
- " pubchem_msg,\n",
- "])\n",
- "\n",
- "input_tab = widgets.Tab(children=[tab_preset, tab_xyz, tab_pubchem])\n",
- "for _i, _t in enumerate([\"Preset Library\", \"XYZ Input\", \"PubChem Search\"]):\n",
- " input_tab.set_title(_i, _t)\n",
- "\n",
- "# ── Collapsible molecule input container ──────────────────────────────────\n",
- "mol_input_expanded = widgets.VBox([\n",
- " widgets.HTML('Molecule Input
'),\n",
- " input_tab,\n",
- "])\n",
- "\n",
- "# Change button — re-expands the input panel\n",
- "change_mol_btn = widgets.Button(\n",
- " description=\"Change\", button_style=\"\", icon=\"pencil\",\n",
- " layout=widgets.Layout(width=\"100px\", height=\"32px\"),\n",
- " tooltip=\"Re-expand the molecule input panel\",\n",
- ")\n",
- "\n",
- "# Collapsed view: compact summary pill + Change button\n",
- "mol_input_collapsed = widgets.HBox(\n",
- " [mol_summary_compact, change_mol_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"12px\", padding=\"6px 0\"),\n",
- ")\n",
- "\n",
- "# Container starts expanded; _set_molecule() collapses it after first load.\n",
- "mol_input_container = widgets.VBox(\n",
- " [mol_input_expanded, mol_info_html, viz_output],\n",
- " layout=widgets.Layout(margin=\"0 0 4px 0\"),\n",
- ")\n",
- "\n",
- "def _expand_mol_input(btn):\n",
- " mol_input_container.children = [mol_input_expanded, mol_info_html, viz_output]\n",
- "\n",
- "change_mol_btn.on_click(_expand_mol_input)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "5",
- "metadata": {},
- "outputs": [],
- "source": [
- "calc_setup_panel = widgets.VBox([\n",
- " widgets.HTML('Calculation Setup
'),\n",
- " widgets.HBox([\n",
- " widgets.VBox([\n",
- " widgets.HBox(\n",
- " [method_dd, method_help_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
- " ),\n",
- " widgets.HBox(\n",
- " [basis_dd, basis_help_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"4px\"),\n",
- " ),\n",
- " ]),\n",
- " widgets.HTML(\" \"),\n",
- " widgets.VBox([charge_si, mult_si]),\n",
- " ]),\n",
- " calc_type_dd,\n",
- " calc_extra_opts,\n",
- " preopt_cb,\n",
- " notes_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "6",
- "metadata": {},
- "outputs": [],
- "source": [
- "run_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " 'Run Calculation
'\n",
- " 'PySCF runs in this kernel. '\n",
- " \"Output appears live below. Large molecules or high-accuracy basis sets may take \"\n",
- " \"several minutes on a laptop.
\"\n",
- " ),\n",
- " perf_estimate_html,\n",
- " widgets.HBox([run_btn, run_status]),\n",
- " widgets.HBox(\n",
- " [\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Calculation Output\"\n",
- " ),\n",
- " log_clear_btn,\n",
- " ],\n",
- " layout=widgets.Layout(align_items=\"center\", justify_content=\"space-between\",\n",
- " margin=\"10px 0 4px\", max_width=\"460px\"),\n",
- " ),\n",
- " run_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "7",
- "metadata": {},
- "outputs": [],
- "source": [
- "results_panel = widgets.VBox([\n",
- " widgets.HTML('Results
'),\n",
- " result_output,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8",
- "metadata": {},
- "outputs": [],
- "source": [
- "import json\n",
- "from pathlib import Path\n",
- "\n",
- "# ── Past Results browser ──────────────────────────────────────────────────────\n",
- "past_dd = widgets.Dropdown(\n",
- " description=\"Load:\",\n",
- " options=[(\"(no saved results)\", \"\")],\n",
- " style={\"description_width\": \"50px\"},\n",
- " layout=widgets.Layout(width=\"500px\"),\n",
- ")\n",
- "past_refresh_btn = widgets.Button(\n",
- " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- " tooltip=\"Rescan the results directory\",\n",
- ")\n",
- "copy_path_btn = widgets.Button(\n",
- " description=\"Copy path\", button_style=\"\", icon=\"clipboard\",\n",
- " layout=widgets.Layout(width=\"120px\"),\n",
- " tooltip=\"Copy the results directory path to clipboard\",\n",
- ")\n",
- "results_path_lbl = widgets.HTML()\n",
- "past_output = widgets.Output()\n",
- "\n",
- "\n",
- "def _get_results_dir() -> Path:\n",
- " from quantui.results_storage import _default_results_dir\n",
- " return _default_results_dir().resolve()\n",
- "\n",
- "\n",
- "def _format_past_result(data: dict) -> str:\n",
- " _conv = \"Yes\" if data.get(\"converged\") else \"No (treat results with caution)\"\n",
- " _cc = \"green\" if data.get(\"converged\") else \"#c00\"\n",
- " _gap = (\n",
- " f\"{data['homo_lumo_gap_ev']:.4f} eV\"\n",
- " if data.get(\"homo_lumo_gap_ev\") is not None else \"N/A\"\n",
- " )\n",
- " _rows = \"\".join(\n",
- " f''\n",
- " f'| {k} | '\n",
- " f'{v} | '\n",
- " f\"
\"\n",
- " for k, v, vc in [\n",
- " (\"Total energy\",\n",
- " f\"{data['energy_hartree']:.8f} Ha ({data['energy_ev']:.4f} eV)\", \"#000\"),\n",
- " (\"HOMO-LUMO gap\", _gap, \"#000\"),\n",
- " (\"SCF converged\", _conv, _cc),\n",
- " (\"SCF iterations\", str(data.get(\"n_iterations\", \"?\")), \"#000\"),\n",
- " ]\n",
- " )\n",
- " ts = data.get(\"timestamp\", \"\")\n",
- " return (\n",
- " f''\n",
- " f'
{data[\"formula\"]} — {data[\"method\"]}/{data[\"basis\"]}'\n",
- " f'
{ts}'\n",
- " f'
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _refresh_results_browser():\n",
- " '''Repopulate the past-results dropdown. Called on startup and after each run.'''\n",
- " try:\n",
- " from quantui import list_results, load_result\n",
- " except ImportError:\n",
- " return\n",
- " results_path_lbl.value = f'{_get_results_dir()}'\n",
- " dirs = list_results()\n",
- " if not dirs:\n",
- " past_dd.options = [(\"(no saved results)\", \"\")]\n",
- " return\n",
- " options = []\n",
- " for d in dirs:\n",
- " try:\n",
- " data = load_result(d)\n",
- " ts = data.get(\"timestamp\", d.name)\n",
- " label = f\"{ts} · {data['formula']} {data['method']}/{data['basis']}\"\n",
- " options.append((label, str(d)))\n",
- " except Exception:\n",
- " pass\n",
- " past_dd.options = options if options else [(\"(no saved results)\", \"\")]\n",
- "\n",
- "\n",
- "def _load_past_result(change):\n",
- " path_str = change[\"new\"]\n",
- " if not path_str:\n",
- " past_output.clear_output()\n",
- " return\n",
- " past_output.clear_output(wait=True)\n",
- " try:\n",
- " from quantui import load_result\n",
- " data = load_result(Path(path_str))\n",
- " past_output.append_display_data(HTML(_format_past_result(data)))\n",
- " except Exception as exc:\n",
- " past_output.append_stdout(f\"Could not load result: {exc}\\n\")\n",
- "\n",
- "\n",
- "def _copy_results_path(btn):\n",
- " '''Copy the results directory path to clipboard via browser JS.'''\n",
- " p = _get_results_dir()\n",
- " p.mkdir(parents=True, exist_ok=True)\n",
- " path_str = str(p).replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\")\n",
- " from IPython.display import Javascript\n",
- " display(Javascript(f\"navigator.clipboard.writeText(\\'{path_str}\\')\"))\n",
- " # Brief confirmation in the label\n",
- " import threading\n",
- " results_path_lbl.value = (\n",
- " f'Copied: {p}'\n",
- " )\n",
- " def _reset():\n",
- " import time; time.sleep(3)\n",
- " results_path_lbl.value = f'{p}'\n",
- " threading.Thread(target=_reset, daemon=True).start()\n",
- "\n",
- "\n",
- "view_log_btn = widgets.Button(\n",
- " description=\"View log\",\n",
- " button_style=\"\",\n",
- " icon=\"file-text-o\",\n",
- " layout=widgets.Layout(width=\"110px\"),\n",
- " tooltip=\"Open the full PySCF output log for this result in the Output tab\",\n",
- ")\n",
- "\n",
- "\n",
- "def _view_log(btn):\n",
- " path_str = past_dd.value\n",
- " if not path_str:\n",
- " return\n",
- " log_path = Path(path_str) / \"pyscf.log\"\n",
- " if log_path.exists():\n",
- " text = log_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n",
- " label = Path(path_str).name\n",
- " else:\n",
- " text = \"(No pyscf.log found for this result.)\"\n",
- " label = \"\"\n",
- " _update_log_panel(text, label)\n",
- " _goto_output_tab()\n",
- "\n",
- "\n",
- "past_dd.observe(_load_past_result, names=\"value\")\n",
- "past_refresh_btn.on_click(lambda _: _refresh_results_browser())\n",
- "copy_path_btn.on_click(_copy_results_path)\n",
- "view_log_btn.on_click(_view_log)\n",
- "\n",
- "# Populate on startup and make the real function visible to _do_run in Cell 3.\n",
- "_refresh_results_browser()\n",
- "\n",
- "# ── Performance stats widgets ───────────────────────────────────────────────────────────────────\n",
- "_perf_stats_html = widgets.HTML()\n",
- "_perf_events_html = widgets.HTML()\n",
- "\n",
- "_reset_btn = widgets.Button(\n",
- " description=\"Reset performance database\",\n",
- " button_style=\"danger\",\n",
- " icon=\"trash\",\n",
- " layout=widgets.Layout(width=\"230px\"),\n",
- ")\n",
- "_reset_confirm_html = widgets.HTML(\n",
- " ''\n",
- " \"Warning: This will permanently delete all performance records. \"\n",
- " \"Time estimates will reset to “no data”.\"\n",
- ")\n",
- "_reset_confirm_yes = widgets.Button(\n",
- " description=\"Yes, delete all records\",\n",
- " button_style=\"danger\",\n",
- " icon=\"check\",\n",
- " layout=widgets.Layout(width=\"190px\"),\n",
- ")\n",
- "_reset_confirm_no = widgets.Button(\n",
- " description=\"Cancel\",\n",
- " button_style=\"\",\n",
- " icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\"),\n",
- ")\n",
- "_reset_confirm_box = widgets.VBox(\n",
- " [\n",
- " _reset_confirm_html,\n",
- " widgets.HBox(\n",
- " [_reset_confirm_yes, _reset_confirm_no],\n",
- " layout=widgets.Layout(gap=\"8px\", margin=\"4px 0 0\"),\n",
- " ),\n",
- " ],\n",
- " layout=widgets.Layout(\n",
- " display=\"none\",\n",
- " border=\"1px solid #fca5a5\",\n",
- " padding=\"8px 10px\",\n",
- " margin=\"6px 0 0\",\n",
- " ),\n",
- ")\n",
- "\n",
- "\n",
- "def _build_perf_stats_html() -> str:\n",
- " from quantui.calc_log import get_perf_history\n",
- " records = get_perf_history()\n",
- " if not records:\n",
- " return (\n",
- " ''\n",
- " \"No performance data recorded yet.\"\n",
- " )\n",
- " groups: dict = {}\n",
- " for r in records:\n",
- " key = (r.get(\"method\", \"?\"), r.get(\"basis\", \"?\"))\n",
- " groups.setdefault(key, []).append(r)\n",
- " rows = \"\"\n",
- " for (meth, bas), recs in sorted(groups.items()):\n",
- " times = [r[\"elapsed_s\"] for r in recs if \"elapsed_s\" in r]\n",
- " n = len(recs)\n",
- " if times:\n",
- " avg = sum(times) / len(times)\n",
- " rows += (\n",
- " \"\"\n",
- " f'| {meth} | '\n",
- " f'{bas} | '\n",
- " f'{n} | '\n",
- " f'{avg:.1f} s | '\n",
- " f'{min(times):.1f} s | '\n",
- " f'{max(times):.1f} s | '\n",
- " \"
\"\n",
- " )\n",
- " header = (\n",
- " \"\"\n",
- " '| Method | '\n",
- " 'Basis | '\n",
- " 'Runs | '\n",
- " 'Avg | '\n",
- " 'Min | '\n",
- " 'Max | '\n",
- " \"
\"\n",
- " )\n",
- " return (\n",
- " ''\n",
- " f\"{header}{rows}
\"\n",
- " )\n",
- "\n",
- "\n",
- "def _build_events_html() -> str:\n",
- " from quantui.calc_log import get_recent_events\n",
- " events = get_recent_events(20)\n",
- " if not events:\n",
- " return (\n",
- " ''\n",
- " \"No events recorded yet.\"\n",
- " )\n",
- " rows = \"\"\n",
- " for e in reversed(events):\n",
- " ts = e.get(\"timestamp\", \"\")[:19].replace(\"T\", \" \")\n",
- " evt = e.get(\"event\", \"\")\n",
- " msg = e.get(\"message\", \"\")\n",
- " rows += (\n",
- " \"\"\n",
- " f'| {ts} | '\n",
- " f'{evt} | '\n",
- " f'{msg} | '\n",
- " \"
\"\n",
- " )\n",
- " return (\n",
- " '\"\n",
- " )\n",
- "\n",
- "\n",
- "def _refresh_perf_stats():\n",
- " _perf_stats_html.value = _build_perf_stats_html()\n",
- " _perf_events_html.value = _build_events_html()\n",
- "\n",
- "\n",
- "def _on_reset_click(btn):\n",
- " _reset_confirm_box.layout.display = \"\"\n",
- "\n",
- "\n",
- "def _on_confirm_yes(btn):\n",
- " from quantui.calc_log import reset_perf_log\n",
- " reset_perf_log()\n",
- " _reset_confirm_box.layout.display = \"none\"\n",
- " _refresh_perf_stats()\n",
- "\n",
- "\n",
- "def _on_confirm_no(btn):\n",
- " _reset_confirm_box.layout.display = \"none\"\n",
- "\n",
- "\n",
- "_reset_btn.on_click(_on_reset_click)\n",
- "_reset_confirm_yes.on_click(_on_confirm_yes)\n",
- "_reset_confirm_no.on_click(_on_confirm_no)\n",
- "_refresh_perf_stats()\n",
- "\n",
- "_perf_stats_panel = widgets.VBox([\n",
- " _perf_stats_html,\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Recent events (last 20)
\"\n",
- " ),\n",
- " _perf_events_html,\n",
- " widgets.HBox(\n",
- " [_reset_btn],\n",
- " layout=widgets.Layout(margin=\"14px 0 4px\"),\n",
- " ),\n",
- " _reset_confirm_box,\n",
- "])\n",
- "\n",
- "_perf_accordion = widgets.Accordion(children=[_perf_stats_panel], selected_index=None)\n",
- "_perf_accordion.set_title(0, \"Performance stats\")\n",
- "\n",
- "# ── History panel (assembled widget for the History tab) ─────────────────\n",
- "history_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Calculations are saved automatically. Select one below to view its results.
\"\n",
- " ),\n",
- " widgets.HBox(\n",
- " [past_dd, past_refresh_btn, copy_path_btn, view_log_btn],\n",
- " layout=widgets.Layout(align_items=\"center\", gap=\"8px\"),\n",
- " ),\n",
- " results_path_lbl,\n",
- " past_output,\n",
- " _perf_accordion,\n",
- "])\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9",
- "metadata": {},
- "outputs": [],
- "source": [
- "from pathlib import Path\n",
- "\n",
- "# ── Compare tab widgets ────────────────────────────────────────────────────────\n",
- "compare_select = widgets.SelectMultiple(\n",
- " options=[(\"(no saved results)\", \"\")],\n",
- " rows=8,\n",
- " description=\"\",\n",
- " layout=widgets.Layout(width=\"100%\"),\n",
- ")\n",
- "compare_refresh_btn = widgets.Button(\n",
- " description=\"Refresh\", button_style=\"\", icon=\"refresh\",\n",
- " layout=widgets.Layout(width=\"100px\"),\n",
- ")\n",
- "compare_btn = widgets.Button(\n",
- " description=\"Compare selected\", button_style=\"primary\", icon=\"bar-chart\",\n",
- " disabled=True,\n",
- " layout=widgets.Layout(width=\"180px\"),\n",
- ")\n",
- "compare_clear_btn = widgets.Button(\n",
- " description=\"Clear\", button_style=\"warning\", icon=\"times\",\n",
- " layout=widgets.Layout(width=\"90px\"),\n",
- ")\n",
- "compare_output = widgets.Output()\n",
- "\n",
- "\n",
- "def _populate_compare_list():\n",
- " '''Repopulate compare_select with all saved results.'''\n",
- " from quantui.results_storage import list_results, load_result\n",
- " dirs = list_results()\n",
- " if not dirs:\n",
- " compare_select.options = [(\"(no saved results)\", \"\")]\n",
- " compare_btn.disabled = True\n",
- " return\n",
- " options = []\n",
- " for d in dirs:\n",
- " try:\n",
- " data = load_result(d)\n",
- " ts = data.get(\"timestamp\", d.name[:19])\n",
- " label = f\"{ts} {data['formula']} {data['method']}/{data['basis']}\"\n",
- " options.append((label, str(d)))\n",
- " except Exception:\n",
- " options.append((d.name, str(d)))\n",
- " compare_select.options = options\n",
- " compare_btn.disabled = False\n",
- "\n",
- "\n",
- "def _on_compare_refresh(btn):\n",
- " _populate_compare_list()\n",
- "\n",
- "\n",
- "def _on_compare(btn):\n",
- " selected = compare_select.value # tuple of selected path strings\n",
- " if not selected or selected == (\"\",):\n",
- " return\n",
- " compare_output.clear_output(wait=True)\n",
- " from quantui.results_storage import load_result\n",
- " from quantui import summary_from_saved_result, comparison_table_html, plot_comparison\n",
- " summaries = []\n",
- " for path_str in selected:\n",
- " if not path_str:\n",
- " continue\n",
- " try:\n",
- " data = load_result(Path(path_str))\n",
- " summaries.append(summary_from_saved_result(data))\n",
- " except Exception as exc:\n",
- " with compare_output:\n",
- " display(HTML(f'Error loading result: {exc}
'))\n",
- " if not summaries:\n",
- " return\n",
- " with compare_output:\n",
- " display(HTML(comparison_table_html(summaries)))\n",
- " if len(summaries) > 1:\n",
- " try:\n",
- " import matplotlib.pyplot as plt\n",
- " fig = plot_comparison(summaries)\n",
- " display(fig)\n",
- " plt.close(fig)\n",
- " except Exception:\n",
- " pass\n",
- "\n",
- "\n",
- "def _on_compare_clear(btn):\n",
- " compare_select.value = ()\n",
- " compare_output.clear_output()\n",
- "\n",
- "\n",
- "compare_refresh_btn.on_click(_on_compare_refresh)\n",
- "compare_btn.on_click(_on_compare)\n",
- "compare_clear_btn.on_click(_on_compare_clear)\n",
- "\n",
- "# Populate on load; expose to Cell-3 placeholder so _do_run refreshes it.\n",
- "_populate_compare_list()\n",
- "globals()[\"_populate_compare_list\"] = _populate_compare_list\n",
- "\n",
- "# ── Compare panel (assembled widget for the Compare tab) ──────────────────────\n",
- "compare_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " 'Compare Calculations
'\n",
- " ''\n",
- " \"Select two or more saved calculations to compare side-by-side. \"\n",
- " \"Hold Ctrl (or ⌘) to select multiple entries.
\"\n",
- " ),\n",
- " widgets.HBox([compare_refresh_btn]),\n",
- " compare_select,\n",
- " widgets.HBox(\n",
- " [compare_btn, compare_clear_btn],\n",
- " layout=widgets.Layout(gap=\"8px\", margin=\"6px 0\"),\n",
- " ),\n",
- " compare_output,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Advanced accordion (Export only) ──────────────────────────────────────────\n",
- "_export_content = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Download a self-contained PySCF script you can study or run outside the notebook.
\"\n",
- " ),\n",
- " widgets.HBox([export_btn, export_status]),\n",
- "])\n",
- "advanced_accordion = widgets.Accordion(children=[_export_content])\n",
- "advanced_accordion.set_title(0, \"Export Script\")\n",
- "advanced_accordion.selected_index = None # collapsed by default\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "10",
- "metadata": {},
- "outputs": [],
- "source": [
- "from quantui.help_content import HELP_TOPICS\n",
- "\n",
- "# ── Help tab content ──────────────────────────────────────────────────────────\n",
- "_help_keys = list(HELP_TOPICS.keys())\n",
- "_help_labels = [HELP_TOPICS[k][\"title\"] for k in _help_keys]\n",
- "\n",
- "help_topic_dd = widgets.Dropdown(\n",
- " options=list(zip(_help_labels, _help_keys)),\n",
- " description=\"Topic:\",\n",
- " style={\"description_width\": \"60px\"},\n",
- " layout=widgets.Layout(width=\"460px\"),\n",
- ")\n",
- "help_content_html = widgets.HTML()\n",
- "\n",
- "def _render_help_topic(change=None):\n",
- " key = help_topic_dd.value\n",
- " if key and key in HELP_TOPICS:\n",
- " entry = HELP_TOPICS[key]\n",
- " help_content_html.value = (\n",
- " f''\n",
- " f'
'\n",
- " f'{entry[\"title\"]}
'\n",
- " f'
'\n",
- " f'{entry[\"body\"]}
'\n",
- " f'
'\n",
- " )\n",
- "\n",
- "help_topic_dd.observe(_render_help_topic, names=\"value\")\n",
- "_render_help_topic() # render first topic on load\n",
- "\n",
- "help_tab_panel = widgets.VBox([\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Browse help topics below. Click ? next to the Method or Basis Set \"\n",
- " \"dropdown in the Calculate tab to jump directly to a relevant topic.
\"\n",
- " ),\n",
- " help_topic_dd,\n",
- " help_content_html,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Assemble root tab ─────────────────────────────────────────────────────────\n",
- "_calculate_content = widgets.VBox([\n",
- " step_progress.widget,\n",
- " mol_input_container,\n",
- " calc_setup_panel,\n",
- " run_panel,\n",
- " results_panel,\n",
- " advanced_accordion,\n",
- "], layout=widgets.Layout(padding=\"8px 0\"))\n",
- "\n",
- "# ── Output log tab ────────────────────────────────────────────────────────────────\n",
- "_log_output_html = widgets.HTML(\n",
- " ''\n",
- " \"No log yet — run a calculation first, or use \"\n",
- " \"View log in the History tab.\"\n",
- ")\n",
- "_log_source_lbl = widgets.HTML()\n",
- "_log_clear_btn = widgets.Button(\n",
- " description=\"Clear\",\n",
- " button_style=\"\",\n",
- " icon=\"times\",\n",
- " layout=widgets.Layout(width=\"80px\"),\n",
- ")\n",
- "\n",
- "\n",
- "def _render_log(text: str, source_label: str = \"\") -> None:\n",
- " # Render text as syntax-highlighted HTML in the log panel.\n",
- " import html as _html_mod\n",
- " lines = text.splitlines()\n",
- " rows = []\n",
- " for line in lines:\n",
- " esc = _html_mod.escape(line)\n",
- " if \"converged SCF energy\" in line or \"SCF converged\" in line:\n",
- " style = \"color:#16a34a;font-weight:600\"\n",
- " elif \"cycle=\" in line and \"E=\" in line:\n",
- " style = \"color:#475569\"\n",
- " elif \"HOMO\" in line or \"LUMO\" in line:\n",
- " style = \"color:#2563eb\"\n",
- " elif \"Warning\" in line or \"warning\" in line:\n",
- " style = \"color:#d97706\"\n",
- " elif \"Error\" in line or \"error\" in line or \"failed\" in line:\n",
- " style = \"color:#dc2626\"\n",
- " else:\n",
- " style = \"color:#1e293b\"\n",
- " rows.append(f'{esc}
')\n",
- " _log_output_html.value = (\n",
- " ''\n",
- " + \"\".join(rows)\n",
- " + \"
\"\n",
- " )\n",
- " _log_source_lbl.value = (\n",
- " f'Source: {source_label}'\n",
- " if source_label\n",
- " else \"\"\n",
- " )\n",
- "\n",
- "\n",
- "def _update_log_panel(log_text: str, label: str = \"\") -> None:\n",
- " _render_log(log_text, label)\n",
- "\n",
- "\n",
- "def _goto_output_tab() -> None:\n",
- " root_tab.selected_index = 3\n",
- "\n",
- "\n",
- "def _clear_log(_):\n",
- " _log_output_html.value = (\n",
- " 'Log cleared.'\n",
- " )\n",
- " _log_source_lbl.value = \"\"\n",
- "\n",
- "\n",
- "_log_clear_btn.on_click(_clear_log)\n",
- "\n",
- "log_tab_panel = widgets.VBox(\n",
- " [\n",
- " widgets.HTML(\n",
- " ''\n",
- " \"Full PySCF output for the most recent calculation. \"\n",
- " \"Use View log in the History tab to load a saved result's log.
\"\n",
- " ),\n",
- " widgets.HBox(\n",
- " [_log_clear_btn],\n",
- " layout=widgets.Layout(margin=\"0 0 8px\"),\n",
- " ),\n",
- " _log_source_lbl,\n",
- " _log_output_html,\n",
- " ],\n",
- " layout=widgets.Layout(padding=\"8px 0\"),\n",
- ")\n",
- "\n",
- "# Overwrite Cell-3 placeholders so later widgets can be reached.\n",
- "globals()[\"_update_log_panel\"] = _update_log_panel\n",
- "globals()[\"_goto_output_tab\"] = _goto_output_tab\n",
- "\n",
- "root_tab = widgets.Tab(children=[\n",
- " _calculate_content,\n",
- " history_panel,\n",
- " compare_panel,\n",
- " log_tab_panel,\n",
- " help_tab_panel,\n",
- "])\n",
- "root_tab.set_title(0, \"Calculate\")\n",
- "root_tab.set_title(1, \"History\")\n",
- "root_tab.set_title(2, \"Compare\")\n",
- "root_tab.set_title(3, \"Output\")\n",
- "root_tab.set_title(4, \"Help\")\n",
- "\n",
- "# ── \"?\" button callbacks: jump to Help tab with specific topic ────────────────\n",
- "def _show_help_topic(topic: str) -> None:\n",
- " if topic in HELP_TOPICS:\n",
- " help_topic_dd.value = topic\n",
- " root_tab.selected_index = 4\n",
- "\n",
- "# Overwrite Cell-3 placeholder so \"?\" buttons find the real function.\n",
- "globals()[\"_show_help_topic\"] = _show_help_topic\n",
- "\n",
- "# ── Single root display call ──────────────────────────────────────────────────\n",
- "display(root_tab)\n"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "name": "python",
- "version": "3.11.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/quantui/app.py b/quantui/app.py
index 64f8484..2347b59 100644
--- a/quantui/app.py
+++ b/quantui/app.py
@@ -82,7 +82,7 @@
_PREOPT_AVAILABLE = False
# ── Module-level constants ────────────────────────────────────────────────────
-_THEME_HUE: dict = {"Dark": 180, "Dark Blue": 200, "Dark Maroon": 160}
+_THEME_HUE: dict = {"Dark": 180}
_APP_CSS: str = """