Skip to content

Voronoi analysis

VoronoiAnalysis divides space into the Voronoi cells of the atoms, the regions closer to one atom than to any other, and gives the volume and the coordination of each cell, and, when asked for, its faces, its Voronoi index and its shape as a polyhedron.

import numpy as np
from mdapy.analysis import VoronoiAnalysis
from mdapy.build import build_crystal

# an fcc crystal of 5 x 5 x 5 cubic cells, atoms displaced by about 0.05 Å
crystal = build_crystal("fcc", 3.6, elements="Cu", repeat=(5, 5, 5))
shake = np.random.default_rng(0).normal(scale=0.05, size=(len(crystal), 3))
system = crystal.with_columns(positions=crystal["positions"] + shake)

voronoi = VoronoiAnalysis(relative_face_threshold=0.01, edge_threshold=0.5)
result = voronoi.compute(system)
result.volume  # ų, a³/4 = 11.66 on average
result.coordination  # 12 for every atom
system = system.with_columns(result)  # voronoi_volume, ..., cavity_radius

# the faces too: the atom across each, its area and its edges
faces = VoronoiAnalysis(relative_face_threshold=0.01, edge_threshold=0.5, faces=True)
result = faces.compute(system)
result.index(6)  # faces with 3, 4, 5 and 6 edges: [0, 12, 0, 0]

shapes = VoronoiAnalysis(polyhedra=True).compute(system).polyhedra
shapes.vertices  # Å, the corners of every cell

The cells of the atoms near the middle of an fcc and a bcc crystal, drawn by the renderer:

from mdapy.build import build_crystal
from mdapy.visualization import render, write_png

pictures = []
for crystal in (
    build_crystal("fcc", 3.6, elements="Cu", repeat=(3, 3, 3)),
    build_crystal("bcc", 2.87, elements="Fe", repeat=(4, 4, 4)),
):
    cells = VoronoiAnalysis(polyhedra=True).compute(crystal).polyhedra
    middle = crystal.box.matrix.diagonal() / 2
    near = np.linalg.norm(crystal["positions"] - middle, axis=1) < 3.0
    pictures.append(
        render(crystal, atoms=near, polyhedra=cells, box=False, width=600, height=600)
    )
write_png("images/voronoi.png", np.hstack(pictures))

Voronoi cells of fcc and bcc crystals

Left: in fcc every cell is a rhombic dodecahedron of 12 faces. Right: in bcc a truncated octahedron of 8 hexagons, toward the nearest neighbors, and 6 squares, toward the second neighbors.

Parameters

Name Type Default Unit Meaning
face_threshold float64 0 Ų faces of this area or less do not count
relative_face_threshold float64 0 faces of this fraction of the surface of their cell or less do not count; from 0 to below 1
edge_threshold float64 0 Å edges shorter than this do not count, and faces with fewer than three edges that count do not count
faces bool False also give the faces of each cell that count, with the atom across each, its area and its edges
polyhedra bool False also give the shape of each cell

Without faces, the faces of each cell are counted and let go as soon as its volume and coordination are known, so that the memory a computation takes does not grow with the faces: some 14 per atom, which take as much as the rest of the result several times over.

compute(system, *, progress=None, cancel=None) takes the progress and cancel of every analysis, but no neighbor list: the neighbors of each cell are searched as far as the cell needs. No two atoms may be at the same position. Directions that are not periodic are closed by the faces of the box, moved out as far as needed to include every atom.

The thresholds remove the small faces and short edges that thermal motion creates where a perfect crystal has a vertex shared by more than four cells, as in fcc; they do not change the volume, the radius or the shapes. The atoms across the faces are also a kind of neighbor list, built with NeighborList.build(system, voronoi=True) and the same thresholds.

Results

A VoronoiResult:

Attribute or method Type Meaning
volume (N,) float64 the volume of each cell in ų; its column voronoi_volume
coordination (N,) int64 the number of faces that count shared with other atoms; its column voronoi_coordination
radius (N,) float64 the distance from each atom to the farthest vertex of its cell in Å; its column cavity_radius
offsets (N + 1,) int64 the faces that count of atom i are offsets[i]:offsets[i + 1]; with faces, else None
j (F,) mdapy.index_dtype the atom across each face, −1 for a wall of the box; with faces
shifts (F, 3) int8 the periodic image of that atom; with faces
areas (F,) float64 the area of each face in Ų; with faces
edges (F,) int32 the number of edges of each face that count; with faces
index(largest=8) (N, largest − 2) int64 the Voronoi index: the number of faces with 3, 4, ..., largest edges shared with other atoms; with faces
polyhedra VoronoiPolyhedra or None the shapes of the cells, with polyhedra=True

The faces are listed like the pairs of a neighbor list: the vector from atom i to the atom across a face is x[j] - x[i] + shifts @ box.matrix.

The walls of the box are faces of the cells of the atoms next to them, in the faces with j = −1, but they are not neighbors: coordination, index and the Voronoi neighbors count only the faces shared with atoms. OVITO and LAMMPS count the walls in the coordination and the Voronoi index as well, so their values are larger for atoms next to a wall.

A VoronoiPolyhedra holds every face of every cell, those that do not count included, so that each cell is a closed polyhedron, for example to draw it:

Attribute Type Meaning
vertices (V, 3) float64 the corners of the cells in Å, each cell with its own; a cell may reach out of the box
vertex_offsets (N + 1,) int64 the vertices of the cell of atom i are vertex_offsets[i]:vertex_offsets[i + 1]
face_offsets (N + 1,) int64 the faces of the cell of atom i are face_offsets[i]:face_offsets[i + 1]
corner_offsets (F + 1,) int64 the corners of face f are corners[corner_offsets[f]:corner_offsets[f + 1]]
corners (C,) int64 the vertex of each corner, counterclockwise seen from outside the cell
j (F,) mdapy.index_dtype the atom across each face, −1 for a wall of the box
areas (F,) float64 the area of each face in Ų
f = shapes.face_offsets[0]  # the first face of the cell of atom 0
loop = shapes.corners[shapes.corner_offsets[f] : shapes.corner_offsets[f + 1]]
polygon = shapes.vertices[loop]  # its corners in order

Errors

The constructor raises InvalidParameter for a negative threshold or a relative_face_threshold of 1 or more; compute raises it for two atoms at the same position, and index for a largest below 3.

Definition

The Voronoi cell of atom \(i\) at \(\mathbf r_i\) is the set of points closer to it than to any other atom or periodic image,

\[ V_i = \{\, \mathbf x : \lvert \mathbf x - \mathbf r_i \rvert \le \lvert \mathbf x - \mathbf r_j - \mathbf s H \rvert \text{ for every } (j, \mathbf s) \ne (i, \mathbf 0) \,\}, \]

with \(H\) the matrix of the lattice vectors as rows. It is a convex polyhedron whose faces lie on the planes halfway to its neighbors, or on a wall of the box in a direction that is not periodic. A face counts when its area \(A\) and its edges satisfy

\[ A > \max(A_\text{min},\; f\, S_i) \quad\text{and}\quad \#\{\text{edges of length} \ge \ell_\text{min}\} \ge 3 , \]

with \(A_\text{min}\) the face_threshold, \(f\) the relative_face_threshold, \(\ell_\text{min}\) the edge_threshold and \(S_i\) the surface of the cell, walls included. The coordination is the number of faces that count shared with atoms, and the Voronoi index \(\langle n_3, n_4, n_5, \dots \rangle\) the number of these faces with 3, 4, 5, ... edges that count. The radius, the distance to the farthest vertex of the cell, is that of the largest sphere centered on a vertex of the cell with no atom inside.

Crystal Cell Faces Volume
fcc rhombic dodecahedron 12 rhombi \(a^3/4\)
bcc truncated octahedron 6 squares, 8 hexagons \(a^3/2\)
hcp trapezo-rhombic dodecahedron 12 quadrilaterals \(\sqrt 2\, a^3 / 2\)
cubic diamond 12 triangles, 4 hexagons \(a^3/8\)

Please cite

Voronoi (1908).

See also