Source code for torchani.potentials.zbl

import math
import typing as tp
import torch
from torch import Tensor
from torchani.cutoffs import CutoffArg
from torchani.potentials.core import BasePairPotential
from torchani.neighbors import Neighbors


# Ziegler-Biersack-Littman (ZBL) repulsion potential, which
# models screened nuclear repulsion
#  Chapter 2, "Universal screening fn" etc
# TODO: trainable
[docs] class RepulsionZBL(BasePairPotential): def __init__( self, symbols: tp.Sequence[str], # Potential k: float = 0.8853, # LAMMPS uses 0.46850 (internally uses angstrom) screen_coeffs: tp.Sequence[float] = (), screen_exponents: tp.Sequence[float] = (), eff_exponent: float = 0.23, eff_atomic_nums: tp.Sequence[float] = (), trainable: tp.Sequence[str] = (), *, # Cutoff cutoff: float = math.inf, cutoff_fn: CutoffArg = "smooth", ): super().__init__(symbols, cutoff=cutoff, cutoff_fn=cutoff_fn) eff_atomic_nums = self._validate_elem_seq( "eff_atomic_nums", eff_atomic_nums, torch.arange(118, dtype=torch.float).tolist(), ) if not len(screen_exponents) == len(screen_coeffs): raise ValueError( "screen_exponents and screen_coeffs must have the same len" ) # Defaults from Ziegler et. al. # Note: In Ziegler et.al. the parameters are: # coeffs: 0.1818, 0.5099 0.2802 0.02817 (typo in a part of the book) # exponents: 3.2, 0.9423, 0.4028, 0.2016 # These parameters come directly from the LAMMPS code if not screen_coeffs: # Lammps uses 0.02817 for the last coeff, and that is needed to make # the sum be 1. There is probably a typo in the SRIM book screen_coeffs = [0.18175, 0.50986, 0.28022, 0.02817] if not screen_exponents: screen_exponents = [3.19980, 0.94229, 0.40290, 0.20162] if not math.isclose(sum(screen_coeffs), 1.0): raise ValueError("Screen coeffs must sum to 1") # Params and buffers self.register_buffer( "_eff_atomic_nums", torch.tensor(eff_atomic_nums), persistent=False ) self.register_buffer( "_coeffs", torch.tensor(screen_coeffs).view(1, -1), persistent=False ) self.register_buffer( "_exponents", torch.tensor(screen_exponents).view(1, -1), persistent=False ) self._k = k self._kz = eff_exponent
[docs] def pair_energies(self, elem_idxs: Tensor, neighbors: Neighbors) -> Tensor: # Clamp distances to prevent singularities when dividing by zero # All internal calcs use atomic units, so convert to Bohr dists = self.clamp(neighbors.distances) * self.ANGSTROM_TO_BOHR elem_pairs = elem_idxs.view(-1)[neighbors.indices] eff_za = self._eff_atomic_nums[elem_pairs[0]] eff_zb = self._eff_atomic_nums[elem_pairs[1]] eff_coulomb_term = eff_za * eff_zb / dists reduced_dists = dists * (eff_za**self._kz + eff_zb**self._kz) / self._k screen_factor = self.screen_fn(reduced_dists) return eff_coulomb_term * screen_factor
def screen_fn(self, dists: Tensor) -> Tensor: # Output shape is same as input return (self._coeffs * torch.exp(-self._exponents * dists.view(-1, 1))).sum(-1)