Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ research and classroom use.

---

## Example results

Real output from QuantUI, straight from the app:

<table>
<tr>
<td width="50%" align="center" valign="top">
<img src="docs/images/cisplatin_lumo.png" alt="Cisplatin LUMO isosurface" width="400"><br>
<b>Cisplatin LUMO</b><br>
<sub>Molecular-orbital isosurface · B3LYP/LANL2DZ (ECP on Pt &amp; Cl)</sub>
</td>
<td width="50%" align="center" valign="top">
<img src="docs/images/aspartame_orbital_diagram.png" alt="Aspartame orbital energy-level diagram" width="300"><br>
<b>Aspartame orbital energies</b><br>
<sub>Occupied/virtual levels · HOMO–LUMO gap 6.09 eV · B3LYP/6-31G*</sub>
</td>
</tr>
<tr>
<td width="50%" align="center" valign="top">
<img src="docs/images/benzene_ir_spectrum.png" alt="Benzene IR spectrum" width="400"><br>
<b>Benzene IR spectrum</b><br>
<sub>Analytical Hessian · ωB97X-D/6-31G · the four IR-active bands</sub>
</td>
<td width="50%" align="center" valign="top">
<img src="docs/images/cisplatin_optimization.png" alt="Cisplatin geometry-optimization energy convergence" width="400"><br>
<b>Geometry optimization</b><br>
<sub>Cisplatin relaxing over 21 BFGS steps · B3LYP/LANL2DZ</sub>
</td>
</tr>
</table>

▶ **[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
Expand Down
59 changes: 59 additions & 0 deletions docs/examples/benzene_vib_mode.html

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions docs/examples/cisplatin_trajectory.html

Large diffs are not rendered by default.

Binary file added docs/images/aspartame_orbital_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/benzene_ir_spectrum.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cisplatin_lumo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cisplatin_optimization.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -341,6 +358,7 @@
<a class="nav__logo" href="#">QuantUI</a>
<span class="nav__badge">v0.3 &middot; MIT</span>
<a class="nav__link" href="#features">Features</a>
<a class="nav__link" href="#examples">Examples</a>
<a class="nav__link" href="#install">Install</a>
<a class="nav__link" href="#tutorials">Tutorials</a>
<a class="btn btn--ghost" href="https://github.com/The-Schultz-Lab/QuantUI">GitHub &rarr;</a>
Expand Down Expand Up @@ -566,6 +584,73 @@ <h2 class="section__title">A complete PySCF workflow</h2>
</div>
</section>

<!-- ── Example gallery ───────────────────────────────────────────────────── -->
<section class="section section--tinted" id="examples">
<div class="container">
<h2 class="section__title">See it in action</h2>
<p class="section__subtitle">
Real QuantUI output &mdash; a molecular orbital, an orbital energy-level
diagram, an IR spectrum, and a geometry optimization. The two panels marked
<em>Interactive</em> are live: drag to rotate, press play.
</p>
<div class="gallery-grid">

<figure class="gallery-figure">
<img src="images/cisplatin_lumo.png" alt="Cisplatin LUMO isosurface" loading="lazy">
<figcaption>
<strong>Cisplatin LUMO</strong>
<span>Molecular-orbital isosurface, B3LYP / LANL2DZ (ECP on Pt and Cl).</span>
</figcaption>
</figure>

<figure class="gallery-figure">
<img src="images/aspartame_orbital_diagram.png" alt="Aspartame orbital energy-level diagram" loading="lazy">
<figcaption>
<strong>Aspartame orbital energies</strong>
<span>Occupied/virtual energy levels, HOMO&ndash;LUMO gap 6.09 eV (B3LYP / 6-31G*).</span>
</figcaption>
</figure>

<figure class="gallery-figure">
<img src="images/benzene_ir_spectrum.png" alt="Benzene IR spectrum" loading="lazy">
<figcaption>
<strong>Benzene IR spectrum</strong>
<span>Analytical Hessian, &omega;B97X-D / 6-31G &mdash; the four IR-active bands.</span>
</figcaption>
</figure>

<figure class="gallery-figure">
<img src="images/cisplatin_optimization.png" alt="Cisplatin geometry-optimization energy convergence" loading="lazy">
<figcaption>
<strong>Geometry optimization</strong>
<span>Cisplatin relaxing over 21 BFGS steps, B3LYP / LANL2DZ.</span>
</figcaption>
</figure>

<figure class="gallery-figure">
<iframe src="examples/cisplatin_trajectory.html" title="Cisplatin optimization trajectory (interactive)" loading="lazy"></iframe>
<figcaption>
<span class="gallery-badge">Interactive</span>
<strong>Optimization trajectory</strong>
<span>Watch cisplatin relax step by step &mdash; drag to rotate.
<a href="examples/cisplatin_trajectory.html" target="_blank" rel="noopener">Open full &nearr;</a></span>
</figcaption>
</figure>

<figure class="gallery-figure">
<iframe src="examples/benzene_vib_mode.html" title="Benzene vibrational mode (interactive)" loading="lazy"></iframe>
<figcaption>
<span class="gallery-badge">Interactive</span>
<strong>Vibrational mode</strong>
<span>A benzene normal mode animated in 3D.
<a href="examples/benzene_vib_mode.html" target="_blank" rel="noopener">Open full &nearr;</a></span>
</figcaption>
</figure>

</div>
</div>
</section>

<!-- ── Platform Support ──────────────────────────────────────────────────── -->
<section class="section section--tinted" id="platforms">
<div class="container--narrow">
Expand Down
42 changes: 39 additions & 3 deletions quantui/log_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <x> == LUMO <y>`` 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():
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions tests/test_log_utils_digest.py
Original file line number Diff line number Diff line change
@@ -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