Skip to content

Surface Viewer

SurfaceViewer renders compact, precomputed isosurfaces from Gaussian CUBE data. Install the optional dependencies first:

pip install "aseview[surface]"

See the SurfaceViewer API reference for the full parameter list.

Single Orbital Cube

from aseview import SurfaceViewer

viewer = SurfaceViewer(
    "HOMO.cube",
    surface_kind="orbital",
    isovalue=0.04,
    positive_color="#ef4444",
    negative_color="#2563eb",
)
viewer.show()

Orbital surfaces are signed, so positive and negative lobes use separate colors.

Density Cube

from aseview import SurfaceViewer

viewer = SurfaceViewer(
    "density.cube",
    surface_kind="density",
    isovalue=0.002,
    surface_color="#22c55e",
    opacity=0.55,
)
viewer.save_html("density-surface.html")

Density surfaces are unsigned and use surface_color.

Opacity And Layering

Solid surfaces can be transparent while still sharing the molecular viewer's atoms, bonds, and cell rendering. Transparent solid shells are drawn with a back-face pass followed by a front-face pass, which keeps orbital lobes readable around atoms without the hard clipping artifacts caused by depth pre-passes.

viewer = SurfaceViewer(
    "HOMO.cube",
    surface_kind="orbital",
    isovalue=0.04,
    opacity=0.65,
    surface_style="solid",
)
viewer.show()

PNG export follows the normal viewer behavior and can keep a transparent background. GIF export uses the viewer background by default so animated surface or trajectory exports do not accumulate transparent-frame trails.

Cube Difference

from aseview import SurfaceViewer

viewer = SurfaceViewer.from_cube_diff(
    "state-a-density.cube",
    "state-b-density.cube",
    diff_sign="1-2",
    isovalue=0.0015,
    positive_color="#f97316",
    negative_color="#0ea5e9",
)
viewer.show()

Use diff_sign="2-1" when the second cube should be subtracted from the first in the opposite order.

Three-File CHGCAR Difference

The documentation ships a BSD-licensed matgenb CHGCAR fixture under docs/assets/examples/surface/chgcar_data/. It contains the combined LiMoS2_F_CHGCAR system and the constituent LiMoS2_CHGCAR and F_CHGCAR systems on the same real-space grid.

from pathlib import Path

from aseview import SurfaceViewer
from cube import delta_grid_field, read_grid_field

base = Path("docs/assets/examples/surface/chgcar_data")

chgcar_ab = read_grid_field(
    base / "LiMoS2_F_CHGCAR",
    format="chgcar",
    name="LiMoS2_F",
    kind="density",
    chgcar_dataset="total",
)
chgcar_a = read_grid_field(
    base / "LiMoS2_CHGCAR",
    format="chgcar",
    name="LiMoS2",
    kind="density",
    chgcar_dataset="total",
)
chgcar_b = read_grid_field(
    base / "F_CHGCAR",
    format="chgcar",
    name="F",
    kind="density",
    chgcar_dataset="total",
)

chgdiff_ab_minus_a = delta_grid_field(chgcar_ab, chgcar_a, sign="1-2")
chgdiff = delta_grid_field(chgdiff_ab_minus_a, chgcar_b, sign="1-2")
chgdiff = chgdiff.copy_with(name="LiMoS2_F_minus_LiMoS2_minus_F")

viewer = SurfaceViewer(
    chgdiff,
    surface_kind="delta_density",
    isovalue=0.01,
    positive_color="#f97316",
    negative_color="#2563eb",
    molecule=chgcar_ab.to_atoms(),
)
viewer.save_html("surface_limos2_f_chgdiff.html")

The same field can also be written explicitly when direct array arithmetic is clearer for an exploratory notebook:

from cube import require_same_grid

require_same_grid(chgcar_ab, chgcar_a)
require_same_grid(chgcar_ab, chgcar_b)

manual_chgdiff = chgcar_ab.copy_with(
    data=chgcar_ab.data - chgcar_a.data - chgcar_b.data,
    name="LiMoS2_F_minus_LiMoS2_minus_F",
    kind="delta_density",
)

viewer = SurfaceViewer(
    manual_chgdiff,
    surface_kind="delta_density",
    isovalue=0.01,
    positive_color="#f97316",
    negative_color="#2563eb",
    molecule=chgcar_ab.to_atoms(),
)
viewer.save_html("surface_limos2_f_chgdiff.html")

Precomputed CHGDIFF/charge-density-difference files can also be loaded directly with surface_kind="delta_density" when the file already contains signed difference data.

from aseview import SurfaceViewer

viewer = SurfaceViewer(
    "docs/assets/examples/surface/samples/chgdiff/zenodo_13752735_d24_diff.vasp",
    surface_kind="delta_density",
    isovalue=0.025,
    positive_color="#f97316",
    negative_color="#2563eb",
)
viewer.save_html("surface_zenodo_chgdiff_direct.html")

Additional CHGCAR, CHGDIFF, and CUBE fixtures are cataloged under docs/assets/examples/surface/samples/.

LiMoS2_F total density
LiMoS2 total density
F total density
LiMoS2_F - LiMoS2 - F
Precomputed CHGDIFF direct load

Multi-Isovalue Shells

from aseview import SurfaceViewer

viewer = SurfaceViewer(
    "HOMO.cube",
    surface_kind="orbital",
    isovalue="0.03:0.09:4",
    surface_color="#3b82f6",
    positive_color="#ef4444",
    negative_color="#2563eb",
    opacity_ramp=[0.25, 0.45, 0.65, 0.8],
    surface_style="solid",
)
viewer.show()

isovalue accepts explicit lists, comma-separated strings, start:stop:count, and linspace(start,stop,count) strings. For density surfaces, all shells use surface_color. For orbital and delta-density surfaces, each level creates a positive shell and a negative shell using positive_color and negative_color; opacity_ramp supplies the per-level opacity.

Contour Style

viewer = SurfaceViewer(
    "HOMO.cube",
    surface_kind="orbital",
    isovalue="0.03:0.09:4",
    surface_style="contour",
)
viewer.show()

Contour surfaces use a denser extraction grid by default and render horizontal slice lines instead of mesh edges, so a higher isovalue count produces a more closely spaced contour field.

Surface Sequences

SurfaceViewer.sequence() renders a frame list where each molecular frame can carry one or more volumetric surfaces. This is the direct Python contract and is best for scripts that already know the density files next to each trajectory frame:

The generated viewer uses the normal MolecularViewer animation controls: the frame slider and play button step through trajectory frames, while the Surface card switches between per-frame surfaces such as HOMO/LUMO. PNG and GIF export use the same toolbar buttons as MolecularViewer.

from aseview import SurfaceViewer

frames = [
    {
        "label": "step 0",
        "molecule": atoms_0,
        "surfaces": {
            "HOMO": "step_000/HOMO.cube",
            "LUMO": "step_000/LUMO.cube",
        },
    },
    {
        "label": "step 1",
        "molecule": atoms_1,
        "surfaces": {
            "HOMO": "step_001/HOMO.cube",
            "LUMO": "step_001/LUMO.cube",
        },
    },
]

viewer = SurfaceViewer.sequence(frames, surface_kind="orbital", isovalue=0.03)
viewer.save_html("homo-lumo-sequence.html")

For the CLI, put one frame object per line in JSONL. Paths are resolved relative to the manifest:

{"label":"step 0","molecule":{"symbols":["H"],"positions":[[0,0,0]]},"surfaces":{"HOMO":"step_000/HOMO.cube","LUMO":"step_000/LUMO.cube"}}
{"label":"step 1","molecule":{"symbols":["H"],"positions":[[0,0,0.1]]},"surfaces":{"HOMO":"step_001/HOMO.cube","LUMO":"step_001/LUMO.cube"}}
aseview frames.jsonl -v surface-sequence --surface-kind orbital --isovalue 0.03

The tracked Psi4 density sample uses an in-memory two-frame manifest over docs/assets/examples/surface/samples/cube/psi4_Da.cube then docs/assets/examples/surface/samples/cube/psi4_Db.cube:

from pathlib import Path

from aseview import SurfaceViewer

base = Path("docs/assets/examples/surface/samples/cube")
frames = [
    {
        "label": "Da density",
        "surfaces": [
            {
                "label": "Da density",
                "path": str(base / "psi4_Da.cube"),
                "surface_kind": "density",
                "isovalue": 0.0317329,
            }
        ],
    },
    {
        "label": "Db density",
        "surfaces": [
            {
                "label": "Db density",
                "path": str(base / "psi4_Db.cube"),
                "surface_kind": "density",
                "isovalue": 0.0317329,
            }
        ],
    },
]

viewer = SurfaceViewer.sequence(
    frames,
    surface_kind="density",
    isovalue=0.0317329,
    normalize_orbital=False,
)
viewer.save_html("surface_psi4_density_sequence.html")
Psi4 Da/Db density sequence

The embedded docs viewer keeps the sequence compact by rendering only the two tracked density frames, with labels Da density and Db density.

For archive-style trajectories, extxyz frame metadata can hold a surface path:

aseview traj.extxyz -v surface-sequence --surface-path-key surface_path --surface-kind density

v1 Scope

The first SurfaceViewer release does not implement ESP/NCI features. ESP colormaps, ESP colorbars, NCI modes, NCI color controls, solvent surfaces, and VS Code .cube editor integration are excluded from v1.