Coverage for ase / geometry / rdf.py: 98.90%
91 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1"""Radial distribution function (RDF)."""
3from __future__ import annotations
5from collections.abc import Iterable
6from math import pi
8import numpy as np
10from ase import Atoms
11from ase.cell import Cell
12from ase.neighborlist import NeighborList
13from ase.symbols import symbols2numbers
16class CellTooSmall(Exception):
17 pass
20class VolumeNotDefined(Exception):
21 pass
24def get_rdf(
25 atoms: Atoms | Iterable[Atoms],
26 rmax: float,
27 nbins: int,
28 distance_matrix: np.ndarray | None = None,
29 elements: Iterable[int | str] | None = None,
30 no_dists: bool = False,
31 volume: float | None = None,
32):
33 """Calculate the radial distribution function (RDF) :math:`g(r)`.
35 .. versionchanged:: 3.27.0
36 Partial RDFs are fixed to be consistent with LAMMPS ``compute rdf``.
37 They now satisfy :math:`g_{ij}(r) = g_{ji}(r)` and
38 :math:`g(r) = \\sum_{ij} c_i c_j g_{ij}(r)`, where `i` and `j` index
39 the elements and `c_{i,j}` are their atomic fractions.
41 Parameters
42 ----------
43 atoms : Atoms | Iterable[Atoms]
44 :class:`~ase.Atoms` or their iterable for which the RDF is computed.
46 .. versionadded:: 3.28.0
47 This can accept an iterable of :class:`~ase.Atoms`
48 to compute the average RDF over the images.
50 rmax : float
51 The maximum distance that will contribute to the RDF.
52 The unit cell should be large enough so that it encloses a
53 sphere with radius rmax in the periodic directions.
54 nbins : int
55 Number of bins to divide the RDF into.
56 distance_matrix : numpy.array
57 An array of distances between atoms, typically
58 obtained by atoms.get_all_distances().
59 Default None meaning that a NeighborList will be used.
60 elements : Iterable[int | str] | None, default: None
61 Two atomic numbers of chemical symbols. If not None, the partial
62 RDF for the supplied elements will be returned.
64 .. versionadded:: 3.28.0
65 Each item of ``elements`` can be a chemical element symbol.
67 no_dists : bool
68 If True then the second array with RDF distances will not be returned.
69 volume : float or None
70 Optionally specify the volume of the system. If specified, the volume
71 will be used instead atoms.cell.volume.
73 Returns
74 -------
75 rdf : np.ndarray
76 RDFs.
77 rr : np.ndarray
78 Corresponding distances.
80 Notes
81 -----
82 The RDF is computed following the standard solid state definition which uses
83 the cell volume in the normalization.
84 This may or may not be appropriate in cases where one or more directions is
85 non-periodic.
87 """
88 images = [atoms] if isinstance(atoms, Atoms) else atoms
89 rdfs = []
90 bin_edges = np.linspace(0.0, rmax, nbins + 1)
91 for atoms in images:
92 rdf = _get_rdf_per_frame(
93 atoms,
94 bin_edges,
95 distance_matrix=distance_matrix,
96 elements=elements,
97 volume=volume,
98 )
99 rdfs.append(rdf)
100 rdf = np.mean(rdfs, axis=0)
101 rr = 0.5 * (bin_edges[:-1] + bin_edges[1:]) # mid point of each bin
102 return rdf if no_dists else (rdf, rr)
105def _get_rdf_per_frame(
106 atoms: Atoms,
107 bin_edges: np.ndarray,
108 *,
109 elements: Iterable[int | str] | None = None,
110 distance_matrix: np.ndarray | None = None,
111 volume: float | None = None,
112) -> np.ndarray:
113 # First check whether the cell is sufficiently large
114 vol = atoms.cell.volume if volume is None else volume
115 if vol < 1.0e-10:
116 raise VolumeNotDefined
118 rmax = bin_edges.max()
119 nbins = bin_edges.size - 1
121 check_cell_and_r_max(atoms, rmax)
123 natoms = len(atoms)
124 dr = float(rmax / nbins)
126 atomic_numbers = None if elements is None else symbols2numbers(elements)
128 if atomic_numbers is None:
129 i_indices = np.arange(natoms)
130 n = natoms # number of center atoms
131 rho = natoms / vol # average number density
132 else:
133 i_indices = np.where(atoms.numbers == atomic_numbers[0])[0]
134 j_indices = np.where(atoms.numbers == atomic_numbers[1])[0]
135 n = len(i_indices) # number of center atoms
136 rho = len(j_indices) / vol # average number density
138 if distance_matrix is None:
139 nl = NeighborList(np.ones(natoms) * rmax * 0.5, bothways=True)
140 nl.update(atoms)
142 rdf = np.zeros(nbins + 1)
143 for i in i_indices:
144 j_indices, offsets = nl.get_neighbors(i)
145 if atomic_numbers is not None:
146 mask = atoms.numbers[j_indices] == atomic_numbers[1]
147 j_indices = j_indices[mask]
148 offsets = offsets[mask]
150 if np.count_nonzero(j_indices) == 0:
151 continue
153 ps = atoms.positions
154 d = ps[j_indices] + offsets @ atoms.cell - ps[i]
155 distances = np.sqrt(np.add.reduce(d**2, axis=1))
157 indices = np.asarray(np.ceil(distances / dr), dtype=int)
158 rdf += np.bincount(indices, minlength=nbins + 1)[: nbins + 1]
159 else:
160 indices = np.asarray(np.ceil(distance_matrix / dr), dtype=int)
161 if atomic_numbers is None:
162 x = indices.ravel()
163 else:
164 x = indices[i_indices][:, j_indices].ravel()
165 rdf = np.bincount(x, minlength=nbins + 1)[: nbins + 1].astype(float)
167 shell_vols = 4.0 * pi * dr * (bin_edges**2 + bin_edges * dr + dr**2 / 3.0)
168 rdf[1:] /= n * rho * shell_vols[:-1]
170 return rdf[1:]
173def check_cell_and_r_max(atoms: Atoms, rmax: float) -> None:
174 cell = atoms.get_cell()
175 pbc = atoms.get_pbc()
177 vol = atoms.cell.volume
179 for i in range(3):
180 if pbc[i]:
181 axb = np.cross(cell[(i + 1) % 3, :], cell[(i + 2) % 3, :])
182 h = vol / np.linalg.norm(axb)
183 if h < 2 * rmax:
184 recommended_r_max = get_recommended_r_max(cell, pbc)
185 raise CellTooSmall(
186 'The cell is not large enough in '
187 f'direction {i}: {h:.3f} < 2*rmax={2 * rmax: .3f}. '
188 f'Recommended rmax = {recommended_r_max}'
189 )
192def get_recommended_r_max(cell: Cell, pbc: list[bool]) -> float:
193 recommended_r_max = 5.0
194 vol = cell.volume
195 for i in range(3):
196 if pbc[i]:
197 axb = np.cross(
198 cell[(i + 1) % 3, :], # type: ignore[index]
199 cell[(i + 2) % 3, :], # type: ignore[index]
200 )
201 h = vol / np.linalg.norm(axb)
202 assert isinstance(h, float) # mypy
203 recommended_r_max = min(h / 2 * 0.99, recommended_r_max)
204 return recommended_r_max
207def get_containing_cell_length(atoms: Atoms) -> np.ndarray:
208 atom2xyz = atoms.get_positions()
209 return np.amax(atom2xyz, axis=0) - np.amin(atom2xyz, axis=0) + 2.0
212def get_volume_estimate(atoms: Atoms) -> float:
213 return np.prod(get_containing_cell_length(atoms))