Coverage for gamdpy/interactions/nblist_linked_lists.py: 98%

61 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 18:46 +0200

1 

2import numpy as np 

3import numba 

4import math 

5from numba import cuda 

6 

7class NbListLinkedLists(): 

8 

9 def __init__(self, configuration, exclusions, max_num_nbs): 

10 self.configuration = configuration 

11 self.N, self.D = configuration.N, configuration.D 

12 self.nblist = np.zeros((configuration.N, max_num_nbs+1), dtype=np.int32) 

13 self.nbflag = np.zeros(3, dtype=np.int32) 

14 self.r_ref = np.zeros_like(configuration['r']) # Inherents also data type 

15 self.exclusions = exclusions # Should be able to be a list (eg from bonds, angles, etc), and merge 

16 self.d_simbox_last_rebuild = cuda.to_device(np.zeros(configuration.simbox.len_sim_box_data, dtype=np.float32)) 

17 

18 def copy_to_device(self): 

19 self.d_nblist = cuda.to_device(self.nblist) 

20 self.d_nbflag = cuda.to_device(self.nbflag) 

21 self.d_r_ref = cuda.to_device(self.r_ref) 

22 self.d_exclusions = cuda.to_device(self._exclusions) 

23 self.d_cells_per_dimension = cuda.to_device(self.cells_per_dimension) # Mapping cells to 1D, so need 'shape' 

24 self.d_cells = cuda.to_device(self.cells) 

25 self.d_my_cell = cuda.to_device(self.my_cell) 

26 self.d_next_particle_in_cell = cuda.to_device(self.next_particle_in_cell) 

27 

28 def get_params(self, max_cut, compute_plan, verbose=True): 

29 self.max_cut = max_cut 

30 self.skin, = [compute_plan[key] for key in ['skin']] 

31 

32 if type(self.exclusions) == np.ndarray: # Don't change user-set properties 

33 self._exclusions = self.exclusions.copy() 

34 else: 

35 self._exclusions = np.zeros((self.r_ref.shape[0], 2), dtype=np.int32) 

36 

37 self.cells_per_dimension = np.zeros((self.D), dtype=np.int32) 

38 min_cells_size = (self.max_cut + self.skin)/2 

39 simbox_lengths = self.configuration.simbox.get_lengths() 

40 for i in range(self.D): 

41 self.cells_per_dimension[i] = int(simbox_lengths[i]/min_cells_size) 

42 assert self.cells_per_dimension[i] > 4 

43 if i == 0: 

44 assert self.cells_per_dimension[i] > 6 

45 # TODO: Take care of 

46 # - changing simbox size during simulation (NPT, compression) 

47 

48 

49 #print(self.cells_per_dimension) 

50 self.my_cell = np.zeros((self.N, self.D), np.int32) # my_cell[:, -1] 1d index 

51 self.cells = -np.ones(self.cells_per_dimension, dtype=np.int32) 

52 

53 self.next_particle_in_cell = -np.ones(self.N, dtype=np.int32) # -1 = no further particles in list 

54 self.copy_to_device() 

55 return (np.float32(self.max_cut), np.float32(self.skin), self.d_nbflag, self.d_r_ref, self.d_exclusions, 

56 self.d_cells_per_dimension, self.d_cells, self.d_my_cell, self.d_next_particle_in_cell, self.d_simbox_last_rebuild) 

57 

58 def get_kernel(self, configuration, compute_plan, compute_flags, verbose=False, force_update=False): 

59 

60 # Unpack parameters from configuration and compute_plan 

61 D, num_part = configuration.D, configuration.N 

62 pb, tp, gridsync, UtilizeNIII = [compute_plan[key] for key in ['pb', 'tp', 'gridsync', 'UtilizeNIII']] 

63 num_blocks = (num_part - 1) // pb + 1 

64 loop_x_addition = configuration.simbox.get_loop_x_addition() 

65 

66 # Unpack indices for vectors and scalars to be compiled into kernel 

67 r_id, f_id = [configuration.vectors.indices[key] for key in ['r', 'f']] 

68 

69 # JIT compile functions to be compiled into kernel 

70 dist_sq_function = numba.njit(configuration.simbox.get_dist_sq_function()) 

71 dist_moved_exceeds_limit_function = numba.njit(configuration.simbox.get_dist_moved_exceeds_limit_function()) 

72 loop_x_shift_function = numba.njit(configuration.simbox.get_loop_x_shift_function()) 

73 

74 @cuda.jit( device=gridsync ) 

75 def nblist_check(vectors, sim_box, skin, r_ref, nbflag, simbox_last_rebuild, cut): # pragma: no cover 

76 """ Check validity of nblist, i.e. did any particle mode more than skin/2 since last nblist update? 

77 Each tread-block checks the assigned particles (global_id) 

78 nbflag[0] = 0 : No update needed 

79 nbflag[0] = num_blocks : Update needed 

80 Kernel configuration: [num_blocks, (pb, tp)] 

81 """ 

82 

83 global_id, my_t = cuda.grid(2) 

84 if force_update: # nblist update forced (for benchmark or similar) 

85 if global_id==0 and my_t==0: 

86 nbflag[0]=num_blocks 

87 

88 if global_id < num_part and my_t==0: 

89 if dist_moved_exceeds_limit_function(vectors[r_id][global_id], r_ref[global_id], sim_box, simbox_last_rebuild, skin, cut): 

90 nbflag[0] = num_blocks 

91 

92 return 

93 

94 @cuda.jit(device=gridsync) 

95 def put_particles_in_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell): # pragma: no cover 

96 """ Each particle computers its cells coordinates, and inserts itself in cell-list 

97 Kernel configuration: [num_blocks, (pb, tp)] 

98 """ 

99 

100 global_id, my_t = cuda.grid(2) 

101 if global_id < num_part and my_t == 0: 

102 for k in range(D): 

103 #my_cell[global_id,k] = int(math.floor(vectors[r_id][global_id,k]*cells_per_dimension[k]/sim_box[k]))%cells_per_dimension[k] 

104 my_cell[global_id,k] = int(math.floor((vectors[r_id][global_id,k]+0.5*sim_box[k])*cells_per_dimension[k]/sim_box[k]))%cells_per_dimension[k] 

105 #if my_cell[global_id,k]<0: 

106 # print(global_id,k, my_cell[global_id,k],vectors[r_id][global_id,k]) 

107 index = (my_cell[global_id,0], my_cell[global_id,1], my_cell[global_id,2]) # 3D  

108 next_particle_in_cell[global_id] = cuda.atomic.exch(cells, index, global_id) # index needs to be tuple when multidim 

109 return 

110 

111 @cuda.jit(device=gridsync) 

112 def nblist_update_from_linked_lists(vectors, sim_box, cut_plus_skin, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell, nblist, r_ref, exclusions): # pragma: no cover 

113 """ Order N neighbor-list update from linked lists  

114 Kernel configuration: [num_blocks, (pb, tp)] USING ONLY MY_T == 0 for now... 

115 """ 

116 

117 global_id, my_t = cuda.grid(2) 

118 

119 cell_length_x = sim_box[0] / cells_per_dimension[0] 

120 loop_x_shift = loop_x_shift_function(sim_box, cell_length_x) 

121 

122 max_nbs = nblist.shape[1]-1 # Last index is used for storing number of neighbors 

123 

124 if global_id < num_part and my_t==0: 

125 my_num_nbs = 0 

126 my_num_exclusions = exclusions[global_id, -1] 

127 

128 for ix in range(-2-loop_x_addition,3+loop_x_addition,1): 

129 for iy in range(-2,3,1): 

130 # Correct handling of LEBC requires modifyng the loop over neighbor cells to take the box shift into account. 

131 other_cell_y_unwrapped = my_cell[global_id, 1]+iy 

132 y_wrap_cell = 1 if other_cell_y_unwrapped >= cells_per_dimension[1] else -1 if other_cell_y_unwrapped < 0 else 0 

133 shifted_ix = ix + y_wrap_cell * loop_x_shift 

134 

135 for iz in range(-2,3,1): 

136 other_index = ( 

137 (my_cell[global_id, 0]+shifted_ix)%cells_per_dimension[0], 

138 (my_cell[global_id, 1]+iy)%cells_per_dimension[1], 

139 (my_cell[global_id, 2]+iz)%cells_per_dimension[2]) 

140 other_global_id = cells[other_index] 

141 while other_global_id >= 0: # To use tp>1: read tp particles ahead, and pick yours 

142 if UtilizeNIII: # Could be done per cell basis... 

143 #flag = other_global_id < global_id 

144 TwodN = 2*(other_global_id - global_id) 

145 flag = other_global_id < num_part and (0 < TwodN <= num_part or TwodN < -num_part) 

146 else: 

147 flag = other_global_id != global_id 

148 if flag: 

149 dist_sq = dist_sq_function(vectors[r_id][other_global_id], vectors[r_id][global_id], sim_box) 

150 if dist_sq < cut_plus_skin*cut_plus_skin: 

151 not_excluded = True # Check exclusion list. Do later ??? 

152 for k in range(my_num_exclusions): 

153 if exclusions[global_id, k] == other_global_id: 

154 not_excluded = False 

155 if not_excluded: 

156 my_num_nbs += 1 

157 if my_num_nbs < max_nbs: 

158 nblist[global_id, my_num_nbs-1] = other_global_id # Last entry is number of neighbors 

159 other_global_id = next_particle_in_cell[other_global_id] 

160 nblist[global_id, -1] = my_num_nbs 

161 

162 # Various house-keeping 

163 for k in range(D): 

164 r_ref[global_id, k] = vectors[r_id][global_id, k] # Store positions for wich nblist was updated ( used in nblist_check() )  

165 #if local_id == 0 and my_t==0: 

166 # cuda.atomic.add(nbflag, 0, -1) # nbflag[0] = 0 by when all blocks are done. Moved to clear_cells 

167 if global_id == 0 and my_t==0: 

168 cuda.atomic.add(nbflag, 2, 1) # Count how many updates are done in nbflag[2] 

169 if my_num_nbs >= max_nbs: # Overflow detected, nbflag[1] should be checked later, and then 

170 cuda.atomic.max(nbflag, 1, my_num_nbs) # re-allocate larger nb-list, and redo computations from last safe state 

171 

172 return 

173 

174 

175 @cuda.jit(device=gridsync) 

176 def clear_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell): # pragma: no cover 

177 """ Particles clears cell-list (only one (e.g. tail) actually needs to do this) 

178 Kernel configuration: [num_blocks, (pb, tp)] 

179 """ 

180 

181 global_id, my_t = cuda.grid(2) 

182 

183 if global_id < num_part and my_t == 0: # Change to simple Nullify kernel? 

184 cells[my_cell[global_id,0], my_cell[global_id,1], my_cell[global_id,2]] = -1 # Every body writes, but thats OK 

185 next_particle_in_cell[global_id] = -1 

186 

187 

188 if cuda.threadIdx.x == 0 and my_t==0: # One thread per threadblock decreases from numblocks 

189 cuda.atomic.add(nbflag, 0, -1) # i.e., nbflag[0] = 0 by when all blocks are done 

190 

191 return 

192 

193 if gridsync: 

194 # A device function, calling a number of device functions, using gridsync to syncronize 

195 @cuda.jit( device=gridsync ) 

196 def check_and_update(grid, vectors, scalars, ptype, sim_box, nblist, nblist_parameters): # pragma: no cover 

197 max_cut, skin, nbflag, r_ref, exclusions, cells_per_dimension, cells, my_cell, next_particle_in_cell, simbox_last_rebuild = nblist_parameters 

198 nblist_check(vectors, sim_box, skin, r_ref, nbflag, simbox_last_rebuild, max_cut) 

199 grid.sync() 

200 if nbflag[0] > 0: 

201 put_particles_in_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell) 

202 grid.sync() 

203 nblist_update_from_linked_lists(vectors, sim_box, max_cut+skin, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell, nblist, r_ref, exclusions) 

204 grid.sync() 

205 clear_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell) 

206 return 

207 return check_and_update 

208 

209 else: 

210 # A python function, making several kernel calls to syncronize  

211 def check_and_update(grid, vectors, scalars, ptype, sim_box, nblist, nblist_parameters): 

212 max_cut, skin, nbflag, r_ref, exclusions, cells_per_dimension, cells, my_cell, next_particle_in_cell, simbox_last_rebuild = nblist_parameters 

213 nblist_check[num_blocks, (pb, 1)](vectors, sim_box, skin, r_ref, nbflag, simbox_last_rebuild, max_cut) 

214 if nbflag[0] > 0: 

215 put_particles_in_cells[num_blocks, (pb, 1)](vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell) 

216 nblist_update_from_linked_lists[num_blocks, (pb, 1)](vectors, sim_box, max_cut+skin, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell, nblist, r_ref, exclusions) 

217 clear_cells[num_blocks, (pb, 1)](vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell) 

218 return 

219 return check_and_update