Skip to content

Rendering

mdapy.visualization.render draws a system by ray tracing: atoms as spheres, optionally bonds, the box, Voronoi cells, surfaces, dislocations and arrows of vectors, with ambient occlusion, shadows and outlines, in four styles. A Scene takes any spheres, cylinders and polygons for pictures made by hand.

import mdapy as mp
from mdapy.build import build_crystal, build_polycrystal
from mdapy.identify import CommonNeighborAnalysis
from mdapy.visualization import render

copper = build_crystal("fcc", 3.615, elements="Cu")
grains = build_polycrystal(copper, mp.Box(60.0), 6, seed=1)
structures = CommonNeighborAnalysis().compute(grains)

image = render(grains, color=structures, width=800, height=600)  # (600, 800, 4)
boundaries = render(
    grains, atoms=structures.structure != 1, color=structures, style="flat"
)
import numpy as np
from mdapy.visualization import STYLES, write_png

pictures = [
    render(grains, color=structures, style=style, width=400, height=300, device="auto")
    for style in STYLES
]
write_png("images/styles.png", np.hstack(pictures))

The styles

The four styles on a polycrystal colored by common neighbor analysis: fast, matte, shiny and flat.

Parameters

render(system, path=None, *, atoms=None, color=None, colormap="viridis", value_range=None, representation="spheres", radius=None, bonds=None, bond_radius=None, box=True, box_radius=None, box_color="black", polyhedra=None, polyhedra_alpha=0.6, polyhedra_color="atom", polyhedra_edges=None, mesh=None, mesh_color="#8fb0d8", mesh_alpha=1.0, dislocations=None, dislocation_radius=0.5, burgers_vectors=False, burgers_scale=3.0, burgers_radius=0.3, burgers_color=(0.7, 0.7, 0.7), vectors=None, vector_scale=1.0, vector_radius=0.5, vector_color=(1.0, 1.0, 0.0), vector_alignment="base", lines=None, line_radius=0.1, line_color=(0.6, 0.6, 0.6), wrap_lines=False, legend=False, **options)

Name Type Default Unit Meaning
path str or path None a PNG file to write
atoms mask or indices all the atoms shown
color the color column, else elements the colors of the atoms, below
colormap str "viridis" one of COLORMAPS, for values: below
value_range (float64, float64) finite range the values at the ends of the colormap
representation str "spheres" how atoms and bonds are drawn, below; it sets the defaults of radius, bonds and bond_radius
radius float32, (N,) float32 or str the radius column, else of the representation Å the radius of the atoms, or those of the elements: "display", "covalent" or "vdw"
bonds float64, NeighborList, "covalent" or "topology" of the representation Å bonds between atoms closer than a cutoff, the pairs of a list, covalent bonds, or the bonds of the topology of the system, such as those of a data file or of create_bonds
bond_radius float32 of the representation Å
box, box_radius bool, float32 True, 0.1 × radius Å the edges of the box
box_color color "black" their color
polyhedra VoronoiPolyhedra None the Voronoi cells of the atoms shown
polyhedra_alpha float32 0.6 their opacity
polyhedra_color str "atom" faces colored as their atoms, or "edges" by their number of edges
polyhedra_edges float32 0.05 × radius Å the radius of the edges of the cells, 0 for none
mesh TriangleMesh None a surface, such as that of a surface mesh; triangles across a periodic boundary are cut there and the part outside moved in through the opposite face
mesh_color color or (F, 3) float32 light blue one color, or one per triangle
mesh_alpha float32 1 its opacity
dislocations DislocationResult None dislocation lines, such as those of the dislocation analysis: tubes in the colors of the families of their Burgers vectors, cut at the periodic faces of the box
dislocation_radius float32 0.5 Å the radius of the tubes
burgers_vectors bool False also an arrow from the middle of each line along its Burgers vector in space
burgers_scale float32 3 the length of the arrows in Burgers vectors
burgers_radius float32 0.3 Å the radius of their shafts
burgers_color color or "family" light gray one color for the arrows, or the color of each line
vectors str or (N, 3) float32 None an arrow for each atom shown: a column of three components, such as displacement, velocities or forces, or vectors; below
vector_scale float32 1 the length of the arrows in units of the vectors; negative reverses them
vector_radius float32 0.5 Å the radius of their shafts
vector_color color or "atom" yellow one color for the arrows, or the color of each atom
vector_alignment str "base" the point of the arrow at its atom: base, center or head
lines (M, T, 3) float32 or list of (T, 3) float32 None Å polylines, such as the paths of trajectory_lines
line_radius, line_color float32, color 0.1, gray Å their radius and color
wrap_lines bool False cut the lines at the periodic faces of the box and move the parts outside in through the opposite faces
legend bool, str or mapping False a legend at the top left, which needs matplotlib: True gives the names of the structures or elements of the atoms shown with their colors, or a bar with the values and the colormap of a column or of (N,) values; a string does the same with this title; a mapping gives its own names and colors
Representation Atoms Bonds
spheres display radii none, radius 0.15 Å when given
ball_and_stick a quarter of the van der Waals radii covalent, radius 0.15 Å
space_filling van der Waals radii none
licorice as wide as the bonds covalent, radius 0.2 Å

color is one of: None for the color column of the system, which color_coding, color_by_type and assign_color set, or without one the colors of the elements; a single color, "#rrggbb", a name such as "red" or three numbers from 0 to 1; (N, 3) colors; a column name or (N,) values, through the colormap; or a structure result such as that of CommonNeighborAnalysis, in its colors. The colors and display radii of the elements are those of OVITO, and for the elements OVITO leaves out the colors of Jmol and the covalent radii; covalent radii are those of Cordero et al. (2008), van der Waals radii those of Alvarez (2013), 1.5 Å and 2 Å for elements without one. Covalent bonds, covalent_bonds(system), join the atoms closer than the sum of their covalent radii and 0.45 Å, as in Jmol. Bonds are drawn as two halves in the colors of their atoms; a bond to a periodic image is drawn as the half next to each atom only, so nothing leaves the box.

Colormaps

colormap(values, name="viridis", value_range=None) gives the (N, 3) colors of values; COLORMAPS holds the names. rainbow (OVITO's default), viridis, magma, jet, hot, blue_white_red and gray are computed as OVITO's color coding computes them, viridis and magma from their tables of 256 colors; plasma and coolwarm are MDAPY's own.

labels is for discrete values such as clusters, layers or grains: render(system, color="grain", colormap="labels", legend=True). Each value takes one of LABEL_COLORS, in increasing order, and the colors start again after the twelfth. It takes no value_range, and the legend lists the values with their colors, up to twelve of them.

The options are those of Scene.render:

Name Type Default Meaning
width, height int32 1200, 900 size in pixels
view str or (3,) float32 "iso" top, bottom, front, back, left, right, iso, or a direction of view
up (3,) float32 of the view the direction up in the picture
perspective, fov bool, float32 False, 30 a perspective camera and its vertical angle in degrees
zoom float32 1 magnification of the scene framed to fill the picture
focus (3,) float32 middle of the scene the point in Å at the middle of the picture
span float32 the scene framed the length in Å the picture shows from bottom to top at the focus, in place of the framing and zoom: with focus, the region a viewport shows
aperture float32 0 the radius in Å of the lens of a perspective camera: 0 for a picture sharp everywhere, more for depth of field
focal_distance float32 to the focus the distance in Å from the camera along the view to the plane that stays sharp
style str "matte" one of the styles below
background color or None "white" None for transparent
device str "cpu" "cpu", "gpu", or "auto" for the GPU when there is one; see below
progress, cancel None as in Parallelism
Style Look
fast quick, as the viewports of OVITO: an ambient part, a light from the viewer and a broad highlight from the upper left, neither occlusion nor shadows
matte matte spheres, as the pictures of the Tachyon renderer of OVITO: a bright sky darkened by ambient occlusion, a weak light from the viewer with sharp shadows
shiny shiny spheres with a metallic sheen, after the pictures of the OSPRay renderer of OVITO: light from a sky and a wide key light that bounces once between the atoms, soft shadows and small highlights; about three times slower than matte
flat plain colors, every atom outlined, as ASE draws atoms; for small systems

Any setting of a style can be changed by name: ambient, diffuse, specular, shininess, metalness (from 0 to 1: metals reflect less light diffusely and color their highlights), key, highlight and fill (directions toward the key light, the light of the highlights, by default the key light, and the fill light: right, up and toward the viewer), fill_intensity, base_light (light every surface gets, whatever hides it), srgb (light the colors as given in sRGB, as the renderers of OVITO do, in place of linear light: stronger contrast), ao_samples, ao_distance (Å, default 3 times the median radius), ao_strength, shadows, shadow_strength, flat, toon_levels, outline_width (pixels), outline_depth (Å), outline_color, fog, supersample (rays per pixel along each axis), samples (rays per subpixel, the first through its middle and the others anywhere in it: more for smoother soft shadows, depth of field and global illumination), light_size (the angular radius of the key light in radians, 0 for a point, wider for softer shadows) and, for global illumination, bounces (0 for none). style_settings(style) gives every setting of a style as a dictionary.

Depth of field and soft shadows

import numpy as np
from mdapy.visualization import write_png

camera = {"perspective": True, "fov": 40, "focus": [45, 15, 45], "span": 70}
sharp = render(grains, color=structures, width=480, height=360, **camera)
blurred = render(
    grains,
    color=structures,
    width=480,
    height=360,
    **camera,
    aperture=3.0,
    light_size=0.15,
    supersample=1,
    samples=32,
)
write_png("images/depth_of_field.png", np.hstack([sharp, blurred]))

Depth of field

The polycrystal through a pinhole, left, and through a lens 3 Å wide that keeps the front left around the focus sharp, lit by a key light 0.15 rad wide, right.

A perspective camera with an aperture sees through a lens of that radius: the plane focal_distance from the camera, through the focus unless given, stays sharp, and things blur the more the farther they are from it. A key light light_size radians wide casts soft shadows in every style. Both are sampled by the rays of each pixel and look grainy with few of them: raise samples for smoother pictures, at a cost in time that grows with it.

On the GPU

With device="gpu" the rays are traced on the GPU: with Metal on macOS, with Direct3D 12 on Windows, which comes with the system, and with Vulkan on Linux, whose loader and driver come with the graphics drivers of NVIDIA, AMD and Intel (a build from source needs Vulkan's headers, see Installation). gpu_available() tells whether there is a GPU. A driver that runs on the processor, as in a virtual machine without a GPU, does not count: MDAPY's own threads trace faster there, so device="auto" uses them. A GPU with ray tracing units uses them: on Linux and Windows those of NVIDIA RTX, AMD Radeon RX 6000 and later and Intel Arc, for every ray; on macOS those of M3 and later, for the rays of occlusion and shadows. The environment variable MDAPY_GPU_RAY_TRACING=0 turns them off. The picture is the same as on the processor, with the same random samples in the same precision, up to rounding at a few edges. For a million atoms at 1920 × 1080:

Device matte shiny
Apple M5, GPU 0.3 s 1.6 s
the same without its ray tracing units 0.9 s 2.2 s
Apple M5, processor 3.9 s 11.2 s
NVIDIA RTX A2000 0.4 s 1.0 s
the same without its ray tracing units 1.2 s 2.8 s
Intel Core i5-12600K, processor 4.2 s 11.8 s
NVIDIA RTX 4060 Laptop 0.2 s 0.5 s
the same without its ray tracing units 0.7 s 1.5 s
AMD Ryzen 9 7945HX, processor 2.3 s 7.4 s
from mdapy.visualization import gpu_available

device = "gpu" if gpu_available() else "cpu"
picture = render(grains, color=structures, device=device)

Molecules

Organic molecules are drawn with covalent bonds, found from the elements:

import numpy as np

# benzene: C-C 1.39 Å, C-H 1.09 Å
angles = np.arange(6) * np.pi / 3
ring = np.stack([np.cos(angles), np.sin(angles), np.zeros(6)], axis=1)
benzene = mp.System(
    np.vstack([1.39 * ring, 2.48 * ring]), elements=["C"] * 6 + ["H"] * 6
)
drawn = {
    representation: render(
        benzene,
        representation=representation,
        box=False,
        view=(0.3, 0.6, -1.0),
        style="shiny",
        width=400,
        height=300,
        device="auto",
        legend=True,
    )
    for representation in ("ball_and_stick", "licorice", "space_filling")
}
write_png("images/molecules.png", np.hstack(list(drawn.values())))

Molecules

Benzene in the representations ball_and_stick, licorice and space_filling, in the shiny style.

Vectors

An arrow along a vector of each atom, such as its displacement, velocity or force: here the displacements of an edge dislocation put into copper.

import numpy as np
from mdapy.build import build_crystal, insert_dislocation

# x along [1-10], y normal to the (111) glide plane, z along the line
crystal = build_crystal(
    "fcc",
    3.615,
    elements="Cu",
    orientation=[(1, -1, 0), (1, 1, 1), (-1, -1, 2)],
    repeat=(24, 6, 1),
)
center = crystal.box.matrix.diagonal() / 2 + [0.1, 0.1, 0.0]
edge = insert_dislocation(crystal, [3.615 / np.sqrt(2), 0, 0], line="z", center=center)
edge = edge.with_columns(displacement=edge["positions"] - crystal["positions"])
render(
    edge,
    "images/vectors.png",
    radius=0.5,
    vectors="displacement",
    vector_scale=2.5,
    vector_radius=0.12,
    vector_color=(0.85, 0.25, 0.2),
    view="top",
    width=800,
    height=440,
)

Displacements around an edge dislocation

The displacements of the atoms around an edge dislocation, 2.5 times their length, seen along the line: the planes above the glide plane open around the extra half plane and close below it.

The arrows are those of OVITO's vector display with normal shading: a shaft of vector_radius, which is OVITO's arrow width, and a head 2.5 times as wide and 4.5 times as long as that radius; an arrow shorter than its head is a cone only, narrowed in proportion. OVITO draws flat arrows facing the viewer by default. Zero vectors and atoms not shown get no arrow.

Scenes

import numpy as np
from mdapy.visualization import Scene

scene = Scene()
scene.add_spheres([[0, 0, 0], [2.5, 0, 0]], 1.0, [[0.8, 0.5, 0.2], [0.2, 0.5, 0.8]])
scene.add_cylinders([[0, 0, 0]], [[2.5, 0, 0]], 0.3, (0.5, 0.5, 0.5))
scene.add_triangles([[[-2, -2, -1.5], [4, -2, -1.5], [1, 3, -1.5]]], "gray", alpha=0.5)
picture = scene.render(width=400, height=300, style="shiny", view="front")
Method Adds
add_spheres(centers, radii, colors, *, ids=None) spheres
add_cylinders(starts, ends, radii, colors, *, ids=None) cylinders without caps
add_triangles(vertices, colors, *, alpha=1.0, ids=None) (T, 3, 3) triangles, seen from both sides, opaque or not
add_polygons(vertices, corner_offsets, corners, colors, *, alpha=1.0, ids=None) convex polygons, in the layout of VoronoiPolyhedra
add_box(box, *, radius=0.15, color="black") the twelve edges of a box

Outlines separate primitives of different ids. write_png(path, image) writes a picture; voxel_faces(values, box) gives the cells of a grid over the box as triangles on its faces, for mesh (see Isosurfaces and voxels); colormap(values, name, value_range), element_colors(elements) and element_radii(elements, kind="display") give the colors and radii used by render.

In a notebook

render returns the picture, and show_image shows it in a notebook such as Jupyter, without saving a file. It also takes a PNG or JPEG file, and width sets the width on the page in pixels. It needs IPython.

from mdapy.visualization import render, show_image

show_image(render(system))
show_image("picture.png", width=400)

Results

An (height, width, 4) uint8 array of RGBA pixels in sRGB, rows from the top, the same for any number of threads.

Errors

They raise InvalidParameter for an unknown style, view, colormap, color or option, a size that is not positive or is above the limits below, a negative aperture or one with an orthographic camera, a focal distance that is not positive, arrays of the wrong shape, an opacity out of [0, 1], a polygon of fewer than three corners, a negative id or an invalid mask of atoms; Cancelled when cancelled.

A picture has at most 8192 pixels on a side, and supersample times that side is at most 16384, as larger pictures can use up the memory: 8192 × 8192 pixels with supersample=2 take about 5 GB, and each doubling of the side four times as much.

Definition

Every pixel traces supersample² × samples rays from an orthographic or perspective camera into a bounding volume hierarchy of the primitives; through a lens, each ray starts at a random point of it and passes through the point where the ray from its middle meets the sharp plane. At the nearest hit, the color of the primitive, in linear RGB (or as given with srgb), is lit by base_light, by an ambient term weighted by ambient occlusion, the fraction of ao_samples rays that leave without hitting anything closer than ao_distance (cosine-weighted about the normal, one in each of ao_samples rings of equal weight, their turns a golden angle apart), by the diffuse light of the key light, darkened by its shadow toward a random point of the light, and of the fill light, and by a Phong highlight, the light from highlight mirrored about the normal toward the viewer. A metal reflects 1 − 0.6 metalness of the diffuse light and colors its highlights, as the rasterizer of OVITO approximates metals. See-through triangles add their color and let the ray go on. An outline is drawn on the pixels next to a different primitive that lies at least outline_depth closer, or next to the background. The rays of a pixel are averaged, over the background, into sRGB.

With bounces above 0, each ray instead follows samples paths, the first through the center of its part of the pixel and the others anywhere in it. At each surface a path adds the light of the key light, sampled over a disk light_size wide and blocked by anything in the way, then turns into a cosine-weighted direction about the normal (the first turns of the paths of a subpixel spread over rings of equal weight): if it leaves the scene it adds the sky, of brightness ambient, otherwise it goes on to the next surface, its light now colored by the surfaces it met, for at most bounces more surfaces. Shadows soften and corners darken without ambient occlusion, and colors bleed onto neighbors; the noise falls as the samples grow.