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

191 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +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 #self.cells = -np.ones(np.prod(self.cells_per_dimension), dtype=np.int32) # Map to 1D array 

53 #print(self.cells.shape) 

54 

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

56 self.copy_to_device() 

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

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

59 

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

61 

62 # Unpack parameters from configuration and compute_plan 

63 D, num_part = configuration.D, configuration.N 

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

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

66 compute_stresses = compute_flags['stresses'] 

67 

68 loop_x_addition = configuration.simbox.get_loop_x_addition() 

69 

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

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

72 if compute_stresses: 

73 sx_id = configuration.vectors.indices['sx'] 

74 if D > 1: 

75 sy_id = configuration.vectors.indices['sy'] 

76 if D > 2: 

77 sz_id = configuration.vectors.indices['sz'] 

78 if D > 3: 

79 sw_id = configuration.vectors.indices['sw'] 

80 

81 

82 # JIT compile functions to be compiled into kernel 

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

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

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

86 

87 @cuda.jit( device=gridsync ) 

88 def nblist_check(vectors, sim_box, skin, r_ref, nbflag, simbox_last_rebuild, cut): 

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

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

91 nbflag[0] = 0 : No update needed 

92 nbflag[0] = num_blocks : Update needed 

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

94 """ 

95 

96 global_id, my_t = cuda.grid(2) 

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

98 if global_id==0 and my_t==0: 

99 nbflag[0]=num_blocks 

100 

101 if global_id < num_part and my_t==0: 

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

103 nbflag[0] = num_blocks 

104 

105 #if global_id < num_part and my_t==0: 

106 # dist_sq = dist_sq_function(vectors[r_id][global_id], r_ref[global_id], sim_box) 

107 # if dist_sq > skin*skin*numba.float32(0.25): 

108 # nbflag[0]=num_blocks 

109 

110 

111 return 

112 

113 @cuda.jit(device=gridsync) 

114 def put_particles_in_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell): 

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

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

117 """ 

118 

119 global_id, my_t = cuda.grid(2) 

120 if global_id < num_part and my_t == 0: 

121 for k in range(D): 

122 #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] 

123 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] 

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

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

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

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

128 

129 @cuda.jit(device=gridsync) 

130 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): 

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

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

133 """ 

134 

135 global_id, my_t = cuda.grid(2) 

136 local_id = cuda.threadIdx.x 

137 

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

139 loop_x_shift = loop_x_shift_function(sim_box, cell_length_x) 

140 

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

142 

143 if global_id < num_part and my_t==0: 

144 my_num_nbs = 0 

145 my_num_exclusions = exclusions[global_id, -1] 

146 

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

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

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

150 other_cell_y_unwrapped = my_cell[global_id, 1]+iy 

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

152 shifted_ix = ix + y_wrap_cell * loop_x_shift 

153 

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

155 other_index = ( 

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

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

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

159 other_global_id = cells[other_index] 

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

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

162 #flag = other_global_id < global_id 

163 TwodN = 2*(other_global_id - global_id) 

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

165 else: 

166 flag = other_global_id != global_id 

167 if flag: 

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

169 if dist_sq < cut_plus_skin*cut_plus_skin: 

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

171 for k in range(my_num_exclusions): 

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

173 not_excluded = False 

174 if not_excluded: 

175 my_num_nbs += 1 

176 if my_num_nbs < max_nbs: 

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

178 other_global_id = next_particle_in_cell[other_global_id] 

179 nblist[global_id, -1] = my_num_nbs 

180 

181 # Various house-keeping 

182 for k in range(D): 

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

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

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

186 if global_id == 0 and my_t==0: 

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

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

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

190 

191 

192 @cuda.jit(device=gridsync) 

193 def clear_cells(vectors, sim_box, nbflag, cells_per_dimension, cells, my_cell, next_particle_in_cell): 

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

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

196 """ 

197 

198 global_id, my_t = cuda.grid(2) 

199 local_id = cuda.threadIdx.x 

200 

201 if global_id < num_part and my_t == 0: 

202 for k in range(D): 

203 if my_cell[global_id,k]<0: 

204 print(global_id,k, my_cell[global_id,k],vectors[r_id][global_id,k]) 

205 

206 

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

208 #cells[my_cell[global_id,:]] = -1 # Every body writes, but thats OK 

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

210 next_particle_in_cell[global_id] = -1 

211 

212 

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

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

215 # (Not necessarry, after check moved to final kernel?) 

216 

217 @cuda.jit(device=gridsync) 

218 def nblist_update(vectors, sim_box, cut_plus_skin, nbflag, nblist, r_ref, exclusions): 

219 """ N^2 Update neighbor-list using numba.cuda  

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

221 """ 

222 

223 global_id, my_t = cuda.grid(2) 

224 local_id = cuda.threadIdx.x 

225 

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

227 

228 if global_id < num_part and my_t==0: 

229 nblist[global_id, max_nbs] = 0 # Set number of neighbors (stored at index max_nbs) to zero 

230 

231 cuda.syncthreads() # wait for nblist[global_id, max_nbs] to be ready 

232 

233 if global_id < num_part: 

234 my_num_exclusions = exclusions[global_id,-1] 

235 for i in range(0, num_part, pb*tp): # Loop over blocks 

236 for j in range(pb): # Loop over particles the pb in block 

237 other_global_id = j + i + my_t*pb 

238 if UtilizeNIII: 

239 TwodN = 2*(other_global_id - global_id) 

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

241 else: 

242 flag = other_global_id != global_id and other_global_id < num_part 

243 if flag: 

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

245 if dist_sq < cut_plus_skin*cut_plus_skin: 

246 not_excluded = True # Check exclusion list 

247 for k in range(my_num_exclusions): 

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

249 not_excluded = False 

250 if not_excluded: 

251 my_num_nbs = cuda.atomic.add(nblist, (global_id, max_nbs), 1) # Find next free index in nblist 

252 if my_num_nbs < max_nbs: 

253 nblist[global_id, my_num_nbs] = other_global_id # Last entry is number of neighbors 

254 

255 # Various house-keeping 

256 if global_id < num_part and my_t==0: 

257 for k in range(D): 

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

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

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

261 if global_id == 0 and my_t==0: 

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

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

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

265 

266 return 

267 

268 if gridsync==True: 

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

270 @cuda.jit( device=gridsync ) 

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

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

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

274 grid.sync() 

275 if nbflag[0] > 0: 

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

277 grid.sync() 

278 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) 

279 #nblist_update(vectors, sim_box, max_cut+skin, nbflag, nblist, r_ref, exclusions) 

280 grid.sync() 

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

282 return 

283 return check_and_update 

284 

285 else: 

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

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

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

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

290 if nbflag[0] > 0: 

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

292 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) 

293 #nblist_update[num_blocks, (pb, tp)](vectors, sim_box, max_cut+skin, nbflag, nblist, r_ref, exclusions) # Maybe tp = 1 ??? 

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

295 return 

296 return check_and_update