Source code for xnn.hybrid.featurizers.radial
"""Radial basis of the hybrid family: the exponential Bernstein polynomials of SpookyNet."""
import math
import torch
from torch import Tensor, nn
from xnn.common.models.ops import softplus_inverse
[docs]
class ExponentialBernsteinRBF(nn.Module):
r"""Exponential Bernstein polynomials, the radial basis of SpookyNet.
Unke *et al.*, *Nat. Commun.* 12, 7273 (2021), eqs 14 and 15: the
distance is mapped to ``x = exp(-gamma r)`` in ``(0, 1]`` and expanded in
the ``K`` Bernstein polynomials of degree ``K - 1``,
``b_k(x) = binom(K - 1, k) x^k (1 - x)^(K - 1 - k)``, ``k = 0, ..., K - 1``,
which form a partition of unity and approximate any continuous function
of ``x`` as ``K`` grows. The decay ``gamma`` is one positive parameter
shared by all functions (kept positive through a softplus), so the rate
at which a learned radial function can vary falls off with the distance.
The polynomials are evaluated in log space,
``log binom + k log x + (K - 1 - k) log(1 - x)``, which keeps the large
binomials and small powers apart. A cutoff envelope is not included; the
model multiplies it in.
Parameters
----------
n_rbf : int, optional
Number of basis functions ``K``, by default 16.
gamma : float, optional
Initial decay ``gamma`` in inverse length units, by default 0.5 / bohr
(0.9449 / Angstrom), the paper's value.
trainable : bool, optional
Learn ``gamma``, by default ``True`` (the paper).
exp_weighting : bool, optional
Multiply every function by ``x = exp(-gamma r)``, an option of the
reference code that is not part of the paper's eq 14. Default
``False``.
Attributes
----------
gamma_raw : Tensor
``softplus^-1(gamma)``, a parameter when ``trainable``.
"""
__jit_unused_properties__ = ["gamma"]
def __init__(self, n_rbf: int = 16, gamma: float = 0.5 / 0.5291772109044924,
trainable: bool = True, exp_weighting: bool = False):
super().__init__()
self.n_rbf = int(n_rbf)
self.exp_weighting = bool(exp_weighting)
k = torch.arange(self.n_rbf, dtype=torch.float64)
n = float(self.n_rbf - 1)
log_binom = (math.lgamma(n + 1.0) - torch.lgamma(k + 1.0)
- torch.lgamma(n - k + 1.0))
dtype = torch.get_default_dtype()
self.register_buffer("log_binom", log_binom.to(dtype), persistent=False)
self.register_buffer("power_x", k.to(dtype), persistent=False)
self.register_buffer("power_1mx", (n - k).to(dtype), persistent=False)
raw = torch.tensor(float(softplus_inverse(float(gamma))), dtype=dtype)
if trainable:
self.gamma_raw = nn.Parameter(raw)
else:
self.register_buffer("gamma_raw", raw)
@property
def gamma(self) -> Tensor:
"""The decay ``gamma = softplus(gamma_raw)``."""
return nn.functional.softplus(self.gamma_raw)
[docs]
def exact_constants(self) -> bool:
"""Rebuild float64 log-binomials exactly after a cast; whether they changed."""
buf = self.log_binom
if buf.dtype != torch.float64:
return False
k = torch.arange(self.n_rbf, dtype=torch.float64, device=buf.device)
n = float(self.n_rbf - 1)
exact = math.lgamma(n + 1.0) - torch.lgamma(k + 1.0) - torch.lgamma(n - k + 1.0)
if torch.equal(buf, exact):
return False
with torch.no_grad():
buf.copy_(exact)
return True
[docs]
def forward(self, r: Tensor) -> Tensor:
"""Expand distances into the basis.
Parameters
----------
r : Tensor
Interatomic distances ``(E,)``, positive.
Returns
-------
Tensor
``(E, n_rbf)``; column ``k`` is ``b_k(exp(-gamma r))``.
"""
log_x = -nn.functional.softplus(self.gamma_raw) * r.unsqueeze(-1)
log_b = (self.log_binom + self.power_x * log_x
+ self.power_1mx * torch.log(-torch.expm1(log_x)))
rbf = torch.exp(log_b)
if self.exp_weighting:
rbf = rbf * torch.exp(log_x)
return rbf