diff --git a/README.md b/README.md index bf90e2f..8a6156b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,41 @@ research and classroom use. --- +## Example results + +Real output from QuantUI, straight from the app: + + + + + + + + + + +
+ Cisplatin LUMO isosurface
+ Cisplatin LUMO
+ Molecular-orbital isosurface · B3LYP/LANL2DZ (ECP on Pt & Cl) +
+ Aspartame orbital energy-level diagram
+ Aspartame orbital energies
+ Occupied/virtual levels · HOMO–LUMO gap 6.09 eV · B3LYP/6-31G* +
+ Benzene IR spectrum
+ Benzene IR spectrum
+ Analytical Hessian · ωB97X-D/6-31G · the four IR-active bands +
+ Cisplatin geometry-optimization energy convergence
+ Geometry optimization
+ Cisplatin relaxing over 21 BFGS steps · B3LYP/LANL2DZ +
+ +▶ **[Live interactive gallery →](https://the-schultz-lab.github.io/QuantUI/#examples)** — rotate the 3D structures, and watch the optimization trajectory and a vibrational mode animate. + +--- + ## What it does - **Molecule input** — paste XYZ coordinates, browse an indexed bundled library diff --git a/docs/examples/benzene_vib_mode.html b/docs/examples/benzene_vib_mode.html new file mode 100644 index 0000000..f4057d1 --- /dev/null +++ b/docs/examples/benzene_vib_mode.html @@ -0,0 +1,59 @@ + + + + + +QuantUI animation + + + +
+

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

+
+ + + diff --git a/docs/examples/cisplatin_trajectory.html b/docs/examples/cisplatin_trajectory.html new file mode 100644 index 0000000..80a5ba7 --- /dev/null +++ b/docs/examples/cisplatin_trajectory.html @@ -0,0 +1,101 @@ + + + + + +Geo Opt: H6Cl2N2Pt + + + +
+

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

+
+
Step 21 / 21
+ + diff --git a/docs/images/aspartame_orbital_diagram.png b/docs/images/aspartame_orbital_diagram.png new file mode 100644 index 0000000..3f1f8f6 Binary files /dev/null and b/docs/images/aspartame_orbital_diagram.png differ diff --git a/docs/images/benzene_ir_spectrum.png b/docs/images/benzene_ir_spectrum.png new file mode 100644 index 0000000..14708b6 Binary files /dev/null and b/docs/images/benzene_ir_spectrum.png differ diff --git a/docs/images/cisplatin_lumo.png b/docs/images/cisplatin_lumo.png new file mode 100644 index 0000000..8490e34 Binary files /dev/null and b/docs/images/cisplatin_lumo.png differ diff --git a/docs/images/cisplatin_optimization.png b/docs/images/cisplatin_optimization.png new file mode 100644 index 0000000..64011b3 Binary files /dev/null and b/docs/images/cisplatin_optimization.png differ diff --git a/docs/index.html b/docs/index.html index 2e2c2cb..83784a4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -195,6 +195,23 @@ @media (max-width: 900px) { .features-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 560px) { .features-grid { grid-template-columns: 1fr; } } + /* ── Example gallery ─────────────────────────────────────────────────── */ + .gallery-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; margin-top: 1rem; } + @media (max-width: 720px) { .gallery-grid { grid-template-columns: 1fr; } } + .gallery-figure { + margin: 0; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 14px; + overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.05), 0 4px 16px rgba(0,0,0,.07); + display: flex; flex-direction: column; + } + .gallery-figure img, .gallery-figure iframe { width: 100%; display: block; background: #ffffff; border: 0; } + .gallery-figure iframe { height: 340px; } + .gallery-figure figcaption { padding: 1rem 1.25rem; border-top: 1px solid #eef2f7; } + .gallery-figure figcaption strong { display: block; color: #0f172a; font-weight: 650; } + .gallery-figure figcaption span { display: block; color: #64748b; font-size: 0.9rem; margin-top: 0.25rem; } + .gallery-figure figcaption a { color: #2563eb; font-weight: 600; text-decoration: none; } + .gallery-figure figcaption a:hover { text-decoration: underline; } + .gallery-badge { color: #2563eb; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; } + .card { background: #ffffff; border-radius: 14px; @@ -341,6 +358,7 @@ v0.3 · MIT Features + Examples Install Tutorials GitHub → @@ -566,6 +584,73 @@

A complete PySCF workflow

+ +
+
+

See it in action

+

+ Real QuantUI output — a molecular orbital, an orbital energy-level + diagram, an IR spectrum, and a geometry optimization. The two panels marked + Interactive are live: drag to rotate, press play. +

+ +
+
+
diff --git a/quantui/log_utils.py b/quantui/log_utils.py index eb3ae5b..8c316b9 100644 --- a/quantui/log_utils.py +++ b/quantui/log_utils.py @@ -379,8 +379,38 @@ def format_log_header( # ============================================================================ -def _extract_warnings(log_text: str) -> list[str]: - """Return list of unique warning/error lines found in log_text.""" +# A converged HOMO-LUMO gap wider than this (eV) means any earlier +# "HOMO == LUMO" degeneracy warning described a pre-convergence density, not the +# result — see _extract_warnings. Small enough that a genuinely (near-)degenerate +# converged state stays below it and keeps its warning. +_DEGENERACY_GAP_THRESHOLD_EV: float = 0.1 + + +def _is_homo_lumo_degeneracy_warning(lower: str) -> bool: + """True for PySCF's ``get_occ`` 'HOMO x == LUMO y' degeneracy warning.""" + return "homo" in lower and "== lumo" in lower + + +def _extract_warnings( + log_text: str, *, converged_gap_ev: float | None = None +) -> list[str]: + """Return list of unique warning/error lines found in log_text. + + PySCF's ``get_occ`` emits ``HOMO == LUMO `` on *every* density it + sees — including the ``minao`` initial guess, before SCF iteration 1. For a + transition-metal complex the bare initial guess has a near-degenerate d + manifold with no ligand field yet, so this warning fires even when the SCF + then converges to a perfectly healthy gap (observed on ferrocene: the digest + showed ``HOMO == LUMO`` right beneath a converged 2.98 eV gap — a false + alarm). When *converged_gap_ev* is provided and comfortably non-degenerate + (> ``_DEGENERACY_GAP_THRESHOLD_EV``), such lines are dropped as transient: + they describe a pre-convergence density, not the result. A genuinely + (near-)degenerate converged state has a small gap and keeps its warning. + (M-UX2 UXP2.6.) + """ + drop_degeneracy = ( + converged_gap_ev is not None and converged_gap_ev > _DEGENERACY_GAP_THRESHOLD_EV + ) seen: set[str] = set() found = [] for line in log_text.splitlines(): @@ -392,6 +422,8 @@ def _extract_warnings(log_text: str) -> list[str]: kw in lower for kw in ("warn", "error", "failed", "not converge", "imaginary") ): + if drop_degeneracy and _is_homo_lumo_degeneracy_warning(lower): + continue if stripped not in seen: seen.add(stripped) found.append(stripped) @@ -420,6 +452,10 @@ def format_log_footer( lines: list[str] = ["", _SEP, " ── Result " + "─" * (_WIDTH - 12)] + # Hoisted: used by the warnings digest below, which runs whether or not + # ``result`` is present. + gap_ev: float | None = None + if result is not None: converged = getattr(result, "converged", None) n_iter = getattr(result, "n_iterations", None) @@ -474,7 +510,7 @@ def format_log_footer( # Warnings digest lines.append(" ── Warnings Digest " + "─" * (_WIDTH - 22)) - warnings = _extract_warnings(log_text) + warnings = _extract_warnings(log_text, converged_gap_ev=gap_ev) if warnings: for w in warnings[:10]: # cap at 10 # Truncate very long lines diff --git a/tests/test_log_utils_digest.py b/tests/test_log_utils_digest.py new file mode 100644 index 0000000..411b946 --- /dev/null +++ b/tests/test_log_utils_digest.py @@ -0,0 +1,86 @@ +"""Warnings Digest filtering — M-UX2 UXP2.6. + +PySCF's ``get_occ`` emits ``HOMO x == LUMO y`` on every density it evaluates, +including the ``minao`` initial guess before SCF iteration 1. A transition-metal +run therefore surfaces that degeneracy warning even when the SCF converges to a +healthy gap. The digest should drop it as transient when the converged gap is +clearly non-degenerate, while keeping it for a genuinely (near-)degenerate +converged state and keeping all other warnings unconditionally. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +from quantui.log_utils import _extract_warnings, format_log_footer + +# The exact shape PySCF writes (%.15g floats), as captured on the ferrocene run. +_FERROCENE_LOG = """\ +Initial guess from minao. +init E= -1650.00339122848 + +WARN: HOMO -0.0401210572232631 == LUMO -0.0400479397532248 + +cycle= 1 E= -1636.07 delta_E= 13.9 |g|= 5.79 +converged SCF energy = -1649.69023957 +""" + + +class TestDegeneracyWarningFilter: + def test_transient_degeneracy_dropped_when_converged_gap_wide(self): + # Ferrocene: warning is from the initial guess; converged gap is 2.98 eV. + warnings = _extract_warnings(_FERROCENE_LOG, converged_gap_ev=2.98) + assert not any("== LUMO" in w for w in warnings) + + def test_degeneracy_kept_when_converged_gap_is_small(self): + # A genuinely (near-)degenerate converged state keeps the warning. + warnings = _extract_warnings(_FERROCENE_LOG, converged_gap_ev=0.002) + assert any("== LUMO" in w for w in warnings) + + def test_degeneracy_kept_when_gap_unknown(self): + # No converged gap (e.g. failed/UHF path) — conservative: keep it. + warnings = _extract_warnings(_FERROCENE_LOG, converged_gap_ev=None) + assert any("== LUMO" in w for w in warnings) + + def test_other_warnings_always_survive(self): + log = ( + "WARN: HOMO -0.04 == LUMO -0.04\n" + "WARN: ECP not specified for something\n" + "SCF did not converge\n" + ) + warnings = _extract_warnings(log, converged_gap_ev=5.0) + joined = "\n".join(warnings) + assert "== LUMO" not in joined # transient degeneracy dropped + assert "ECP not specified" in joined # unrelated warning kept + assert any("not converge" in w for w in warnings) # kept + + def test_threshold_boundary(self): + # Just above the 0.1 eV threshold → dropped; at/below → kept. + assert not any( + "== LUMO" in w + for w in _extract_warnings(_FERROCENE_LOG, converged_gap_ev=0.11) + ) + assert any( + "== LUMO" in w + for w in _extract_warnings(_FERROCENE_LOG, converged_gap_ev=0.10) + ) + + +class TestFooterEndToEnd: + def test_footer_drops_transient_degeneracy_for_healthy_result(self): + result = SimpleNamespace( + converged=True, + n_iterations=25, + energy_hartree=-1649.69023957, + homo_lumo_gap_ev=2.9802, + ) + footer = format_log_footer( + result=result, + wall_time=85.8, + cpu_time=1635.9, + log_text=_FERROCENE_LOG, + success=True, + ) + # The healthy gap is reported; the transient degeneracy warning is gone. + assert "HOMO-LUMO gap: 2.9802 eV" in footer + assert "== LUMO" not in footer