Source code for mdapy.neighbor

# Copyright (c) 2022, mushroomfire in Beijing Institute of Technology
# This file is from the mdapy project, released under the BSD 3-Clause License.

import taichi as ti
import numpy as np

try:
    from neigh._neigh import (
        _build_cell_rec,
        _build_cell_rec_with_jishu,
        _build_cell_tri,
        _build_cell_tri_with_jishu,
    )
except Exception:
    from _neigh import (
        _build_cell_rec,
        _build_cell_rec_with_jishu,
        _build_cell_tri,
        _build_cell_tri_with_jishu,
    )

import warnings

warnings.simplefilter("ignore", category=DeprecationWarning)


[docs] @ti.data_oriented class Neighbor: """This module is used to cerate neighbor of atoms within a given cutoff distance. Using linked-cell list method makes fast neighbor finding possible. Args: pos (np.ndarray): (:math:`N_p, 3`) particles positions. box (np.ndarray): (:math:`4, 3`) system box, must be rectangle. rc (float): cutoff distance. boundary (list, optional): boundary conditions, 1 is periodic and 0 is free boundary. Defaults to [1, 1, 1]. max_neigh (int, optional): maximum neighbor number. If not given, will estimate atomatically. Default to None. Outputs: - **verlet_list** (np.ndarray) - (:math:`N_p, max\_neigh`) verlet_list[i, j] means j atom is a neighbor of i atom if j > -1. - **distance_list** (np.ndarray) - (:math:`N_p, max\_neigh`) distance_list[i, j] means distance between i and j atom. - **neighbor_number** (np.ndarray) - (:math:`N_p`) neighbor atoms number. Examples: >>> import mdapy as mp >>> mp.init() >>> import numpy as np >>> FCC = mp.LatticeMaker(3.615, 'FCC', 10, 10, 10) # Create a FCC structure >>> FCC.compute() # Get atom positions >>> neigh = mp.Neighbor(FCC.pos, FCC.box, 5.) # Initialize Neighbor class. >>> neigh.compute() # Calculate particle neighbor information. >>> neigh.verlet_list # Check the neighbor index. >>> neigh.distance_list # Check the neighbor distance. >>> neigh.neighbor_number # Check the neighbor atom number. """ def __init__(self, pos, box, rc, boundary=[1, 1, 1], max_neigh=None): if pos.dtype != np.float64: pos = pos.astype(np.float64) self.pos = pos if box.dtype != np.float64: box = box.astype(np.float64) if box.shape == (3, 2): self.box = np.zeros((4, 3), dtype=box.dtype) self.box[0, 0], self.box[1, 1], self.box[2, 2] = box[:, 1] - box[:, 0] self.box[-1] = box[:, 0] elif box.shape == (4, 3): self.box = box assert self.box[0, 1] == 0 assert self.box[0, 2] == 0 assert self.box[1, 2] == 0 self.rc = rc self.boundary = ti.Vector([int(boundary[i]) for i in range(3)]) self.bin_length = self.rc + 0.01 self.max_neigh = max_neigh self.box_length = ti.Vector([np.linalg.norm(box[i]) for i in range(3)]) self.N = self.pos.shape[0] for i, direc in zip(range(3), ["x", "y", "z"]): if self.boundary[i] == 1: assert ( self.box_length[i] / 2 > self.bin_length ), f"box length along {direc} direction with periodic boundary should be two times larger than rc." self.ncel = ti.Vector( [ max(int(np.floor(np.linalg.norm(box[i]) / self.bin_length)), 3) for i in range(3) ] ) self.if_computed = False self.rec = True if self.box[1, 0] != 0 or self.box[2, 0] != 0 or self.box[2, 1] != 0: self.rec = False @ti.func def _pbc_rec(self, rij): for m in ti.static(range(3)): if self.boundary[m] == 1: dx = rij[m] x_size = self.box_length[m] h_x_size = x_size * 0.5 if dx > h_x_size: dx = dx - x_size if dx <= -h_x_size: dx = dx + x_size rij[m] = dx return rij @ti.func def _pbc(self, rij, box: ti.types.ndarray(dtype=ti.math.vec3)) -> ti.math.vec3: nz = rij[2] / box[2][2] ny = (rij[1] - nz * box[2][1]) / box[1][1] nx = (rij[0] - ny * box[1][0] - nz * box[2][0]) / box[0][0] n = ti.Vector([nx, ny, nz]) for i in ti.static(range(3)): if self.boundary[i] == 1: if n[i] > 0.5: n[i] -= 1 elif n[i] < -0.5: n[i] += 1 return n[0] * box[0] + n[1] * box[1] + n[2] * box[2] @ti.kernel def _build_verlet_list( self, pos: ti.types.ndarray(dtype=ti.math.vec3), atom_cell_list: ti.types.ndarray(), cell_id_list: ti.types.ndarray(), verlet_list: ti.types.ndarray(), distance_list: ti.types.ndarray(), neighbor_number: ti.types.ndarray(), box: ti.types.ndarray(dtype=ti.math.vec3), ): rcsq = self.rc * self.rc icel, jcel, kcel, rij = ( 0, 0, 0, ti.Vector([ti.f64(0.0), ti.f64(0.0), ti.f64(0.0)]), ) for i in range(pos.shape[0]): nindex = 0 if ti.static(self.rec): icel, jcel, kcel = ti.floor( (pos[i] - box[3]) / self.bin_length, dtype=ti.i32 ) else: r = pos[i] - box[3] nz = r[2] / box[2][2] ny = (r[1] - nz * box[2][1]) / box[1][1] nx = (r[0] - ny * box[1][0] - nz * box[2][0]) / box[0][0] icel = ti.floor((nx * box[0]).norm() / self.bin_length, int) jcel = ti.floor((ny * box[1]).norm() / self.bin_length, int) kcel = ti.floor((nz * box[2]).norm() / self.bin_length, int) iicel, jjcel, kkcel = icel, jcel, kcel # make sure correct cell if icel < 0: iicel = 0 elif icel > self.ncel[0] - 1: iicel = self.ncel[0] - 1 if jcel < 0: jjcel = 0 elif jcel > self.ncel[1] - 1: jjcel = self.ncel[1] - 1 if kcel < 0: kkcel = 0 elif kcel > self.ncel[2] - 1: kkcel = self.ncel[2] - 1 for iiicel in range(iicel - 1, iicel + 2): for jjjcel in range(jjcel - 1, jjcel + 2): for kkkcel in range(kkcel - 1, kkcel + 2): iiiicel = iiicel jjjjcel = jjjcel kkkkcel = kkkcel if iiicel < 0: iiiicel += self.ncel[0] elif iiicel > self.ncel[0] - 1: iiiicel -= self.ncel[0] if jjjcel < 0: jjjjcel += self.ncel[1] elif jjjcel > self.ncel[1] - 1: jjjjcel -= self.ncel[1] if kkkcel < 0: kkkkcel += self.ncel[2] elif kkkcel > self.ncel[2] - 1: kkkkcel -= self.ncel[2] j = cell_id_list[iiiicel, jjjjcel, kkkkcel] while j > -1: if ti.static(self.rec): rij = self._pbc_rec(pos[j] - pos[i]) else: rij = self._pbc(pos[j] - pos[i], box) rijdis_sq = rij[0] ** 2 + rij[1] ** 2 + rij[2] ** 2 if rijdis_sq <= rcsq and j != i: verlet_list[i, nindex] = j distance_list[i, nindex] = ti.sqrt(rijdis_sq) nindex += 1 j = atom_cell_list[j] neighbor_number[i] = nindex
[docs] def compute(self): """Do the real neighbor calculation.""" atom_cell_list = np.zeros(self.N, dtype=np.int32) cell_id_list = ( np.zeros((self.ncel[0], self.ncel[1], self.ncel[2]), dtype=np.int32) ) - 1 need_check = True if self.max_neigh is None: max_neigh_list = np.zeros_like(cell_id_list) if self.rec: _build_cell_rec_with_jishu( self.pos, atom_cell_list, cell_id_list, np.ascontiguousarray(self.box[-1]), np.array([i for i in self.ncel]), self.bin_length, max_neigh_list, ) else: _build_cell_tri_with_jishu( self.pos, atom_cell_list, cell_id_list, self.box, np.array([i for i in self.ncel]), self.bin_length, max_neigh_list, ) self.max_neigh = np.partition(max_neigh_list.flatten(), -4)[-4:].sum() need_check = False else: if self.rec: _build_cell_rec( self.pos, atom_cell_list, cell_id_list, np.ascontiguousarray(self.box[-1]), np.array([i for i in self.ncel]), self.bin_length, ) else: _build_cell_tri( self.pos, atom_cell_list, cell_id_list, self.box, np.array([i for i in self.ncel]), self.bin_length, ) self.verlet_list = np.zeros((self.N, self.max_neigh), dtype=int) - 1 self.distance_list = ( np.zeros((self.N, self.max_neigh), dtype=self.pos.dtype) + self.rc + 1.0 ) self.neighbor_number = np.zeros(self.N, dtype=int) self._build_verlet_list( self.pos, atom_cell_list, cell_id_list, self.verlet_list, self.distance_list, self.neighbor_number, self.box, ) if need_check: max_neigh_number = self.neighbor_number.max() assert ( max_neigh_number <= self.max_neigh ), f"Increase the max_neigh larger than {max_neigh_number}." self._if_computed = True
@ti.kernel def _partition_select_sort( self, indices: ti.types.ndarray(), keys: ti.types.ndarray(), N: int ): """This function sorts N-th minimal value in keys. Args: indices (ti.types.ndarray): indices. keys (ti.types.ndarray): values to be sorted. N (int): number of sorted values. """ for i in range(indices.shape[0]): for j in range(N): minIndex = j for k in range(j + 1, indices.shape[1]): if keys[i, k] < keys[i, minIndex]: minIndex = k if minIndex != j: keys[i, minIndex], keys[i, j] = keys[i, j], keys[i, minIndex] indices[i, minIndex], indices[i, j] = ( indices[i, j], indices[i, minIndex], )
[docs] def sort_verlet_by_distance(self, N: int): """This function sorts the first N-th verlet_list by distance_list. Args: N (int): number of sorted values """ if not self._if_computed: self.compute() self._partition_select_sort(self.verlet_list, self.distance_list, N)
if __name__ == "__main__": from lattice_maker import LatticeMaker from time import time # ti.init(ti.gpu, device_memory_GB=4.0) ti.init(ti.cpu) start = time() lattice_constant = 3.615 x, y, z = 100, 100, 50 FCC = LatticeMaker(lattice_constant, "FCC", x, y, z) FCC.compute() end = time() print(f"Build {FCC.pos.shape[0]} atoms FCC time: {end-start} s.") start = time() neigh = Neighbor(FCC.pos, FCC.box, 5.0, max_neigh=None) neigh.compute() end = time() print(neigh.ncel) print(f"Build neighbor time: {end-start} s.") print(neigh.verlet_list[0]) print(neigh.distance_list[0]) # print(neigh.verlet_list.shape[1]) # print(neigh.neighbor_number.max()) # print(neigh.neighbor_number.min()) # neigh.sort_verlet_by_distance(12) # print(neigh.verlet_list[0]) # print(neigh.distance_list[0])