Source code for ase.geometry.rdf

"""Radial distribution function (RDF)."""

from __future__ import annotations

import math

import numpy as np

from ase import Atoms
from ase.cell import Cell
from ase.neighborlist import NeighborList


class CellTooSmall(Exception):
    pass


class VolumeNotDefined(Exception):
    pass


[docs] def get_rdf( atoms: Atoms, rmax: float, nbins: int, distance_matrix: np.ndarray | None = None, elements: list[int] | tuple | None = None, no_dists: bool = False, volume: float | None = None, ): """Calculate the radial distribution function (RDF) :math:`g(r)`. .. versionchanged:: 3.27.0 Partial RDFs are fixed to be consistent with LAMMPS ``compute rdf``. They now satisfy :math:`g_{ij}(r) = g_{ji}(r)` and :math:`g(r) = \\sum_{ij} c_i c_j g_{ij}(r)`, where `i` and `j` index the elements and `c_{i,j}` are their atomic fractions. Parameters ---------- atoms : Atoms ASE ``Atoms`` object for which the RDF is computed. rmax : float The maximum distance that will contribute to the RDF. The unit cell should be large enough so that it encloses a sphere with radius rmax in the periodic directions. nbins : int Number of bins to divide the RDF into. distance_matrix : numpy.array An array of distances between atoms, typically obtained by atoms.get_all_distances(). Default None meaning that a NeighborList will be used. elements : list[int] | tuple[int, int] List of two atomic numbers. If elements is not None the partial RDF for the supplied elements will be returned. no_dists : bool If True then the second array with RDF distances will not be returned. volume : float or None Optionally specify the volume of the system. If specified, the volume will be used instead atoms.cell.volume. Returns ------- rdf : np.ndarray RDFs. rr : np.ndarray Corresponding distances. Notes ----- The RDF is computed following the standard solid state definition which uses the cell volume in the normalization. This may or may not be appropriate in cases where one or more directions is non-periodic. """ # First check whether the cell is sufficiently large vol = atoms.cell.volume if volume is None else volume if vol < 1.0e-10: raise VolumeNotDefined check_cell_and_r_max(atoms, rmax) natoms = len(atoms) dr = float(rmax / nbins) if elements is None: i_indices = np.arange(natoms) n = natoms # number of center atoms rho = natoms / vol # average number density else: i_indices = np.where(atoms.numbers == elements[0])[0] j_indices = np.where(atoms.numbers == elements[1])[0] n = len(i_indices) # number of center atoms rho = len(j_indices) / vol # average number density if distance_matrix is None: nl = NeighborList(np.ones(natoms) * rmax * 0.5, bothways=True) nl.update(atoms) rdf = np.zeros(nbins + 1) for i in i_indices: j_indices, offsets = nl.get_neighbors(i) if elements is not None: mask = atoms.numbers[j_indices] == elements[1] j_indices = j_indices[mask] offsets = offsets[mask] if np.count_nonzero(j_indices) == 0: continue ps = atoms.positions d = ps[j_indices] + offsets @ atoms.cell - ps[i] distances = np.sqrt(np.add.reduce(d**2, axis=1)) indices = np.asarray(np.ceil(distances / dr), dtype=int) rdf += np.bincount(indices, minlength=nbins + 1)[: nbins + 1] else: indices = np.asarray(np.ceil(distance_matrix / dr), dtype=int) if elements is None: x = indices.ravel() else: x = indices[i_indices][:, j_indices].ravel() rdf = np.bincount(x, minlength=nbins + 1)[: nbins + 1].astype(float) rr = np.arange(dr / 2, rmax, dr) shell_volumes = 4.0 * math.pi * dr * (rr * rr + (dr * dr / 12)) rdf[1:] /= n * rho * shell_volumes if no_dists: return rdf[1:] return rdf[1:], rr
def check_cell_and_r_max(atoms: Atoms, rmax: float) -> None: cell = atoms.get_cell() pbc = atoms.get_pbc() vol = atoms.cell.volume for i in range(3): if pbc[i]: axb = np.cross(cell[(i + 1) % 3, :], cell[(i + 2) % 3, :]) h = vol / np.linalg.norm(axb) if h < 2 * rmax: recommended_r_max = get_recommended_r_max(cell, pbc) raise CellTooSmall( 'The cell is not large enough in ' f'direction {i}: {h:.3f} < 2*rmax={2 * rmax: .3f}. ' f'Recommended rmax = {recommended_r_max}' ) def get_recommended_r_max(cell: Cell, pbc: list[bool]) -> float: recommended_r_max = 5.0 vol = cell.volume for i in range(3): if pbc[i]: axb = np.cross( cell[(i + 1) % 3, :], # type: ignore[index] cell[(i + 2) % 3, :], # type: ignore[index] ) h = vol / np.linalg.norm(axb) assert isinstance(h, float) # mypy recommended_r_max = min(h / 2 * 0.99, recommended_r_max) return recommended_r_max def get_containing_cell_length(atoms: Atoms) -> np.ndarray: atom2xyz = atoms.get_positions() return np.amax(atom2xyz, axis=0) - np.amin(atom2xyz, axis=0) + 2.0 def get_volume_estimate(atoms: Atoms) -> float: return np.prod(get_containing_cell_length(atoms))