Coverage for /builds/ase/ase/ase/geometry/rdf.py: 100.00%
66 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import math
4from typing import List, Optional, Tuple, Union
6import numpy as np
8from ase import Atoms
9from ase.cell import Cell
12class CellTooSmall(Exception):
13 pass
16class VolumeNotDefined(Exception):
17 pass
20def get_rdf(atoms: Atoms, rmax: float, nbins: int,
21 distance_matrix: Optional[np.ndarray] = None,
22 elements: Optional[Union[List[int], Tuple]] = None,
23 no_dists: Optional[bool] = False,
24 volume: Optional[float] = None):
25 """Returns two numpy arrays; the radial distribution function
26 and the corresponding distances of the supplied atoms object.
27 If no_dists = True then only the first array is returned.
29 Note that the rdf is computed following the standard solid state
30 definition which uses the cell volume in the normalization.
31 This may or may not be appropriate in cases where one or more
32 directions is non-periodic.
34 Parameters:
36 rmax : float
37 The maximum distance that will contribute to the rdf.
38 The unit cell should be large enough so that it encloses a
39 sphere with radius rmax in the periodic directions.
41 nbins : int
42 Number of bins to divide the rdf into.
44 distance_matrix : numpy.array
45 An array of distances between atoms, typically
46 obtained by atoms.get_all_distances().
47 Default None meaning that it will be calculated.
49 elements : list or tuple
50 List of two atomic numbers. If elements is not None the partial
51 rdf for the supplied elements will be returned.
53 no_dists : bool
54 If True then the second array with rdf distances will not be returned.
56 volume : float or None
57 Optionally specify the volume of the system. If specified, the volume
58 will be used instead atoms.cell.volume.
59 """
61 # First check whether the cell is sufficiently large
62 vol = atoms.cell.volume if volume is None else volume
63 if vol < 1.0e-10:
64 raise VolumeNotDefined
66 check_cell_and_r_max(atoms, rmax)
68 dm = distance_matrix
69 if dm is None:
70 dm = atoms.get_all_distances(mic=True)
72 rdf = np.zeros(nbins + 1)
73 dr = float(rmax / nbins)
75 indices = np.asarray(np.ceil(dm / dr), dtype=int)
76 natoms = len(atoms)
78 if elements is None:
79 # Coefficients to use for normalization
80 phi = natoms / vol
81 norm = 2.0 * math.pi * dr * phi * len(atoms)
83 indices_triu = np.triu(indices)
84 for index in range(nbins + 1):
85 rdf[index] = np.count_nonzero(indices_triu == index)
87 else:
88 i_indices = np.where(atoms.numbers == elements[0])[0]
89 phi = len(i_indices) / vol
90 norm = 4.0 * math.pi * dr * phi * natoms
92 for i in i_indices:
93 for j in np.where(atoms.numbers == elements[1])[0]:
94 index = indices[i, j]
95 if index <= nbins:
96 rdf[index] += 1
98 rr = np.arange(dr / 2, rmax, dr)
99 rdf[1:] /= norm * (rr * rr + (dr * dr / 12))
101 if no_dists:
102 return rdf[1:]
104 return rdf[1:], rr
107def check_cell_and_r_max(atoms: Atoms, rmax: float) -> None:
108 cell = atoms.get_cell()
109 pbc = atoms.get_pbc()
111 vol = atoms.cell.volume
113 for i in range(3):
114 if pbc[i]:
115 axb = np.cross(cell[(i + 1) % 3, :], cell[(i + 2) % 3, :])
116 h = vol / np.linalg.norm(axb)
117 if h < 2 * rmax:
118 recommended_r_max = get_recommended_r_max(cell, pbc)
119 raise CellTooSmall(
120 'The cell is not large enough in '
121 f'direction {i}: {h:.3f} < 2*rmax={2 * rmax: .3f}. '
122 f'Recommended rmax = {recommended_r_max}')
125def get_recommended_r_max(cell: Cell, pbc: List[bool]) -> float:
126 recommended_r_max = 5.0
127 vol = cell.volume
128 for i in range(3):
129 if pbc[i]:
130 axb = np.cross(cell[(i + 1) % 3, :], # type: ignore[index]
131 cell[(i + 2) % 3, :]) # type: ignore[index]
132 h = vol / np.linalg.norm(axb)
133 assert isinstance(h, float) # mypy
134 recommended_r_max = min(h / 2 * 0.99, recommended_r_max)
135 return recommended_r_max
138def get_containing_cell_length(atoms: Atoms) -> np.ndarray:
139 atom2xyz = atoms.get_positions()
140 return np.amax(atom2xyz, axis=0) - np.amin(atom2xyz, axis=0) + 2.0
143def get_volume_estimate(atoms: Atoms) -> float:
144 return np.prod(get_containing_cell_length(atoms))