# Visualization

Numbers tell you *how much*; pictures tell you *where* and *why*. This chapter turns the density and orbitals
from an SCF into images you can inspect — 3-D isosurfaces of orbitals and real-space fields, 2-D plots, and
convergence curves — all from inside a notebook, through the **`qc.view`** layer.

:::{tip} Extra packages for viewing
The 3-D viewer uses **py3Dmol** and static-image export uses **kaleido**; the 2-D plots use **matplotlib**.
Install what you need into your project venv:

```bash
uv add py3dmol kaleido --project "$UV_PROJECT"   # 3-D viewer + static PNG export
```

(matplotlib usually comes with the base scientific stack.) Every viewer entry point exists both as a method
(`mychk.view3d(...)`) and a function (`qc.view3d(mychk, ...)`).
:::

## The orbital table

Before drawing anything, list the orbitals so you know what to ask for. **`mychk.orbitals`** prints a picking
table — index, occupancy, energy — with the frontier orbitals marked, and renders as an HTML table in a
notebook:

```python
import qc
water = "O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469"
m = qc.chk.new(atom=water, ao="cc-pvdz", unit="angstrom").scf(ref="r").run()

m.orbitals
#  idx   occ        E(Eh)   label
#  ---  ----  -----------  -----
#    3   2.0     -0.56656
#    4   2.0     -0.49314   HOMO
#    5   0.0      0.18604   LUMO
#    6   0.0      0.25618
```

Indices are **0-based absolute** (the same convention as PySCF), while the human-friendly path is the
`HOMO`/`LUMO±n` tokens the viewer also accepts. For water/cc-pVDZ the HOMO is index 4 and the LUMO is index 5.

## 3-D isosurfaces: `view3d`

An **isosurface** is the surface on which a scalar field takes a fixed value (the `isovalue`) — the intuitive
way to "see" a cloud-like quantity in 3-D. **`view3d`** renders one inline; the last expression in a cell
displays it:

```python
m.view3d("density")               # electron-density isosurface
m.view3d(orbital="HOMO")          # a signed two-lobe orbital (positive red, negative blue)
m.view3d(orbitals="HOMO-2:LUMO+2")   # a small-multiples gallery
m.view3d("elf", isovalue=0.8)     # a real-space field at a chosen isovalue
```

### Scalar fields

`view3d(field)` accepts the whole qc-prop real-space family — `density`, `spin`, `laplacian`, `rdg`, `iri`,
`elf`, `lol`, `nci`, `igm`, `esp`, `mo`, `alie`, `deformation` — each with a sensible default isovalue. Tune
the render with `isovalue=`, `color=`, `style=` (`"stick"`/`"sphere"`/`"line"`), `width=`/`height=`,
`background=`, and the grid with `spacing=`/`margin=` (bohr). These fields are the visual side of the
[molecular-properties suite](properties/index.md) — the same ELF, NCI, ESP you will compute as numbers there.

### Selecting orbitals

Orbitals render as **signed two-lobe** isosurfaces. Select them with `orbital=` (one) or `orbitals=`
(several → gallery), mixing any of:

| form | example | meaning |
|---|---|---|
| int | `5` | one orbital (0-based absolute) |
| range string | `"10:20"` | an **inclusive** range |
| frontier token | `"HOMO"`, `"LUMO+2"` | resolved from the occupations |
| frontier range | `"HOMO-3:LUMO+3"` | a window around the gap |
| mixed list | `["HOMO-1", "HOMO", 5]` | any combination |

`spin="alpha"` (default) or `"beta"` picks the channel for UHF/ROHF.

## Cube data: `mo_cube`

When you want the **numbers behind the picture** — to export a Gaussian cube, or feed another tool —
`mo_cube` builds and caches the cube data, returning handles that carry each orbital's energy, occupancy, and
cube text:

```python
handles = m.mo_cube("HOMO-1:LUMO+1")
[(h.orbital, round(h.energy, 3), h.occ) for h in handles]
# [(3, -0.567, 2.0), (4, -0.493, 2.0), (5, 0.186, 0.0), (6, 0.256, 0.0)]
```

Cubes are **memoized for the session**, keyed by a hash of the MO coefficients — re-running an SCF that
changes the orbitals automatically invalidates stale cubes (`m.mo_cache` / `m.mo_cache.clear()`).

## 2-D plots

Not everything is best in 3-D. Three matplotlib figures cover the common flat views:

```python
m.plot_convergence()   # the SCF convergence: E, |ΔE|, and RMS gradient vs cycle
m.plot_nci()           # the 2-D NCI plot: reduced density gradient s vs sign(λ₂)ρ
m.plot_field_plane("density", ...)   # a scalar field on a cut plane
```

`plot_convergence()` is the quickest health check on an SCF (a smooth descent to the threshold); `plot_nci()`
is the standard non-covalent-interaction diagnostic (spikes at low density reveal H-bonds, van der Waals
contacts, and steric clashes). Each returns a matplotlib `Figure`.

## Exporting and sharing

A 3-D view can be written to a **self-contained** HTML file (the 3Dmol.js library is embedded, so it opens
offline and in the VSCode webview):

```python
m.view3d(orbital="HOMO").to_html("homo.html")          # standalone HTML
m.view3d(orbital="HOMO").to_html("homo.html", self_contained=False)   # smaller, loads 3Dmol.js from a CDN
```

With **kaleido** installed, the matplotlib/plotly figures can be saved as static PNGs the usual way
(`fig.savefig(...)` / plotly `write_image`).

## Worked example: look at the frontier orbitals

```python
import qc
water = "O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469"
m = qc.chk.new(atom=water, ao="cc-pvdz", unit="angstrom").scf(ref="r").run()

m.orbitals                              # find the HOMO/LUMO (indices 4 and 5)
m.view3d(orbitals="HOMO-1:LUMO+1")      # gallery of the frontier orbitals
m.view3d("density").to_html("water_density.html")   # save the density isosurface
```

:::{exercise}
:label: ex-view

1. `m.orbitals` shows the HOMO at index 4 with occupancy 2.0 and the LUMO at index 5 with occupancy 0.0.
   Write two `view3d` calls: one for just the HOMO, and one for a gallery from HOMO−1 to LUMO+1.
2. You want to hand a colleague an interactive 3-D orbital they can open in a browser with no Python. Which
   method produces that, and what makes the file work offline?
3. Your notebook shows `ImportError: qc.view 3D rendering needs py3Dmol`. What is the one-line fix, and why do
   the 2-D `plot_*` calls still work without it?
:::

:::{solution} ex-view
:class: dropdown

1. `m.view3d(orbital="HOMO")` and `m.view3d(orbitals="HOMO-1:LUMO+1")` (a range string yields a gallery).
2. **`m.view3d(orbital="HOMO").to_html("homo.html")`** — with the default `self_contained=True` it embeds the
   3Dmol.js library in the file, so it renders offline in any browser.
3. `uv add py3dmol --project "$UV_PROJECT"` (then restart the kernel). The 2-D plots use **matplotlib**, a
   different dependency, so they are unaffected by a missing py3Dmol.
:::

Next, the [logging & output chapter](logging-output.md) covers reading, replaying, and saving what a run
emits. Then we reach the large [molecular-properties suite](properties/index.md), whose real-space fields are
exactly what `view3d` draws.
