Download EPT/models/utils.py from BAAI/AIDD: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/BAAI/AIDD/resolve/main/EPT/models/utils.py
- Command line
-
hf download hf://BAAI/AIDD/EPT/models/utils.py
-
curl -L -o utils.py https://huggingface.co/BAAI/AIDD/resolve/main/EPT/models/utils.py
12 kB
| """ | |
| Copyright (c) Facebook, Inc. and its affiliates. | |
| This source code is licensed under the MIT license found in the | |
| LICENSE file in the root directory of this source tree. | |
| """ | |
| import math | |
| import numpy as np | |
| import torch | |
| from scipy.special import binom | |
| from torch_geometric.nn.models.schnet import GaussianSmearing | |
| class PolynomialEnvelope(torch.nn.Module): | |
| """ | |
| Polynomial envelope function that ensures a smooth cutoff. | |
| Parameters | |
| ---------- | |
| exponent: int | |
| Exponent of the envelope function. | |
| """ | |
| def __init__(self, exponent): | |
| super().__init__() | |
| assert exponent > 0 | |
| self.p = exponent | |
| self.a = -(self.p + 1) * (self.p + 2) / 2 | |
| self.b = self.p * (self.p + 2) | |
| self.c = -self.p * (self.p + 1) / 2 | |
| def forward(self, d_scaled): | |
| env_val = ( | |
| 1 | |
| + self.a * d_scaled ** self.p | |
| + self.b * d_scaled ** (self.p + 1) | |
| + self.c * d_scaled ** (self.p + 2) | |
| ) | |
| return torch.where(d_scaled < 1, env_val, torch.zeros_like(d_scaled)) | |
| class ExponentialEnvelope(torch.nn.Module): | |
| """ | |
| Exponential envelope function that ensures a smooth cutoff, | |
| as proposed in Unke, Chmiela, Gastegger, Schütt, Sauceda, Müller 2021. | |
| SpookyNet: Learning Force Fields with Electronic Degrees of Freedom | |
| and Nonlocal Effects | |
| """ | |
| def __init__(self): | |
| super().__init__() | |
| def forward(self, d_scaled): | |
| env_val = torch.exp( | |
| -(d_scaled ** 2) / ((1 - d_scaled) * (1 + d_scaled)) | |
| ) | |
| return torch.where(d_scaled < 1, env_val, torch.zeros_like(d_scaled)) | |
| class SphericalBesselBasis(torch.nn.Module): | |
| """ | |
| 1D spherical Bessel basis | |
| Parameters | |
| ---------- | |
| num_radial: int | |
| Controls maximum frequency. | |
| cutoff: float | |
| Cutoff distance in Angstrom. | |
| """ | |
| def __init__( | |
| self, | |
| num_radial: int, | |
| cutoff: float, | |
| ): | |
| super().__init__() | |
| self.norm_const = math.sqrt(2 / (cutoff ** 3)) | |
| # cutoff ** 3 to counteract dividing by d_scaled = d / cutoff | |
| # Initialize frequencies at canonical positions | |
| self.frequencies = torch.nn.Parameter( | |
| data=torch.tensor( | |
| np.pi * np.arange(1, num_radial + 1, dtype=np.float32) | |
| ), | |
| requires_grad=True, | |
| ) | |
| def forward(self, d_scaled): | |
| return ( | |
| self.norm_const | |
| / d_scaled[:, None] | |
| * torch.sin(self.frequencies * d_scaled[:, None]) | |
| ) # (num_edges, num_radial) | |
| class BernsteinBasis(torch.nn.Module): | |
| """ | |
| Bernstein polynomial basis, | |
| as proposed in Unke, Chmiela, Gastegger, Schütt, Sauceda, Müller 2021. | |
| SpookyNet: Learning Force Fields with Electronic Degrees of Freedom | |
| and Nonlocal Effects | |
| Parameters | |
| ---------- | |
| num_radial: int | |
| Controls maximum frequency. | |
| pregamma_initial: float | |
| Initial value of exponential coefficient gamma. | |
| Default: gamma = 0.5 * a_0**-1 = 0.94486, | |
| inverse softplus -> pregamma = log e**gamma - 1 = 0.45264 | |
| """ | |
| def __init__( | |
| self, | |
| num_radial: int, | |
| pregamma_initial: float = 0.45264, | |
| ): | |
| super().__init__() | |
| prefactor = binom(num_radial - 1, np.arange(num_radial)) | |
| self.register_buffer( | |
| "prefactor", | |
| torch.tensor(prefactor, dtype=torch.float), | |
| persistent=False, | |
| ) | |
| self.pregamma = torch.nn.Parameter( | |
| data=torch.tensor(pregamma_initial, dtype=torch.float), | |
| requires_grad=True, | |
| ) | |
| self.softplus = torch.nn.Softplus() | |
| exp1 = torch.arange(num_radial) | |
| self.register_buffer("exp1", exp1[None, :], persistent=False) | |
| exp2 = num_radial - 1 - exp1 | |
| self.register_buffer("exp2", exp2[None, :], persistent=False) | |
| def forward(self, d_scaled): | |
| gamma = self.softplus(self.pregamma) # constrain to positive | |
| exp_d = torch.exp(-gamma * d_scaled)[:, None] | |
| return ( | |
| self.prefactor * (exp_d ** self.exp1) * ((1 - exp_d) ** self.exp2) | |
| ) | |
| class RadialBasis(torch.nn.Module): | |
| """ | |
| Parameters | |
| ---------- | |
| num_radial: int | |
| Controls maximum frequency. | |
| cutoff: float | |
| Cutoff distance in Angstrom. | |
| rbf: dict = {"name": "gaussian"} | |
| Basis function and its hyperparameters. | |
| envelope: dict = {"name": "polynomial", "exponent": 5} | |
| Envelope function and its hyperparameters. | |
| """ | |
| def __init__( | |
| self, | |
| num_radial: int, | |
| cutoff: float, | |
| rbf: dict = {"name": "gaussian"}, | |
| envelope: dict = {"name": "polynomial", "exponent": 5}, | |
| ): | |
| super().__init__() | |
| self.inv_cutoff = 1 / cutoff | |
| env_name = envelope["name"].lower() | |
| env_hparams = envelope.copy() | |
| del env_hparams["name"] | |
| if env_name == "polynomial": | |
| self.envelope = PolynomialEnvelope(**env_hparams) | |
| elif env_name == "exponential": | |
| self.envelope = ExponentialEnvelope(**env_hparams) | |
| else: | |
| raise ValueError(f"Unknown envelope function '{env_name}'.") | |
| rbf_name = rbf["name"].lower() | |
| rbf_hparams = rbf.copy() | |
| del rbf_hparams["name"] | |
| # RBFs get distances scaled to be in [0, 1] | |
| if rbf_name == "gaussian": | |
| self.rbf = GaussianSmearing( | |
| start=0, stop=1, num_gaussians=num_radial, **rbf_hparams | |
| ) | |
| elif rbf_name == "spherical_bessel": | |
| self.rbf = SphericalBesselBasis( | |
| num_radial=num_radial, cutoff=cutoff, **rbf_hparams | |
| ) | |
| elif rbf_name == "bernstein": | |
| self.rbf = BernsteinBasis(num_radial=num_radial, **rbf_hparams) | |
| else: | |
| raise ValueError(f"Unknown radial basis function '{rbf_name}'.") | |
| def forward(self, d): | |
| d_scaled = d * self.inv_cutoff | |
| env = self.envelope(d_scaled) | |
| return env[:, None] * self.rbf(d_scaled) # (nEdges, num_radial) | |
| def _unit_edges_from_block_edges(unit_block_id, block_src_dst, Z=None, k=None): | |
| ''' | |
| :param unit_block_id [N], id of block of each unit. Assume X is sorted so that block_id starts from 0 to Nb - 1 | |
| :param block_src_dst: [Eb, 2], all edges (block level), represented in (src, dst) | |
| ''' | |
| block_n_units = scatter_sum(torch.ones_like(unit_block_id), unit_block_id) # [Nb], number of units in each block | |
| block_offsets = F.pad(torch.cumsum(block_n_units[:-1], dim=0), (1, 0), value=0) # [Nb] | |
| edge_n_units = block_n_units[block_src_dst] # [Eb, 2], number of units at two end of the block edges | |
| edge_n_pairs = edge_n_units[:, 0] * edge_n_units[:, 1] # [Eb], number of unit-pairs in each edge | |
| # block edge id for unit pairs | |
| edge_id = torch.zeros(edge_n_pairs.sum(), dtype=torch.long, device=edge_n_pairs.device) # [Eu], which edge each unit pair belongs to | |
| edge_start_index = torch.cumsum(edge_n_pairs, dim=0)[:-1] # [Eb - 1], start index of each edge (without the first edge as it starts with 0) in unit_src_dst | |
| edge_id[edge_start_index] = 1 | |
| edge_id = torch.cumsum(edge_id, dim=0) # [Eu], which edge each unit pair belongs to, start from 0, end with Eb - 1 | |
| # get unit-pair src-dst indexes | |
| unit_src_dst = torch.ones_like(edge_id) # [Eu] | |
| unit_src_dst[edge_start_index] = -(edge_n_pairs[:-1] - 1) # [Eu], e.g. [1,1,1,-2,1,1,1,1,-4,1], corresponding to edge id [0,0,0,1,1,1,1,1,2,2] | |
| del edge_start_index # release memory | |
| if len(unit_src_dst) > 0: | |
| unit_src_dst[0] = 0 # [Eu], e.g. [0,1,1,-2,1,1,1,1,-4,1], corresponding to edge id [0,0,0,1,1,1,1,1,2,2] | |
| unit_src_dst = torch.cumsum(unit_src_dst, dim=0) # [Eu], e.g. [0,1,2,0,1,2,3,4,0,1], corresponding to edge id [0,0,0,1,1,1,1,1,2,2] | |
| unit_dst_n = edge_n_units[:, 1][edge_id] # [Eu], each block edge has m*n unit pairs, here n refers to the number of units in the dst block | |
| # turn 1D indexes to 2D indexes (TODO: this block is memory-intensive) | |
| unit_src = torch.div(unit_src_dst, unit_dst_n, rounding_mode='floor') + block_offsets[block_src_dst[:, 0][edge_id]] # [Eu] | |
| unit_dst = torch.remainder(unit_src_dst, unit_dst_n) # [Eu], e.g. [0,1,2,0,0,0,0,0,0,1] for block-pair shape 1*3, 5*1, 1*2 | |
| unit_dist_local = unit_dst | |
| # release some memory | |
| del unit_dst_n, unit_src_dst # release memory | |
| unit_edge_src_start = (unit_dst == 0) | |
| unit_dst = unit_dst + block_offsets[block_src_dst[:, 1][edge_id]] # [Eu] | |
| del block_offsets, block_src_dst # release memory | |
| unit_edge_src_id = unit_edge_src_start.long() | |
| if len(unit_edge_src_id) > 0: | |
| unit_edge_src_id[0] = 0 | |
| unit_edge_src_id = torch.cumsum(unit_edge_src_id, dim=0) # [Eu], e.g. [0,0,0,1,2,3,4,5,6,6] for the above example | |
| if k is None: | |
| return (unit_src, unit_dst), (edge_id, unit_edge_src_start, unit_edge_src_id) | |
| # sparsify, each atom is connected to the nearest k atoms in the other block in the same block edge | |
| D = torch.norm(Z[unit_src] - Z[unit_dst], dim=-1) # [Eu, n_channel] | |
| D = D.sum(dim=-1) # [Eu] | |
| max_n = torch.max(scatter_sum(torch.ones_like(unit_edge_src_id), unit_edge_src_id)) | |
| k = min(k, max_n) | |
| BIGINT = 1e10 # assign a large distance to invalid edges | |
| N = unit_edge_src_id.max() + 1 | |
| # src_dst = src_dst.transpose(0, 1) # [2, Ef] | |
| dist = torch.norm(Z[unit_src] - Z[unit_dst], dim=-1).sum(-1) # [Eu] | |
| dist_mat = torch.ones(N, max_n, device=dist.device, dtype=dist.dtype) * BIGINT # [N, max_n] | |
| dist_mat[(unit_edge_src_id, unit_dist_local)] = dist | |
| del dist | |
| dist_neighbors, dst = torch.topk(dist_mat, k, dim=-1, largest=False) # [N, topk] | |
| del dist_mat | |
| src = torch.arange(0, N, device=dst.device).unsqueeze(-1).repeat(1, k) | |
| unit_edge_src_start = torch.zeros_like(src).bool() # [N, k] | |
| unit_edge_src_start[:, 0] = True | |
| src, dst = src.flatten(), dst.flatten() | |
| unit_edge_src_start = unit_edge_src_start.flatten() | |
| dist_neighbors = dist_neighbors.flatten() | |
| is_valid = dist_neighbors < BIGINT | |
| src = src.masked_select(is_valid) | |
| dst = dst.masked_select(is_valid) | |
| unit_edge_src_start = unit_edge_src_start.masked_select(is_valid) | |
| # extract row, col and edge id | |
| mat = torch.ones(N, max_n, device=unit_src.device, dtype=unit_src.dtype) * -1 | |
| mat[(unit_edge_src_id, unit_dist_local)] = unit_src | |
| unit_src = mat[(src, dst)] | |
| mat[(unit_edge_src_id, unit_dist_local)] = unit_dst | |
| unit_dst = mat[(src, dst)] | |
| mat[(unit_edge_src_id, unit_dist_local)] = edge_id | |
| edge_id = mat[(src, dst)] | |
| unit_edge_src_id = src | |
| return (unit_src, unit_dst), (edge_id, unit_edge_src_start, unit_edge_src_id) | |
| def _block_edge_dist(X, block_id, src_dst): | |
| ''' | |
| Several units constitute a block. | |
| This function calculates the distance of edges between blocks | |
| The distance between two blocks are defined as the minimum distance of unit-pairs between them. | |
| The distance between two units are defined as the minimum distance across different channels. | |
| e.g. For number of channels c = 2, suppose their distance is c1 and c2, then the distance between the two units is min(c1, c2) | |
| :param X: [N, c, 3], coordinates, each unit has c channels. Assume the units in the same block are aranged sequentially | |
| :param block_id [N], id of block of each unit. Assume X is sorted so that block_id starts from 0 to Nb - 1 | |
| :param src_dst: [Eb, 2], all edges (block level) that needs distance calculation, represented in (src, dst) | |
| ''' | |
| (unit_src, unit_dst), (edge_id, _, _) = _unit_edges_from_block_edges(block_id, src_dst) | |
| # calculate unit-pair distances | |
| src_x, dst_x = X[unit_src], X[unit_dst] # [Eu, k, 3] | |
| dist = torch.norm(src_x - dst_x, dim=-1) # [Eu, k] | |
| dist = torch.min(dist, dim=-1).values # [Eu] | |
| dist = scatter_min(dist, edge_id)[0] # [Eb] | |
| return dist | |