| """ |
| Copyright (c) Facebook, Inc. and its affiliates. |
| Copyright (c) Microsoft Corporation. |
| Licensed under the MIT License. |
| Code derived from the OCP codebase: |
| https://github.com/Open-Catalyst-Project/ocp |
| """ |
|
|
| import sys |
|
|
| import numpy as np |
| import torch |
| from torch_scatter import segment_coo, segment_csr |
|
|
| from ...common.utils.globals import get_pyg_device |
|
|
|
|
| def get_pbc_distances( |
| pos: torch.Tensor, |
| edge_index: torch.Tensor, |
| cell: torch.Tensor, |
| cell_offsets: torch.Tensor, |
| neighbors: torch.Tensor, |
| return_offsets: bool = False, |
| return_distance_vec: bool = False, |
| ) -> dict: |
| row, col = edge_index |
|
|
| distance_vectors = pos[row] - pos[col] |
|
|
| |
| neighbors = neighbors.to(cell.device) |
| cell = torch.repeat_interleave(cell, neighbors, dim=0) |
| offsets = cell_offsets.float().view(-1, 1, 3).bmm(cell.float()).view(-1, 3) |
| distance_vectors += offsets |
|
|
| |
| distances = distance_vectors.norm(dim=-1) |
|
|
| |
| nonzero_idx = torch.arange(len(distances))[distances > 0] |
| edge_index = edge_index[:, nonzero_idx] |
| distances = distances[nonzero_idx] |
|
|
| out = { |
| "edge_index": edge_index, |
| "distances": distances, |
| } |
|
|
| if return_distance_vec: |
| out["distance_vec"] = distance_vectors[nonzero_idx] |
|
|
| if return_offsets: |
| out["offsets"] = offsets[nonzero_idx] |
|
|
| return out |
|
|
|
|
| def radius_graph_pbc( |
| pos: torch.Tensor, |
| pbc: torch.Tensor | None, |
| natoms: torch.Tensor, |
| cell: torch.Tensor, |
| radius: float, |
| max_num_neighbors_threshold: int, |
| max_cell_images_per_dim: int = sys.maxsize, |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: |
| """Function computing the graph in periodic boundary conditions on a (batched) set of |
| positions and cells. |
| |
| This function is copied from |
| https://github.com/Open-Catalyst-Project/ocp/blob/main/ocpmodels/common/utils.py, |
| commit 480eb9279ec4a5885981f1ee588c99dcb38838b5 |
| |
| Args: |
| pos (LongTensor): Atomic positions in cartesian coordinates |
| :obj:`[n, 3]` |
| pbc (BoolTensor): indicates periodic boundary conditions per structure. |
| :obj:`[n_structures, 3]` |
| natoms (IntTensor): number of atoms per structure. Has shape |
| :obj:`[n_structures]` |
| cell (Tensor): atomic cell. Has shape |
| :obj:`[n_structures, 3, 3]` |
| radius (float): cutoff radius distance |
| max_num_neighbors_threshold (int): Maximum number of neighbours to consider. |
| |
| Returns: |
| edge_index (IntTensor): index of atoms in edges. Has shape |
| :obj:`[n_edges, 2]` |
| cell_offsets (IntTensor): cell displacement w.r.t. their original position of atoms in edges. Has shape |
| :obj:`[n_edges, 3, 3]` |
| num_neighbors_image (IntTensor): Number of neighbours per cell image. |
| :obj:`[n_structures]` |
| offsets (LongTensor): cartesian displacement w.r.t. their original position of atoms in edges. Has shape |
| :obj:`[n_edges, 3, 3]` |
| atom_distance (LongTensor): edge length. Has shape |
| :obj:`[n_edges]` |
| """ |
| device = pos.device |
| batch_size = len(natoms) |
| pbc_ = [False, False, False] |
|
|
| if pbc is not None: |
| pbc = torch.atleast_2d(pbc) |
| for i in range(3): |
| if not torch.any(pbc[:, i]).item(): |
| pbc_[i] = False |
| elif torch.all(pbc[:, i]).item(): |
| pbc_[i] = True |
| else: |
| raise RuntimeError( |
| "Different structures in the batch have different PBC configurations. This is not currently supported." |
| ) |
|
|
| natoms_squared = (natoms**2).long() |
|
|
| |
| index_offset = torch.cumsum(natoms, dim=0) - natoms |
|
|
| index_offset_expand = torch.repeat_interleave(index_offset, natoms_squared) |
| natoms_expand = torch.repeat_interleave(natoms, natoms_squared) |
|
|
| |
| |
| |
| |
| |
| num_atom_pairs = torch.sum(natoms_squared) |
| index_squared_offset = torch.cumsum(natoms_squared, dim=0) - natoms_squared |
| index_squared_offset = torch.repeat_interleave(index_squared_offset, natoms_squared) |
| atom_count_squared = torch.arange(num_atom_pairs, device=device) - index_squared_offset |
|
|
| |
| |
| index1 = ( |
| torch.div(atom_count_squared, natoms_expand, rounding_mode="floor") |
| ) + index_offset_expand |
| index2 = (atom_count_squared % natoms_expand) + index_offset_expand |
| |
| pos1 = torch.index_select(pos, 0, index1) |
| pos2 = torch.index_select(pos, 0, index2) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| cross_a2a3 = torch.cross(cell[:, 1], cell[:, 2], dim=-1) |
| cell_vol = torch.sum(cell[:, 0] * cross_a2a3, dim=-1, keepdim=True) |
|
|
| if pbc_[0]: |
| inv_min_dist_a1 = torch.norm(cross_a2a3 / cell_vol, p=2, dim=-1) |
| rep_a1 = torch.ceil(radius * inv_min_dist_a1) |
| else: |
| rep_a1 = cell.new_zeros(1) |
|
|
| if pbc_[1]: |
| cross_a3a1 = torch.cross(cell[:, 2], cell[:, 0], dim=-1) |
| inv_min_dist_a2 = torch.norm(cross_a3a1 / cell_vol, p=2, dim=-1) |
| rep_a2 = torch.ceil(radius * inv_min_dist_a2) |
| else: |
| rep_a2 = cell.new_zeros(1) |
|
|
| if pbc_[2]: |
| cross_a1a2 = torch.cross(cell[:, 0], cell[:, 1], dim=-1) |
| inv_min_dist_a3 = torch.norm(cross_a1a2 / cell_vol, p=2, dim=-1) |
| rep_a3 = torch.ceil(radius * inv_min_dist_a3) |
| else: |
| rep_a3 = cell.new_zeros(1) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| max_rep = [ |
| min(int(rep_a1.max()), max_cell_images_per_dim), |
| min(int(rep_a2.max()), max_cell_images_per_dim), |
| min(int(rep_a3.max()), max_cell_images_per_dim), |
| ] |
|
|
| |
| cells_per_dim = [ |
| torch.arange(-rep, rep + 1, device=device, dtype=torch.float) for rep in max_rep |
| ] |
| cell_offsets = torch.cartesian_prod(*cells_per_dim) |
| num_cells = len(cell_offsets) |
| cell_offsets_per_atom = cell_offsets.view(1, num_cells, 3).repeat(len(index2), 1, 1) |
| cell_offsets = torch.transpose(cell_offsets, 0, 1) |
| cell_offsets_batch = cell_offsets.view(1, 3, num_cells).expand(batch_size, -1, -1) |
|
|
| |
| data_cell = torch.transpose(cell, 1, 2) |
| pbc_offsets = torch.bmm(data_cell, cell_offsets_batch) |
| pbc_offsets_per_atom = torch.repeat_interleave(pbc_offsets, natoms_squared, dim=0) |
|
|
| |
| pos1 = pos1.view(-1, 3, 1).expand(-1, -1, num_cells) |
| pos2 = pos2.view(-1, 3, 1).expand(-1, -1, num_cells) |
| index1 = index1.view(-1, 1).repeat(1, num_cells).view(-1) |
| index2 = index2.view(-1, 1).repeat(1, num_cells).view(-1) |
| |
| pos2 = pos2 + pbc_offsets_per_atom |
|
|
| |
| atom_distance_squared = torch.sum((pos1 - pos2) ** 2, dim=1) |
| atom_distance_squared = atom_distance_squared.view(-1) |
|
|
| |
| mask_within_radius = torch.le(atom_distance_squared, radius * radius) |
| |
| mask_not_same = torch.gt(atom_distance_squared, 0.0001) |
| mask = torch.logical_and(mask_within_radius, mask_not_same) |
| index1 = torch.masked_select(index1, mask) |
| index2 = torch.masked_select(index2, mask) |
| cell_offsets = torch.masked_select( |
| cell_offsets_per_atom.view(-1, 3), mask.view(-1, 1).expand(-1, 3) |
| ) |
| cell_offsets = cell_offsets.view(-1, 3) |
| atom_distance_squared = torch.masked_select(atom_distance_squared, mask) |
|
|
| mask_num_neighbors, num_neighbors_image = get_max_neighbors_mask( |
| natoms=natoms, |
| index=index1, |
| atom_distance_squared=atom_distance_squared, |
| max_num_neighbors_threshold=max_num_neighbors_threshold, |
| ) |
|
|
| if not torch.all(mask_num_neighbors): |
| |
| index1 = torch.masked_select(index1, mask_num_neighbors) |
| index2 = torch.masked_select(index2, mask_num_neighbors) |
| atom_distance_squared = torch.masked_select(atom_distance_squared, mask_num_neighbors) |
| cell_offsets = torch.masked_select( |
| cell_offsets.view(-1, 3), mask_num_neighbors.view(-1, 1).expand(-1, 3) |
| ) |
| cell_offsets = cell_offsets.view(-1, 3) |
|
|
| edge_index = torch.stack((index2, index1)) |
| |
|
|
| cell_repeated = torch.repeat_interleave(cell, num_neighbors_image, dim=0) |
| offsets = -cell_offsets.float().view(-1, 1, 3).bmm(cell_repeated.float()).view(-1, 3) |
| return ( |
| edge_index, |
| cell_offsets, |
| num_neighbors_image, |
| offsets, |
| torch.sqrt(atom_distance_squared), |
| ) |
|
|
|
|
| def get_max_neighbors_mask( |
| natoms: torch.Tensor, |
| index: torch.Tensor, |
| atom_distance_squared: torch.Tensor, |
| max_num_neighbors_threshold: int, |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Give a mask that filters out edges so that each atom has at most |
| `max_num_neighbors_threshold` neighbors. |
| Assumes that `index` is sorted. |
| """ |
| device = natoms.device |
| num_atoms = natoms.sum() |
|
|
| |
| |
| ones = index.new_ones(1).expand_as(index) |
| |
| pyg_device = get_pyg_device() |
| device_before = ones.device |
| num_neighbors = segment_coo(ones.to(pyg_device), index.to(pyg_device), dim_size=num_atoms).to( |
| device_before |
| ) |
| max_num_neighbors = num_neighbors.max() |
| num_neighbors_thresholded = num_neighbors.clamp(max=max_num_neighbors_threshold) |
|
|
| |
| image_indptr = torch.zeros(natoms.shape[0] + 1, device=device, dtype=torch.long) |
| image_indptr[1:] = torch.cumsum(natoms, dim=0) |
| num_neighbors_image = segment_csr( |
| num_neighbors_thresholded.to(pyg_device), image_indptr.to(pyg_device) |
| ).to(device_before) |
|
|
| |
| if max_num_neighbors <= max_num_neighbors_threshold or max_num_neighbors_threshold <= 0: |
| mask_num_neighbors = torch.tensor([True], dtype=bool, device=device).expand_as(index) |
| return mask_num_neighbors, num_neighbors_image |
|
|
| |
| |
| distance_sort = torch.full([num_atoms * max_num_neighbors], np.inf, device=device) |
|
|
| |
| |
| index_neighbor_offset = torch.cumsum(num_neighbors, dim=0) - num_neighbors |
| index_neighbor_offset_expand = torch.repeat_interleave(index_neighbor_offset, num_neighbors) |
| index_sort_map = ( |
| index * max_num_neighbors |
| + torch.arange(len(index), device=device) |
| - index_neighbor_offset_expand |
| ) |
| distance_sort.index_copy_(0, index_sort_map, atom_distance_squared) |
| distance_sort = distance_sort.view(num_atoms, max_num_neighbors) |
|
|
| |
| distance_sort, index_sort = torch.sort(distance_sort, dim=1) |
| |
| distance_sort = distance_sort[:, :max_num_neighbors_threshold] |
| index_sort = index_sort[:, :max_num_neighbors_threshold] |
|
|
| |
| index_sort = index_sort + index_neighbor_offset.view(-1, 1).expand( |
| -1, max_num_neighbors_threshold |
| ) |
| |
| mask_finite = torch.isfinite(distance_sort) |
| index_sort = torch.masked_select(index_sort, mask_finite) |
|
|
| |
| |
| |
| mask_num_neighbors = torch.zeros(len(index), device=device, dtype=bool) |
| mask_num_neighbors.index_fill_(0, index_sort, True) |
|
|
| return mask_num_neighbors, num_neighbors_image |
|
|