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:
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:
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:
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 — 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 |
|
one orbital (0-based absolute) |
range string |
|
an inclusive range |
frontier token |
|
resolved from the occupations |
frontier range |
|
a window around the gap |
mixed list |
|
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:
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:
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):
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#
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 8
m.orbitalsshows the HOMO at index 4 with occupancy 2.0 and the LUMO at index 5 with occupancy 0.0. Write twoview3dcalls: one for just the HOMO, and one for a gallery from HOMO−1 to LUMO+1.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?
Your notebook shows
ImportError: qc.view 3D rendering needs py3Dmol. What is the one-line fix, and why do the 2-Dplot_*calls still work without it?
Solution to Exercise 8
m.view3d(orbital="HOMO")andm.view3d(orbitals="HOMO-1:LUMO+1")(a range string yields a gallery).m.view3d(orbital="HOMO").to_html("homo.html")— with the defaultself_contained=Trueit embeds the 3Dmol.js library in the file, so it renders offline in any browser.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 covers reading, replaying, and saving what a run
emits. Then we reach the large molecular-properties suite, whose real-space fields are
exactly what view3d draws.