Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b689fe4
Pre-run guard for metal complexes: basis coverage + charge/multiplicity
NCCU-Schultz-Lab Aug 15, 2026
b84e39e
Bundle inorganic example geometries + metal guidance (M-METAL MET.9)
NCCU-Schultz-Lab Aug 15, 2026
0ca0798
Fix two governance tests for the MET.9 inorganic examples
NCCU-Schultz-Lab Aug 16, 2026
6c1bb4d
MET.4: pre-opt reports an honest failure on metal complexes
NCCU-Schultz-Lab Aug 16, 2026
e502dac
MET.2: warn when a fetched structure resolves to a disconnected salt
NCCU-Schultz-Lab Aug 16, 2026
f2b8fdf
MET.3: viewer falls back to py3Dmol instead of crashing on a metal
NCCU-Schultz-Lab Aug 16, 2026
5231de2
MET.5: add LANL2DZ as a heavy-metal ECP basis option
NCCU-Schultz-Lab Aug 16, 2026
0859adc
MET.5: oxidation-state → spin-state multiplicity suggestion engine
NCCU-Schultz-Lab Aug 16, 2026
5899458
MET.5: one-click "Switch to def2-SVP" fix for a metal on a bad basis
NCCU-Schultz-Lab Aug 16, 2026
b3b312a
Add GFN-FF (xtb) metal-capable pre-optimization backend
NCCU-Schultz-Lab Aug 16, 2026
a88b3e6
MET.6: draw dashed coordination bonds so a metal isn't a lone dot
NCCU-Schultz-Lab Aug 16, 2026
1600c25
MET.5: spin engine — transparency pass (caveats, square-planar d8-onl…
NCCU-Schultz-Lab Aug 16, 2026
60718a7
MET.5: pick-and-apply spin-state helper UI
NCCU-Schultz-Lab Aug 16, 2026
d7ecf29
Bundle 11 more inorganic examples (14 total), GFN-FF-validated
NCCU-Schultz-Lab Aug 16, 2026
b70c2e3
Sync the Charge/Multiplicity fields onto the active molecule
NCCU-Schultz-Lab Aug 16, 2026
049180d
Document inorganic / coordination-complex support (README + landing p…
NCCU-Schultz-Lab Aug 16, 2026
be8c17e
Normalize RDKit's bond-order-perception exception in the PlotlyMol path
NCCU-Schultz-Lab Aug 16, 2026
54885ce
Gate the PySCF-backed basis_fix_button test on PySCF availability
NCCU-Schultz-Lab Aug 16, 2026
c9949c7
Attach ECP for ECP-carrying bases (LANL2DZ/def2) on all Mole builds
NCCU-Schultz-Lab Aug 20, 2026
32f2d1b
Smooth orbital isosurfaces (3Dmol.js smoothness: 5)
NCCU-Schultz-Lab Aug 20, 2026
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
4 changes: 2 additions & 2 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ python -m pip install --upgrade --ignore-installed setuptools wheel || true
# Editable install with the same extras CI uses. Editable + plain `install`
# (not `ci`-style clean installs) so the resolved deps are cached in the
# container image for later sessions.
pip install -e ".[pyscf,ase,dev]"
pip install -e ".[pyscf,ase,dev,xtb]"

echo "QuantUI cloud env ready: package + [pyscf,ase,dev] installed." >&2
echo "QuantUI cloud env ready: package + [pyscf,ase,dev,xtb] installed." >&2

# Warn if the private planning repo isn't attached to this session. In a cloud
# session an attached repo is cloned as a sibling of this one; the planning docs
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ jobs:
- name: Install package and dev dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[pyscf,ase,dev]"
# xtb (GFN-FF metal pre-opt) is Linux-only on PyPI, so it's included
# here (ubuntu) but not in the Windows job below — its tests skip there.
pip install -e ".[pyscf,ase,dev,xtb]"

- name: Run tests (skip network tests)
run: |
Expand Down
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ research and classroom use.

## What it does

- **Molecule input** — paste XYZ coordinates, browse an indexed three-tier
bundled library (20 presets + 156 curated molecules + ~1,900 QM9 structures,
searchable by name/formula), or run a structure search by name, SMILES,
InChI, PubChem CID, InChIKey, or CAS number (PubChem → NCI CACTUS → offline
bundled-library fallback; SMILES/InChI resolve locally with no network)
- **Molecule input** — paste XYZ coordinates, browse an indexed bundled library
(organic presets + curated molecules + ~1,900 QM9 structures + **14
ready-to-run coordination complexes**, searchable by name/formula), or run a
structure search by name, SMILES, InChI, PubChem CID, InChIKey, or CAS number
(PubChem → NCI CACTUS → offline bundled-library fallback; SMILES/InChI resolve
locally with no network)
- **Offline-first** — runs with no internet: the bundled molecule library and
the 3D viewer's JavaScript (3Dmol.js) are vendored, so structure lookup and
every 3D view work in an air-gapped classroom. (Network is used only for the
Expand All @@ -48,6 +49,18 @@ research and classroom use.
animation; vibrational frequency analysis with animated normal modes,
user-tunable playback FPS, and a per-result-directory disk cache so mode
switches on repeat visits and history replay are instant
- **Inorganic / coordination complexes** — first-class support for
transition-metal chemistry the organic pipeline can't handle: 14 bundled
metal complexes (octahedral / tetrahedral / square-planar; aqua, ammine,
cyanide, carbonyl, halide, oxo) with correct charge and spin; a pre-run guard
that catches a metal on an incompatible basis (nudges to def2-SVP / LANL2DZ)
and an impossible charge/multiplicity before the calculation starts; a
**spin-state helper** that suggests a multiplicity from a metal centre's
oxidation state and geometry (both high- and low-spin where the ligand field
decides — you pick); optional **GFN-FF (xtb) pre-optimization** that relaxes a
metal complex the classical organic force field can't; a warning when a name
search returns a disconnected salt instead of the coordinated complex; and a
viewer that draws the coordination bonds so the metal is never a lone dot
- **Results persistence** — every calculation is saved automatically to a
timestamped directory; a built-in browser lets you reload past results
after a kernel restart; the full `pyscf.log` is shown inline
Expand Down Expand Up @@ -164,6 +177,26 @@ and result cards will display the compute device.
Whenever gpu4pyscf can't offload a particular call, QuantUI falls back
to CPU automatically and the result card reflects which device ran.

### Optional: GFN-FF metal pre-optimization (xtb)

The classical (MMFF/UFF) pre-optimizer relies on RDKit's organic valence
model, which can't handle a transition metal. Install
[xtb](https://github.com/grimme-lab/xtb) to enable **GFN-FF**, a general
force field that relaxes coordination complexes across the whole periodic
table. Fully optional — without it, metal pre-opt simply reports that it
isn't available and points you to the DFT geometry optimization.

```bash
# Linux (PyPI wheels bundle the compiled library):
pip install quantui[xtb]

# Windows / macOS (no PyPI wheel — use conda-forge):
conda install -c conda-forge xtb-python
```

QuantUI detects xtb automatically and routes metal pre-optimizations through
GFN-FF; organic molecules still use the faster RDKit force field.

---

## Quick start
Expand Down Expand Up @@ -365,7 +398,13 @@ Five step-by-step notebooks in [`notebooks/tutorials/`](https://github.com/The-S
### Basis sets

STO-3G (fast, good for learning) → 3-21G → 6-31G / 6-31G\* / 6-31G\*\* →
cc-pVDZ / cc-pVTZ → def2-SVP / def2-TZVP
cc-pVDZ / cc-pVTZ → def2-SVP / def2-TZVP → LANL2DZ

For **transition metals and heavy elements**, use **def2-SVP** / **def2-TZVP**
or **LANL2DZ** — these carry the effective core potentials that cover metals,
whereas the Pople (`6-31G…`) and Dunning (`cc-pV…`) sets do not. QuantUI's
pre-run guard flags a metal on an incompatible basis and offers a one-click
switch to def2-SVP before the calculation starts.

---

Expand Down
17 changes: 16 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuantUI — Free, open, and interactive quantum chemistry</title>
<meta name="description" content="QuantUI is a free, open-source frontend for PySCF — DFT, MP2, CCSD, CCSD(T), TD-DFT, NMR, geometry optimization, frequency analysis, PES scans, optional NVIDIA GPU offload, and interactive 3D visualization, running locally in Jupyter or Voilà.">
<meta name="description" content="QuantUI is a free, open-source frontend for PySCF — DFT, MP2, CCSD, CCSD(T), TD-DFT, NMR, geometry optimization, frequency analysis, PES scans, transition-metal / coordination-complex support (GFN-FF pre-optimization, spin-state helper), optional NVIDIA GPU offload, and interactive 3D visualization, running locally in Jupyter or Voilà.">
<meta property="og:title" content="QuantUI — Free, open, and interactive quantum chemistry">
<meta property="og:description" content="PySCF in an interactive Jupyter/Voilà UI. DFT, MP2, CCSD, CCSD(T), NMR, UV-Vis, frequencies, PES scans, GPU offload, and 3D structures — all local, no cluster required.">
<meta property="og:type" content="website">
Expand Down Expand Up @@ -547,6 +547,21 @@ <h2 class="section__title">A complete PySCF workflow</h2>
</p>
</div>

<div class="card">
<div class="feature-card__icon">🧲</div>
<div class="feature-card__title">Inorganic &amp; Coordination Complexes</div>
<p class="feature-card__body">
First-class transition-metal support: 14 bundled metal complexes
(octahedral / tetrahedral / square-planar) with correct charge and
spin, a pre-run guard that catches a metal on an incompatible basis
(nudges to def2-SVP / LANL2DZ) and an impossible multiplicity, a
<strong>spin-state helper</strong> that suggests high/low-spin
multiplicities from oxidation state + geometry, optional
<strong>GFN-FF (xtb)</strong> pre-optimization for metals, and a
viewer that draws the coordination bonds.
</p>
</div>

</div>
</div>
</section>
Expand Down
3 changes: 3 additions & 0 deletions local-setup/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ dependencies:
- 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
- xtb-python>=20.2 # GFN-FF metal-capable pre-optimization (the Python API +
# ASE calculator; conda-forge has Windows/macOS builds,
# whereas the PyPI `xtb` wheel is Linux-only)

# Visualization
- py3dmol>=2.0.0 # Primary 3D molecular viewer
Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ ase = [
"ase>=3.22.0,<4",
]

# GFN-FF metal-capable classical pre-optimization via xtb (Grimme's general
# force field). RDKit's organic valence model can't touch a transition metal;
# GFN-FF covers the whole periodic table. quantui.preopt uses it when present
# and falls back gracefully otherwise. xtb publishes **Linux pip wheels only** —
# on Windows/macOS install it from conda-forge (see local-setup/environment.yml).
# Depends on ase (the optimizer driving the relaxation).
xtb = [
"xtb>=22.1",
"ase>=3.22.0,<4",
]

# Voilà app server — hides notebook code; students see only the widget UI.
# Run with: voila notebooks/molecule_computations.ipynb
app = [
Expand Down
57 changes: 57 additions & 0 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@
from quantui.app_runflow import (
on_accumulate as _run_on_accumulate,
)
from quantui.app_runflow import (
on_basis_fix as _run_on_basis_fix,
)
from quantui.app_runflow import (
on_basis_help as _run_on_basis_help,
)
Expand Down Expand Up @@ -306,6 +309,12 @@
from quantui.app_runflow import (
on_solvent_cb_changed as _run_on_solvent_cb_changed,
)
from quantui.app_runflow import (
on_spin_apply as _run_on_spin_apply,
)
from quantui.app_runflow import (
on_spin_suggest as _run_on_spin_suggest,
)
from quantui.app_runflow import (
populate_compare_list as _run_populate_compare_list,
)
Expand Down Expand Up @@ -1985,6 +1994,14 @@ def _wire_callbacks(self) -> None:
self.basis_dd.observe(self._safe_cb(self._update_notes), names="value")
# Multiplicity drives the open-shell hint (part of _update_notes).
self.mult_si.observe(self._safe_cb(self._update_notes), names="value")
# Keep the active molecule's charge/multiplicity in step with the fields,
# so an edit here (or the spin-state helper's Apply) actually reaches the
# run — the calc reads mol.charge/mol.multiplicity, and the pre-run guard
# reads the widgets, so the two must not drift apart.
self.charge_si.observe(
self._safe_cb(self._sync_charge_to_molecule), names="value"
)
self.mult_si.observe(self._safe_cb(self._sync_mult_to_molecule), names="value")
self.method_dd.observe(self._safe_cb(self._update_estimate), names="value")
self.basis_dd.observe(self._safe_cb(self._update_estimate), names="value")
# Unfinished-calculations list (CHK.6)
Expand All @@ -2000,6 +2017,14 @@ def _wire_callbacks(self) -> None:
# Run
self.run_btn.on_click(self._on_run_clicked)
self.cancel_btn.on_click(self._safe_cb(self._on_cancel))
self.basis_fix_btn.on_click(self._safe_cb(self._on_basis_fix))
self.spin_suggest_btn.on_click(self._safe_cb(self._on_spin_suggest))
self.spin_apply_btns[0].on_click(
self._safe_cb(lambda _b: self._on_spin_apply(0))
)
self.spin_apply_btns[1].on_click(
self._safe_cb(lambda _b: self._on_spin_apply(1))
)
self.preopt_preview_btn.on_click(self._safe_cb(self._on_preopt_preview))
self.preopt_accept_btn.on_click(self._safe_cb(self._on_preopt_accept))
self.preopt_reset_btn.on_click(self._safe_cb(self._on_preopt_reset))
Expand Down Expand Up @@ -3281,6 +3306,17 @@ def _apply_pubchem_search_result(
"⚠ No network detected — resolved offline from the bundled "
f"library (not PubChem). {msg}"
)
# MET.2: a fetched name can resolve to a disconnected ionic salt
# (cisplatin → 2 NH₃ + 2 HCl + Pt²⁺) rather than the coordinated
# complex. Warn rather than let a wrong geometry silently feed a run.
try:
from .connectivity import describe_disconnection

warning = describe_disconnection(mol.atoms, mol.coordinates)
except Exception: # noqa: BLE001 — a detection failure must not block load
warning = None
if warning:
msg = f"⚠ {warning} {msg}"
self.pubchem_msg.value = msg
else:
self.pubchem_msg.value = f"Not found: {error}"
Expand Down Expand Up @@ -3485,6 +3521,15 @@ def _on_cancel(self, btn=None) -> None:
except Exception:
pass

def _on_basis_fix(self, btn=None) -> None:
_run_on_basis_fix(self, btn)

def _on_spin_suggest(self, btn=None) -> None:
_run_on_spin_suggest(self, btn)

def _on_spin_apply(self, index: int) -> None:
_run_on_spin_apply(self, index)

def _on_preopt_preview(self, btn=None) -> None:
_run_on_preopt_preview(self, btn)

Expand Down Expand Up @@ -4108,6 +4153,18 @@ def _on_help_topic_changed(self, change=None) -> None:

# ══ LOGIC METHODS ════════════════════════════════════════════════════════

def _sync_charge_to_molecule(self, change=None) -> None:
"""Push a Charge-field edit onto the active molecule (see the observer
wiring): the run reads ``mol.charge``, so the field must not drift."""
if self._molecule is not None:
self._molecule.charge = int(self.charge_si.value)

def _sync_mult_to_molecule(self, change=None) -> None:
"""Push a Multiplicity-field edit (or a spin-helper Apply) onto the
active molecule: the run reads ``mol.multiplicity``."""
if self._molecule is not None:
self._molecule.multiplicity = int(self.mult_si.value)

def _set_molecule(self, mol: Molecule, label: str = "") -> None:
"""Update shared state and refresh dependent widgets."""
self._molecule = mol
Expand Down
88 changes: 88 additions & 0 deletions quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,82 @@ def build_shared_widgets(
style={"description_width": "100px"},
layout=layout_fn(width="190px"),
)

# MET.5 spin-state helper: suggest a multiplicity for a metal centre from its
# oxidation state (d-count) + geometry. Suggests, never sets — the student
# clicks an Apply button. Charge is not touched (depends on the ligands).
from quantui.spin_presets import supported_metals

app.spin_metal_dd = widgets.Dropdown(
options=supported_metals(),
value="Fe",
description="Metal:",
style={"description_width": "90px"},
layout=layout_fn(width="160px"),
)
app.spin_ox_si = widgets.BoundedIntText(
value=3,
min=-4,
max=8,
description="Oxidation:",
style={"description_width": "90px"},
layout=layout_fn(width="160px"),
)
app.spin_geom_dd = widgets.Dropdown(
options=[
("Octahedral", "octahedral"),
("Tetrahedral", "tetrahedral"),
("Square-planar", "square_planar"),
],
value="octahedral",
description="Geometry:",
style={"description_width": "90px"},
layout=layout_fn(width="200px"),
)
app.spin_suggest_btn = widgets.Button(
description="Suggest multiplicity",
icon="magic",
button_style="info",
layout=layout_fn(width="200px"),
)
app.spin_helper_output = widgets.HTML(value="")
# Up to two candidate spin states (high/low-spin); hidden until suggested.
app.spin_apply_btns = tuple(
widgets.Button(
description="Apply",
icon="check",
button_style="success",
layout=layout_fn(width="260px", display="none"),
)
for _ in range(2)
)
app._spin_suggested_mults: list = []
app.spin_helper_box = widgets.Accordion(
children=[
widgets.VBox(
[
widgets.HTML(
'<span style="font-size:12px;color:#475569">Suggests a '
"spin multiplicity for a transition-metal centre from its "
"oxidation state and geometry. It never sets anything on "
"its own — review the note, then click Apply. Charge is "
"not changed (it depends on your ligands).</span>"
),
widgets.HBox(
[app.spin_metal_dd, app.spin_ox_si, app.spin_geom_dd],
layout=layout_fn(flex_wrap="wrap", gap="6px"),
),
app.spin_suggest_btn,
app.spin_helper_output,
app.spin_apply_btns[0],
app.spin_apply_btns[1],
],
layout=layout_fn(gap="6px"),
)
]
)
app.spin_helper_box.set_title(0, "🧲 Spin-state helper (metal complexes)")
app.spin_helper_box.selected_index = None # collapsed by default
# Classical (MMFF/UFF) pre-optimization is an explicit, transparent tool —
# Preview → Keep/Revert — NOT a silent checkbox baked into the run. This
# avoids the confusing dual path (accepting a previewed geometry while a
Expand Down Expand Up @@ -1142,6 +1218,16 @@ def build_shared_widgets(
tooltip=("Stop the running calculation at the next SCF cycle / optimizer step"),
)

# MET.5: one-click fix shown only when a metal's basis blocks the run.
# Hidden until the pre-run guard reveals it; sets the basis to def2-SVP.
app.basis_fix_btn = widgets.Button(
description="Switch basis to def2-SVP",
button_style="warning",
icon="wrench",
layout=layout_fn(width="220px", height="36px", display="none"),
tooltip="Set the basis set to def2-SVP, which covers transition metals",
)

app.log_clear_btn = widgets.Button(
description="Clear",
button_style="",
Expand Down Expand Up @@ -1529,6 +1615,7 @@ def build_calc_setup(app: Any, *, layout_fn: Any) -> None:
layout=layout_fn(flex_wrap="wrap", align_items="flex-start"),
),
app._open_shell_hint,
app.spin_helper_box,
widgets.HBox(
[app.calc_type_dd, app.calc_type_help_btn],
layout=layout_fn(align_items="center", gap="4px"),
Expand Down Expand Up @@ -1565,6 +1652,7 @@ def build_run_section(app: Any, *, layout_fn: Any) -> None:
[
app.run_btn,
app.cancel_btn,
app.basis_fix_btn,
# Status + elapsed/remaining chip stacked vertically so the
# timer never crowds/truncates the (longer) status line.
widgets.VBox(
Expand Down
Loading