ProteinMotion
Documentation · Density maps & slices

DOCUMENTATION / AUTHORING

Density maps & slices

Load MRC/CCP4 maps and animate contours and slices.

On this page

Load MRC or CCP4 maps, display a density contour, and move a colored slice through the volume. Both the native renderer and EEVEE support these objects. Gemmi, SciPy, and scikit-image are included in the normal package installation.

Complete example and output#

This example uses ubiquitin coordinates from PDB 1UBQ and its PDBe electron-density map. The camera focuses on helix residues 23–34. The contour changes from 1.5 to 2.5 sigma and back, then a slice crosses the selected region.

Code Full script
python
"""1UBQ electron density from PDBe, with a contour and a moving density slice.

Source: https://www.ebi.ac.uk/pdbe/coordinates/files/1ubq.ccp4
Retrieved 2026-09-17. The supplied map covers a complete crystallographic unit
cell. Cropping extends periodic data across the cell boundary when needed.
Sigma contours use the mean and standard deviation of the original map.
"""

from pathlib import Path

from proteinmotion import (
    ColorLegend,
    ColorScale,
    DensityMap,
    FadeIn,
    FadeOut,
    Protein,
    ProteinScene,
    Text,
)

DATA = Path(__file__).parent / "data"


class DensityMaps(ProteinScene):
    def construct(self):
        protein = Protein.from_file(DATA / "1ubq.cif").ball_and_stick().center()
        protein.set_residue_opacity(0.08)
        helix = protein.select(chain="A", residues=(23, 34))
        helix.set_opacity(1)
        density = DensityMap.from_file(DATA / "1ubq.ccp4")
        local = density.crop(helix, padding=2.5)
        shell = local.isosurface(1.5, opacity=0.28, follow=protein)
        scale = ColorScale(-0.5, 2, colors=("#10243d", "#438ca4", "#f5df93"))
        section = local.slice("z", 0.15, scale=scale, follow=protein, resolution=96)
        self.add(protein, shell)
        self.camera.frame(helix, margin=1.55)
        self.camera.orbit(theta=0.22, phi=0.18)
        self.add(Text("Electron density", position=(0.06, 0.07), font_size=42))
        self.add(Text("1UBQ · helix 23–34 · PDBe map", position=(0.06, 0.13), font_size=25))
        self.play(FadeIn(shell), run_time=1)
        self.play(shell.animate.set_level(2.5), self.camera.animate.orbit(theta=0.25), run_time=2)
        self.play(shell.animate.set_level(1.5), run_time=1.5)
        self.play(FadeIn(section), run_time=1)
        self.add(ColorLegend(scale, title="Map value", position=(0.06, 0.81)))
        self.play(section.animate.set_slice(0.85), run_time=3)
        self.play(FadeOut(section), run_time=1)
        self.wait(0.5)
Output Preview · 10.0 s · 60 fps
Electron density and slices

1UBQ PDBe density: animate the contour and move a slice past helix 23–34.

Download the script. The repository includes examples/data/1ubq.cif and 1ubq.ccp4:

bash
proteinmotion render examples/density_maps.py DensityMaps --fps 60 -o density.mp4

The map is experimental input from PDBe. It is independent of the molecular surfaces generated by protein.surface().

Load a map#

python
from proteinmotion import DensityMap

density = DensityMap.from_file("map.mrc")
shell = density.isosurface(1.5, units="sigma", opacity=0.3)
self.add(shell)
self.camera.frame(shell)

The reader supports MRC/CCP4 files and their .gz versions. It preserves cell dimensions, axis order, voxel spacing, non-orthogonal cell geometry, and cropped-map offsets. The default allocation limit is 32 million voxels; change max_voxels explicitly for larger maps.

Map origin conventions vary. origin="auto" uses a nonzero MRC ORIGIN field, otherwise the grid-start fields. Use origin="header" or origin="start" when your source uses another convention. Compare the map with known atoms before using an unfamiliar file. The MRC specification defines these fields.

Complete crystallographic unit-cell maps are periodic. Sampling and cropping can wrap across their boundaries. Cropped maps and EM volumes use their stored extent. ProteinMotion reads the stored grid and performs no crystallographic symmetry expansion.

For an in-memory array:

python
density = DensityMap(values_xyz, spacing=(1.2, 1.2, 1.5), origin=(10, 20, 30))

The array order is (x, y, z). Spacing and origin use ångströms. For a skew grid, supply basis=matrix, whose columns are the three voxel step vectors, instead of spacing. Values must be finite. density.sample(points) performs trilinear sampling; points outside a nonperiodic map return NaN.

Match a transformed protein#

python
protein = Protein.from_file("protein.cif").center()
local = density.crop(protein.select(chain="A", residues=(23, 34)), padding=3)
shell = local.isosurface(1.5, follow=protein)
self.add(protein, shell)

Coordinates and density must start in the same coordinate frame. follow=protein applies the protein's translation, rotation, and scale to the map object. The density remains a fixed measurement during protein deformation or trajectory playback.

crop extracts a box around the selected atoms, with padding in ångströms. It uses the atoms' coordinates before scene transforms. Crops keep the original map's mean and standard deviation for consistent sigma contours.

Contours and slices#

python
shell = density.isosurface(1.5, units="sigma", color="#75d5cb", opacity=0.3)
section = density.slice("z", 0.2, resolution=128)
self.add(shell, section)
self.play(shell.animate.set_level(2.5), run_time=2)
self.play(section.animate.set_slice(0.8), run_time=3)

With units="sigma", the contour is mean + level × standard deviation. units="absolute" uses the stored map value directly, including negative levels for difference maps. A contour outside the map's value range produces an empty surface.

A slice's position runs from 0 to 1 along the selected map axis. Slices use trilinear sampling on a regular plane; resolution sets the number of samples along each edge, from 2 to 512. The color scale stays fixed during motion. Supply a ColorScale and add a ColorLegend to explain the values. Native slices use unlit colors; EEVEE applies its output color transform to the 3D scene.

Contour extraction runs on the CPU and the resulting triangles render on the GPU. A fixed contour is cached. Animating its level rebuilds the mesh. Crop large maps or increase step_size to reduce the extraction cost. Moving slices resample one plane, so they are usually cheaper than changing a dense contour every frame.

View this page on GitHub