Coverage for ase / neighborlist.py: 96.03%

378 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1# fmt: off 

2 

3import itertools 

4 

5import numpy as np 

6import scipy.sparse.csgraph as csgraph 

7from scipy import sparse as sp 

8from scipy.spatial import cKDTree 

9 

10from ase.cell import Cell 

11from ase.data import atomic_numbers, covalent_radii 

12from ase.geometry import ( 

13 complete_cell, 

14 find_mic, 

15 minkowski_reduce, 

16 wrap_positions, 

17) 

18from ase.utils import deprecated 

19 

20 

21def natural_cutoffs(atoms, mult=1, **kwargs): 

22 """Generate a radial cutoff for every atom based on covalent radii. 

23 

24 The covalent radii are a reasonable cutoff estimation for bonds in 

25 many applications such as neighborlists, so function generates an 

26 atoms length list of radii based on this idea. 

27 

28 * atoms: An atoms object 

29 * mult: A multiplier for all cutoffs, useful for coarse grained adjustment 

30 * kwargs: Symbol of the atom and its corresponding cutoff, 

31 used to override the covalent radii 

32 """ 

33 return [kwargs.get(atom.symbol, covalent_radii[atom.number] * mult) 

34 for atom in atoms] 

35 

36 

37def build_neighbor_list(atoms, cutoffs=None, **kwargs): 

38 """Automatically build and update a NeighborList. 

39 

40 Parameters 

41 ---------- 

42 

43 atoms : :class:`~ase.Atoms` object 

44 Atoms to build Neighborlist for. 

45 cutoffs: list of floats 

46 Radii for each atom. If not given it will be produced by calling 

47 :func:`ase.neighborlist.natural_cutoffs` 

48 kwargs: arbitrary number of options 

49 Will be passed to the constructor of 

50 :class:`~ase.neighborlist.NeighborList` 

51 

52 Returns 

53 ------- 

54 

55 return: :class:`~ase.neighborlist.NeighborList` 

56 A :class:`~ase.neighborlist.NeighborList` instance (updated). 

57 """ 

58 if cutoffs is None: 

59 cutoffs = natural_cutoffs(atoms) 

60 

61 nl = NeighborList(cutoffs, **kwargs) 

62 nl.update(atoms) 

63 

64 return nl 

65 

66 

67def get_distance_matrix(graph, limit=3): 

68 """Get Distance Matrix from a Graph. 

69 

70 Parameters 

71 ---------- 

72 

73 graph: array, matrix or sparse matrix, 2 dimensions (N, N) 

74 Graph representation of the connectivity. 

75 See `scipy doc <https://docs.scipy.org/doc/scipy/reference/generated\ 

76/scipy.sparse.csgraph.dijkstra.html#scipy.sparse.csgraph.dijkstra>`_ 

77 for reference. 

78 limit: integer 

79 Maximum number of steps to analyze. For most molecular information, 

80 three should be enough. 

81 

82 Returns 

83 ------- 

84 

85 return: scipy.sparse.csr_matrix, shape (N, N) 

86 A scipy.sparse.csr_matrix. All elements that are not connected within 

87 *limit* steps are set to zero. 

88 

89 This is a potential memory bottleneck, as csgraph.dijkstra produces a 

90 dense output matrix. Here we replace all np.inf values with 0 and 

91 transform back to csr_matrix. 

92 Why not dok_matrix like the connectivity-matrix? Because row-picking 

93 is most likely and this is super fast with csr. 

94 """ 

95 mat = csgraph.dijkstra(graph, directed=False, limit=limit) 

96 mat[mat == np.inf] = 0 

97 return sp.csr_matrix(mat, dtype=np.int8) 

98 

99 

100def get_distance_indices(distanceMatrix, distance): 

101 """Get indices for each node that are distance or less away. 

102 

103 Parameters 

104 ---------- 

105 

106 distanceMatrix: any one of scipy.sparse matrices (NxN) 

107 Matrix containing distance information of atoms. Get it e.g. with 

108 :func:`~ase.neighborlist.get_distance_matrix` 

109 distance: integer 

110 Number of steps to allow. 

111 

112 Returns 

113 ------- 

114 

115 return: list of length N 

116 List of length N. return[i] has all indices connected to item i. 

117 

118 The distance matrix only contains shortest paths, so when looking for 

119 distances longer than one, we need to add the lower values for cases 

120 where atoms are connected via a shorter path too. 

121 """ 

122 shape = distanceMatrix.get_shape() 

123 indices = [] 

124 # iterate over rows 

125 for i in range(shape[0]): 

126 row = distanceMatrix.getrow(i)[0] 

127 # find all non-zero 

128 found = sp.find(row) 

129 # screen for smaller or equal distance 

130 equal = np.where(found[-1] <= distance)[0] 

131 # found[1] contains the indexes 

132 indices.append([found[1][x] for x in equal]) 

133 return indices 

134 

135 

136def mic(dr, cell, pbc=True): 

137 """ 

138 Apply minimum image convention to an array of distance vectors. 

139 

140 Parameters 

141 ---------- 

142 

143 dr : array_like 

144 Array of distance vectors. 

145 cell : array_like 

146 Simulation cell. 

147 pbc : array_like, optional 

148 Periodic boundary conditions in x-, y- and z-direction. Default is to 

149 assume periodic boundaries in all directions. 

150 

151 Returns 

152 ------- 

153 

154 dr : array 

155 Array of distance vectors, wrapped according to the minimum image 

156 convention. 

157 """ 

158 dr, _ = find_mic(dr, cell, pbc) 

159 return dr 

160 

161 

162def primitive_neighbor_list(quantities, pbc, cell, positions, cutoff, 

163 numbers=None, self_interaction=False, 

164 use_scaled_positions=False, max_nbins=1e6): 

165 """Compute a neighbor list for an atomic configuration. 

166 

167 Atoms outside periodic boundaries are mapped into the box. Atoms 

168 outside nonperiodic boundaries are included in the neighbor list 

169 but complexity of neighbor list search for those can become n^2. 

170 

171 The neighbor list is sorted by first atom index 'i', but not by second 

172 atom index 'j'. 

173 

174 Parameters 

175 ---------- 

176 

177 quantities: str 

178 Quantities to compute by the neighbor list algorithm. Each character 

179 in this string defines a quantity. They are returned in a tuple of 

180 the same order. Possible quantities are 

181 

182 * 'i' : first atom index 

183 * 'j' : second atom index 

184 * 'd' : absolute distance 

185 * 'D' : distance vector 

186 * 'S' : shift vector (number of cell boundaries crossed by the bond 

187 between atom i and j). With the shift vector S, the 

188 distances D between atoms can be computed from: 

189 D = positions[j]-positions[i]+S.dot(cell) 

190 pbc: array_like 

191 3-tuple indicating giving periodic boundaries in the three Cartesian 

192 directions. 

193 cell: 3x3 matrix 

194 Unit cell vectors. 

195 positions: list of xyz-positions 

196 Atomic positions. Anything that can be converted to an ndarray of 

197 shape (n, 3) will do: [(x1,y1,z1), (x2,y2,z2), ...]. If 

198 use_scaled_positions is set to true, this must be scaled positions. 

199 cutoff: float or dict 

200 Cutoff for neighbor search. It can be: 

201 

202 * A single float: This is a global cutoff for all elements. 

203 * A dictionary: This specifies cutoff values for element 

204 pairs. Specification accepts element numbers of symbols. 

205 Example: {(1, 6): 1.1, (1, 1): 1.0, ('C', 'C'): 1.85} 

206 * A list/array with a per atom value: This specifies the radius of 

207 an atomic sphere for each atoms. If spheres overlap, atoms are 

208 within each others neighborhood. See 

209 :func:`~ase.neighborlist.natural_cutoffs` 

210 for an example on how to get such a list. 

211 self_interaction: bool 

212 Return the atom itself as its own neighbor if set to true. 

213 Default: False 

214 use_scaled_positions: bool 

215 If set to true, positions are expected to be scaled positions. 

216 max_nbins: int 

217 Maximum number of bins used in neighbor search. This is used to limit 

218 the maximum amount of memory required by the neighbor list. 

219 

220 Returns 

221 ------- 

222 

223 i, j, ... : array 

224 Tuple with arrays for each quantity specified above. Indices in `i` 

225 are returned in ascending order 0..len(a)-1, but the order of (i,j) 

226 pairs is not guaranteed. 

227 

228 """ 

229 

230 # Naming conventions: Suffixes indicate the dimension of an array. The 

231 # following convention is used here: 

232 # c: Cartesian index, can have values 0, 1, 2 

233 # i: Global atom index, can have values 0..len(a)-1 

234 # xyz: Bin index, three values identifying x-, y- and z-component of a 

235 # spatial bin that is used to make neighbor search O(n) 

236 # b: Linearized version of the 'xyz' bin index 

237 # a: Bin-local atom index, i.e. index identifying an atom *within* a 

238 # bin 

239 # p: Pair index, can have value 0 or 1 

240 # n: (Linear) neighbor index 

241 

242 # Return empty neighbor list if no atoms are passed here 

243 if len(positions) == 0: 

244 empty_types = dict(i=(int, (0, )), 

245 j=(int, (0, )), 

246 D=(float, (0, 3)), 

247 d=(float, (0, )), 

248 S=(int, (0, 3))) 

249 retvals = [] 

250 for i in quantities: 

251 dtype, shape = empty_types[i] 

252 retvals += [np.array([], dtype=dtype).reshape(shape)] 

253 if len(retvals) == 1: 

254 return retvals[0] 

255 else: 

256 return tuple(retvals) 

257 

258 # Compute reciprocal lattice vectors. 

259 b1_c, b2_c, b3_c = np.linalg.pinv(cell).T 

260 

261 # Compute distances of cell faces. 

262 l1 = np.linalg.norm(b1_c) 

263 l2 = np.linalg.norm(b2_c) 

264 l3 = np.linalg.norm(b3_c) 

265 face_dist_c = np.array([1 / l1 if l1 > 0 else 1, 

266 1 / l2 if l2 > 0 else 1, 

267 1 / l3 if l3 > 0 else 1]) 

268 

269 if isinstance(cutoff, dict): 

270 max_cutoff = max(cutoff.values()) 

271 else: 

272 if np.isscalar(cutoff): 

273 max_cutoff = cutoff 

274 else: 

275 cutoff = np.asarray(cutoff) 

276 max_cutoff = 2 * np.max(cutoff) 

277 

278 # We use a minimum bin size of 3 A 

279 bin_size = max(max_cutoff, 3) 

280 # Compute number of bins such that a sphere of radius cutoff fits into 

281 # eight neighboring bins. 

282 nbins_c = np.maximum((face_dist_c / bin_size).astype(int), [1, 1, 1]) 

283 nbins = np.prod(nbins_c) 

284 # Make sure we limit the amount of memory used by the explicit bins. 

285 while nbins > max_nbins: 

286 nbins_c = np.maximum(nbins_c // 2, [1, 1, 1]) 

287 nbins = np.prod(nbins_c) 

288 

289 # Compute over how many bins we need to loop in the neighbor list search. 

290 neigh_search_x, neigh_search_y, neigh_search_z = \ 

291 np.ceil(bin_size * nbins_c / face_dist_c).astype(int) 

292 

293 # If we only have a single bin and the system is not periodic, then we 

294 # do not need to search neighboring bins 

295 neigh_search_x = 0 if nbins_c[0] == 1 and not pbc[0] else neigh_search_x 

296 neigh_search_y = 0 if nbins_c[1] == 1 and not pbc[1] else neigh_search_y 

297 neigh_search_z = 0 if nbins_c[2] == 1 and not pbc[2] else neigh_search_z 

298 

299 # Sort atoms into bins. 

300 if use_scaled_positions: 

301 scaled_positions_ic = positions 

302 positions = np.dot(scaled_positions_ic, cell) 

303 else: 

304 scaled_positions_ic = np.linalg.solve(complete_cell(cell).T, 

305 positions.T).T 

306 bin_index_ic = np.floor(scaled_positions_ic * nbins_c).astype(int) 

307 cell_shift_ic = np.zeros_like(bin_index_ic) 

308 

309 for c in range(3): 

310 if pbc[c]: 

311 # (Note: np.divmod does not exist in older numpies) 

312 cell_shift_ic[:, c], bin_index_ic[:, c] = \ 

313 divmod(bin_index_ic[:, c], nbins_c[c]) 

314 else: 

315 bin_index_ic[:, c] = np.clip(bin_index_ic[:, c], 0, nbins_c[c] - 1) 

316 

317 # Convert Cartesian bin index to unique scalar bin index. 

318 bin_index_i = (bin_index_ic[:, 0] + 

319 nbins_c[0] * (bin_index_ic[:, 1] + 

320 nbins_c[1] * bin_index_ic[:, 2])) 

321 

322 # atom_i contains atom index in new sort order. 

323 atom_i = np.argsort(bin_index_i) 

324 bin_index_i = bin_index_i[atom_i] 

325 

326 # Find max number of atoms per bin 

327 max_natoms_per_bin = np.bincount(bin_index_i).max() 

328 

329 # Sort atoms into bins: atoms_in_bin_ba contains for each bin (identified 

330 # by its scalar bin index) a list of atoms inside that bin. This list is 

331 # homogeneous, i.e. has the same size *max_natoms_per_bin* for all bins. 

332 # The list is padded with -1 values. 

333 atoms_in_bin_ba = -np.ones([nbins, max_natoms_per_bin], dtype=int) 

334 for i in range(max_natoms_per_bin): 

335 # Create a mask array that identifies the first atom of each bin. 

336 mask = np.append([True], bin_index_i[:-1] != bin_index_i[1:]) 

337 # Assign all first atoms. 

338 atoms_in_bin_ba[bin_index_i[mask], i] = atom_i[mask] 

339 

340 # Remove atoms that we just sorted into atoms_in_bin_ba. The next 

341 # "first" atom will be the second and so on. 

342 mask = np.logical_not(mask) 

343 atom_i = atom_i[mask] 

344 bin_index_i = bin_index_i[mask] 

345 

346 # Make sure that all atoms have been sorted into bins. 

347 assert len(atom_i) == 0 

348 assert len(bin_index_i) == 0 

349 

350 # Now we construct neighbor pairs by pairing up all atoms within a bin or 

351 # between bin and neighboring bin. atom_pairs_pn is a helper buffer that 

352 # contains all potential pairs of atoms between two bins, i.e. it is a list 

353 # of length max_natoms_per_bin**2. 

354 atom_pairs_pn = np.indices((max_natoms_per_bin, max_natoms_per_bin), 

355 dtype=int) 

356 atom_pairs_pn = atom_pairs_pn.reshape(2, -1) 

357 

358 # Initialized empty neighbor list buffers. 

359 first_at_neightuple_nn = [] 

360 secnd_at_neightuple_nn = [] 

361 cell_shift_vector_x_n = [] 

362 cell_shift_vector_y_n = [] 

363 cell_shift_vector_z_n = [] 

364 

365 # This is the main neighbor list search. We loop over neighboring bins and 

366 # then construct all possible pairs of atoms between two bins, assuming 

367 # that each bin contains exactly max_natoms_per_bin atoms. We then throw 

368 # out pairs involving pad atoms with atom index -1 below. 

369 binz_xyz, biny_xyz, binx_xyz = np.meshgrid(np.arange(nbins_c[2]), 

370 np.arange(nbins_c[1]), 

371 np.arange(nbins_c[0]), 

372 indexing='ij') 

373 # The memory layout of binx_xyz, biny_xyz, binz_xyz is such that computing 

374 # the respective bin index leads to a linearly increasing consecutive list. 

375 # The following assert statement succeeds: 

376 # b_b = (binx_xyz + nbins_c[0] * (biny_xyz + nbins_c[1] * 

377 # binz_xyz)).ravel() 

378 # assert (b_b == np.arange(np.prod(nbins_c))).all() 

379 

380 # First atoms in pair. 

381 _first_at_neightuple_n = atoms_in_bin_ba[:, atom_pairs_pn[0]] 

382 for dz in range(-neigh_search_z, neigh_search_z + 1): 

383 for dy in range(-neigh_search_y, neigh_search_y + 1): 

384 for dx in range(-neigh_search_x, neigh_search_x + 1): 

385 # Bin index of neighboring bin and shift vector. 

386 shiftx_xyz, neighbinx_xyz = divmod(binx_xyz + dx, nbins_c[0]) 

387 shifty_xyz, neighbiny_xyz = divmod(biny_xyz + dy, nbins_c[1]) 

388 shiftz_xyz, neighbinz_xyz = divmod(binz_xyz + dz, nbins_c[2]) 

389 neighbin_b = (neighbinx_xyz + nbins_c[0] * 

390 (neighbiny_xyz + nbins_c[1] * neighbinz_xyz) 

391 ).ravel() 

392 

393 # Second atom in pair. 

394 _secnd_at_neightuple_n = \ 

395 atoms_in_bin_ba[neighbin_b][:, atom_pairs_pn[1]] 

396 

397 # Shift vectors. 

398 _cell_shift_vector_x_n = \ 

399 np.resize(shiftx_xyz.reshape(-1, 1), 

400 (max_natoms_per_bin**2, shiftx_xyz.size)).T 

401 _cell_shift_vector_y_n = \ 

402 np.resize(shifty_xyz.reshape(-1, 1), 

403 (max_natoms_per_bin**2, shifty_xyz.size)).T 

404 _cell_shift_vector_z_n = \ 

405 np.resize(shiftz_xyz.reshape(-1, 1), 

406 (max_natoms_per_bin**2, shiftz_xyz.size)).T 

407 

408 # We have created too many pairs because we assumed each bin 

409 # has exactly max_natoms_per_bin atoms. Remove all surperfluous 

410 # pairs. Those are pairs that involve an atom with index -1. 

411 mask = np.logical_and(_first_at_neightuple_n != -1, 

412 _secnd_at_neightuple_n != -1) 

413 if mask.sum() > 0: 

414 first_at_neightuple_nn += [_first_at_neightuple_n[mask]] 

415 secnd_at_neightuple_nn += [_secnd_at_neightuple_n[mask]] 

416 cell_shift_vector_x_n += [_cell_shift_vector_x_n[mask]] 

417 cell_shift_vector_y_n += [_cell_shift_vector_y_n[mask]] 

418 cell_shift_vector_z_n += [_cell_shift_vector_z_n[mask]] 

419 

420 # Flatten overall neighbor list. 

421 first_at_neightuple_n = np.concatenate(first_at_neightuple_nn) 

422 secnd_at_neightuple_n = np.concatenate(secnd_at_neightuple_nn) 

423 cell_shift_vector_n = np.transpose([np.concatenate(cell_shift_vector_x_n), 

424 np.concatenate(cell_shift_vector_y_n), 

425 np.concatenate(cell_shift_vector_z_n)]) 

426 

427 # Add global cell shift to shift vectors 

428 cell_shift_vector_n += cell_shift_ic[first_at_neightuple_n] - \ 

429 cell_shift_ic[secnd_at_neightuple_n] 

430 

431 # Remove all self-pairs that do not cross the cell boundary. 

432 if not self_interaction: 

433 m = np.logical_not(np.logical_and( 

434 first_at_neightuple_n == secnd_at_neightuple_n, 

435 (cell_shift_vector_n == 0).all(axis=1))) 

436 first_at_neightuple_n = first_at_neightuple_n[m] 

437 secnd_at_neightuple_n = secnd_at_neightuple_n[m] 

438 cell_shift_vector_n = cell_shift_vector_n[m] 

439 

440 # For nonperiodic directions, remove any bonds that cross the domain 

441 # boundary. 

442 for c in range(3): 

443 if not pbc[c]: 

444 m = cell_shift_vector_n[:, c] == 0 

445 first_at_neightuple_n = first_at_neightuple_n[m] 

446 secnd_at_neightuple_n = secnd_at_neightuple_n[m] 

447 cell_shift_vector_n = cell_shift_vector_n[m] 

448 

449 # Sort neighbor list. 

450 i = np.argsort(first_at_neightuple_n) 

451 first_at_neightuple_n = first_at_neightuple_n[i] 

452 secnd_at_neightuple_n = secnd_at_neightuple_n[i] 

453 cell_shift_vector_n = cell_shift_vector_n[i] 

454 

455 # Compute distance vectors. 

456 distance_vector_nc = positions[secnd_at_neightuple_n] - \ 

457 positions[first_at_neightuple_n] + \ 

458 cell_shift_vector_n.dot(cell) 

459 abs_distance_vector_n = \ 

460 np.sqrt(np.sum(distance_vector_nc * distance_vector_nc, axis=1)) 

461 

462 # We have still created too many pairs. Only keep those with distance 

463 # smaller than max_cutoff. 

464 mask = abs_distance_vector_n < max_cutoff 

465 first_at_neightuple_n = first_at_neightuple_n[mask] 

466 secnd_at_neightuple_n = secnd_at_neightuple_n[mask] 

467 cell_shift_vector_n = cell_shift_vector_n[mask] 

468 distance_vector_nc = distance_vector_nc[mask] 

469 abs_distance_vector_n = abs_distance_vector_n[mask] 

470 

471 if isinstance(cutoff, dict) and numbers is not None: 

472 # If cutoff is a dictionary, then the cutoff radii are specified per 

473 # element pair. We now have a list up to maximum cutoff. 

474 per_pair_cutoff_n = np.zeros_like(abs_distance_vector_n) 

475 for (atomic_number1, atomic_number2), c in cutoff.items(): 

476 try: 

477 atomic_number1 = atomic_numbers[atomic_number1] 

478 except KeyError: 

479 pass 

480 try: 

481 atomic_number2 = atomic_numbers[atomic_number2] 

482 except KeyError: 

483 pass 

484 if atomic_number1 == atomic_number2: 

485 mask = np.logical_and( 

486 numbers[first_at_neightuple_n] == atomic_number1, 

487 numbers[secnd_at_neightuple_n] == atomic_number2) 

488 else: 

489 mask = np.logical_or( 

490 np.logical_and( 

491 numbers[first_at_neightuple_n] == atomic_number1, 

492 numbers[secnd_at_neightuple_n] == atomic_number2), 

493 np.logical_and( 

494 numbers[first_at_neightuple_n] == atomic_number2, 

495 numbers[secnd_at_neightuple_n] == atomic_number1)) 

496 per_pair_cutoff_n[mask] = c 

497 mask = abs_distance_vector_n < per_pair_cutoff_n 

498 first_at_neightuple_n = first_at_neightuple_n[mask] 

499 secnd_at_neightuple_n = secnd_at_neightuple_n[mask] 

500 cell_shift_vector_n = cell_shift_vector_n[mask] 

501 distance_vector_nc = distance_vector_nc[mask] 

502 abs_distance_vector_n = abs_distance_vector_n[mask] 

503 elif not np.isscalar(cutoff): 

504 # If cutoff is neither a dictionary nor a scalar, then we assume it is 

505 # a list or numpy array that contains atomic radii. Atoms are neighbors 

506 # if their radii overlap. 

507 mask = abs_distance_vector_n < \ 

508 cutoff[first_at_neightuple_n] + cutoff[secnd_at_neightuple_n] 

509 first_at_neightuple_n = first_at_neightuple_n[mask] 

510 secnd_at_neightuple_n = secnd_at_neightuple_n[mask] 

511 cell_shift_vector_n = cell_shift_vector_n[mask] 

512 distance_vector_nc = distance_vector_nc[mask] 

513 abs_distance_vector_n = abs_distance_vector_n[mask] 

514 

515 # Assemble return tuple. 

516 retvals = [] 

517 for q in quantities: 

518 if q == 'i': 

519 retvals += [first_at_neightuple_n] 

520 elif q == 'j': 

521 retvals += [secnd_at_neightuple_n] 

522 elif q == 'D': 

523 retvals += [distance_vector_nc] 

524 elif q == 'd': 

525 retvals += [abs_distance_vector_n] 

526 elif q == 'S': 

527 retvals += [cell_shift_vector_n] 

528 else: 

529 raise ValueError('Unsupported quantity specified.') 

530 if len(retvals) == 1: 

531 return retvals[0] 

532 else: 

533 return tuple(retvals) 

534 

535 

536def neighbor_list(quantities, a, cutoff, self_interaction=False, 

537 max_nbins=1e6): 

538 """Compute a neighbor list for an atomic configuration. 

539 

540 Atoms outside periodic boundaries are mapped into the box. Atoms 

541 outside nonperiodic boundaries are included in the neighbor list 

542 but complexity of neighbor list search for those can become n^2. 

543 

544 The neighbor list is sorted by first atom index 'i', but not by second 

545 atom index 'j'. 

546 

547 Parameters 

548 ---------- 

549 quantities: str 

550 Quantities to compute by the neighbor list algorithm. Each character 

551 in this string defines a quantity. They are returned in a tuple of 

552 the same order. Possible quantities are: 

553 

554 * 'i' : first atom index 

555 * 'j' : second atom index 

556 * 'd' : absolute distance 

557 * 'D' : distance vector 

558 * 'S' : shift vector (number of cell boundaries crossed by the bond 

559 between atom i and j). With the shift vector S, the 

560 distances D between atoms can be computed from: 

561 D = a.positions[j]-a.positions[i]+S.dot(a.cell) 

562 a: :class:`ase.Atoms` 

563 Atomic configuration. 

564 cutoff: float or dict 

565 Cutoff for neighbor search. It can be: 

566 

567 * A single float: This is a global cutoff for all elements. 

568 * A dictionary: This specifies cutoff values for element 

569 pairs. Specification accepts element numbers of symbols. 

570 Example: {(1, 6): 1.1, (1, 1): 1.0, ('C', 'C'): 1.85} 

571 * A list/array with a per atom value: This specifies the radius of 

572 an atomic sphere for each atoms. If spheres overlap, atoms are 

573 within each others neighborhood. See 

574 :func:`~ase.neighborlist.natural_cutoffs` 

575 for an example on how to get such a list. 

576 

577 self_interaction: bool 

578 Return the atom itself as its own neighbor if set to true. 

579 Default: False 

580 max_nbins: int 

581 Maximum number of bins used in neighbor search. This is used to limit 

582 the maximum amount of memory required by the neighbor list. 

583 

584 Returns 

585 ------- 

586 i, j, ...: array 

587 Tuple with arrays for each quantity specified above. Indices in `i` 

588 are returned in ascending order 0..len(a), but the order of (i,j) 

589 pairs is not guaranteed. 

590 

591 Examples 

592 -------- 

593 

594 >>> import numpy as np 

595 >>> from ase.build import bulk, molecule 

596 

597 1. Coordination counting 

598 

599 >>> atoms = molecule('isobutane') 

600 >>> i = neighbor_list('i', atoms, 1.85) 

601 >>> coord = np.bincount(i, minlength=len(atoms)) 

602 

603 2. Coordination counting with different cutoffs for each pair of species 

604 

605 >>> cutoff = {('H', 'H'): 1.1, ('C', 'H'): 1.3, ('C', 'C'): 1.85} 

606 >>> i = neighbor_list('i', atoms, cutoff) 

607 >>> coord = np.bincount(i, minlength=len(atoms)) 

608 

609 3. Pair distribution function 

610 

611 >>> atoms = bulk('Cu', cubic=True) * 3 

612 >>> atoms.rattle(0.5, rng=np.random.default_rng(42)) 

613 >>> cutoff = 5.0 

614 >>> d = neighbor_list('d', atoms, cutoff) 

615 >>> hist, bin_edges = np.histogram(d, bins=100, range=(0.0, cutoff)) 

616 >>> hist = hist / len(atoms) # per atom 

617 >>> rho_mean = len(atoms) / atoms.cell.volume 

618 >>> dv = 4.0 * np.pi * (bin_edges[1:] ** 3 - bin_edges[:-1] ** 3) / 3.0 

619 >>> rho = hist / dv 

620 >>> pdf = rho / rho_mean 

621 

622 4. Forces of a pair potential 

623 

624 >>> natoms = len(atoms) 

625 >>> i, j, d, D = neighbor_list('ijdD', atoms, 5.0) 

626 >>> # Lennard-Jones potential 

627 >>> eps = 1.0 

628 >>> sgm = 1.0 

629 >>> epairs = 4.0 * eps * ((sgm / d) ** 12 - (sgm / d) ** 6) 

630 >>> energy = 0.5 * epairs.sum() # correct double-counting 

631 >>> dd = -4.0 * eps * (12 * (sgm / d) ** 13 - 6 * (sgm / d) ** 7) / sgm 

632 >>> dd = (dd * (D.T / d)).T 

633 >>> fx = -1.0 * np.bincount(i, weights=dd[:, 0], minlength=natoms) 

634 >>> fy = -1.0 * np.bincount(i, weights=dd[:, 1], minlength=natoms) 

635 >>> fz = -1.0 * np.bincount(i, weights=dd[:, 2], minlength=natoms) 

636 

637 5. Force-constant matrix of a pair potential 

638 

639 >>> i, j, d, D = neighbor_list('ijdD', atoms, 5.0) 

640 >>> epairs = 1.0 / d # Coulomb potential 

641 >>> forces = (D.T / d**3).T # (npairs, 3) 

642 >>> # second derivative 

643 >>> d2 = 3.0 * D[:, :, None] * D[:, None, :] / d[:, None, None] ** 5 

644 >>> for k in range(3): 

645 ... d2[:, k, k] -= 1.0 / d**3 

646 >>> # force-constant matrix 

647 >>> fc = np.zeros((natoms, 3, natoms, 3)) 

648 >>> for ia in range(natoms): 

649 ... for ja in range(natoms): 

650 ... fc[ia, :, ja, :] -= d2[(i == ia) & (j == ja), :, :].sum(axis=0) 

651 >>> for ia in range(natoms): 

652 ... fc[ia, :, ia, :] -= fc[ia].sum(axis=1) 

653 

654 """ 

655 return primitive_neighbor_list(quantities, a.pbc, 

656 a.get_cell(complete=True), 

657 a.positions, cutoff, numbers=a.numbers, 

658 self_interaction=self_interaction, 

659 max_nbins=max_nbins) 

660 

661 

662def first_neighbors(natoms, first_atom): 

663 """ 

664 Compute an index array pointing to the ranges within the neighbor list that 

665 contain the neighbors for a certain atom. 

666 

667 Parameters 

668 ---------- 

669 

670 natoms : int 

671 Total number of atom. 

672 first_atom : array_like 

673 Array containing the first atom 'i' of the neighbor tuple returned 

674 by the neighbor list. 

675 

676 Returns 

677 ------- 

678 

679 seed : array 

680 Array containing pointers to the start and end location of the 

681 neighbors of a certain atom. Neighbors of atom k have indices from s[k] 

682 to s[k+1]-1. 

683 """ 

684 if len(first_atom) == 0: 

685 return np.zeros(natoms + 1, dtype=int) 

686 # Create a seed array (which is returned by this function) populated with 

687 # -1. 

688 seed = -np.ones(natoms + 1, dtype=int) 

689 

690 first_atom = np.asarray(first_atom) 

691 

692 # Mask array contains all position where the number in the (sorted) array 

693 # with first atoms (in the neighbor pair) changes. 

694 mask = first_atom[:-1] != first_atom[1:] 

695 

696 # Seed array needs to start at 0 

697 seed[first_atom[0]] = 0 

698 # Seed array needs to stop at the length of the neighbor list 

699 seed[-1] = len(first_atom) 

700 # Populate all intermediate seed with the index of where the mask array is 

701 # true, i.e. the index where the first_atom array changes. 

702 seed[first_atom[1:][mask]] = (np.arange(len(mask)) + 1)[mask] 

703 

704 # Now fill all remaining -1 value with the value in the seed array right 

705 # behind them. (There are no neighbor so seed[i] and seed[i+1] must point) 

706 # to the same index. 

707 mask = seed == -1 

708 while mask.any(): 

709 seed[mask] = seed[np.arange(natoms + 1)[mask] + 1] 

710 mask = seed == -1 

711 return seed 

712 

713 

714def get_connectivity_matrix(nl, sparse=True): 

715 """Return connectivity matrix for a given NeighborList (dtype=numpy.int8). 

716 

717 A matrix of shape (nAtoms, nAtoms) will be returned. 

718 Connected atoms i and j will have matrix[i,j] == 1, unconnected 

719 matrix[i,j] == 0. If bothways=True the matrix will be symmetric, 

720 otherwise not! 

721 

722 If *sparse* is True, a scipy csr matrix is returned. 

723 If *sparse* is False, a numpy matrix is returned. 

724 

725 Note that the old and new neighborlists might give different results 

726 for periodic systems if bothways=False. 

727 

728 Example: 

729 

730 Determine which molecule in a system atom 1 belongs to. 

731 

732 >>> from scipy import sparse 

733 

734 >>> from ase.build import molecule 

735 >>> from ase.neighborlist import get_connectivity_matrix 

736 >>> from ase.neighborlist import natural_cutoffs 

737 >>> from ase.neighborlist import NeighborList 

738 

739 >>> mol = molecule('CH3CH2OH') 

740 >>> cutoffs = natural_cutoffs(mol) 

741 >>> neighbor_list = NeighborList( 

742 ... cutoffs, self_interaction=False, bothways=True) 

743 >>> neighbor_list.update(mol) 

744 True 

745 

746 >>> matrix = neighbor_list.get_connectivity_matrix() 

747 >>> # or: matrix = get_connectivity_matrix(neighbor_list.nl) 

748 >>> n_components, component_list = sparse.csgraph.connected_components( 

749 ... matrix) 

750 >>> idx = 1 

751 >>> molIdx = component_list[idx] 

752 >>> print("There are {} molecules in the system".format(n_components)) 

753 There are 1 molecules in the system 

754 >>> print("Atom {} is part of molecule {}".format(idx, molIdx)) 

755 Atom 1 is part of molecule 0 

756 >>> molIdxs = [i for i in range(len(component_list)) 

757 ... if component_list[i] == molIdx] 

758 >>> print("Atoms are part of molecule {}: {}".format(molIdx, molIdxs)) 

759 Atoms are part of molecule 0: [0, 1, 2, 3, 4, 5, 6, 7, 8] 

760 """ 

761 

762 nAtoms = len(nl.cutoffs) 

763 

764 if nl.nupdates <= 0: 

765 raise RuntimeError( 

766 'Must call update(atoms) on your neighborlist first!') 

767 

768 if sparse: 

769 matrix = sp.dok_matrix((nAtoms, nAtoms), dtype=np.int8) 

770 else: 

771 matrix = np.zeros((nAtoms, nAtoms), dtype=np.int8) 

772 

773 for i in range(nAtoms): 

774 for idx in nl.get_neighbors(i)[0]: 

775 matrix[i, idx] = 1 

776 

777 return matrix 

778 

779 

780class NewPrimitiveNeighborList: 

781 """Neighbor list object. Wrapper around neighbor_list and first_neighbors. 

782 

783 cutoffs: list of float 

784 List of cutoff radii - one for each atom. If the spheres (defined by 

785 their cutoff radii) of two atoms overlap, they will be counted as 

786 neighbors. 

787 skin: float 

788 If no atom has moved more than the skin-distance since the 

789 last call to the 

790 :meth:`~ase.neighborlist.NewPrimitiveNeighborList.update()` 

791 method, then the neighbor list can be reused. This will save 

792 some expensive rebuilds of the list, but extra neighbors outside 

793 the cutoff will be returned. 

794 sorted: bool 

795 Sort neighbor list. 

796 self_interaction: bool 

797 Should an atom return itself as a neighbor? 

798 bothways: bool 

799 Return all neighbors. Default is to return only "half" of 

800 the neighbors. 

801 

802 Example: 

803 

804 >>> from ase.build import bulk 

805 >>> from ase.neighborlist import NewPrimitiveNeighborList 

806 

807 >>> nl = NewPrimitiveNeighborList([2.3, 1.7]) 

808 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

809 >>> nl.update(atoms.pbc, atoms.get_cell(), atoms.positions) 

810 True 

811 >>> indices, offsets = nl.get_neighbors(0) 

812 """ 

813 

814 def __init__(self, cutoffs, skin=0.3, sorted=False, self_interaction=True, 

815 bothways=False, use_scaled_positions=False): 

816 self.cutoffs = np.asarray(cutoffs) + skin 

817 self.skin = skin 

818 self.sorted = sorted 

819 self.self_interaction = self_interaction 

820 self.bothways = bothways 

821 self.nupdates = 0 

822 self.use_scaled_positions = use_scaled_positions 

823 

824 def update(self, pbc, cell, positions, numbers=None): 

825 """Make sure the list is up to date.""" 

826 

827 if self.nupdates == 0: 

828 self.build(pbc, cell, positions, numbers=numbers) 

829 return True 

830 

831 if ((self.pbc != pbc).any() or (self.cell != cell).any() or 

832 ((self.positions - positions)**2).sum(1).max() > self.skin**2): 

833 self.build(pbc, cell, positions, numbers=numbers) 

834 return True 

835 

836 return False 

837 

838 def build(self, pbc, cell, positions, numbers=None): 

839 """Build the list. 

840 """ 

841 self.pbc = np.array(pbc, copy=True) 

842 self.cell = np.array(cell, copy=True) 

843 self.positions = np.array(positions, copy=True) 

844 

845 pair_first, pair_second, offset_vec = \ 

846 primitive_neighbor_list( 

847 'ijS', pbc, cell, positions, self.cutoffs, numbers=numbers, 

848 self_interaction=self.self_interaction, 

849 use_scaled_positions=self.use_scaled_positions) 

850 

851 if len(positions) > 0 and not self.bothways: 

852 offset_x, offset_y, offset_z = offset_vec.T 

853 

854 mask = offset_z > 0 

855 mask &= offset_y == 0 

856 mask |= offset_y > 0 

857 mask &= offset_x == 0 

858 mask |= offset_x > 0 

859 mask |= (pair_first <= pair_second) & (offset_vec == 0).all(axis=1) 

860 

861 pair_first = pair_first[mask] 

862 pair_second = pair_second[mask] 

863 offset_vec = offset_vec[mask] 

864 

865 if len(positions) > 0 and self.sorted: 

866 mask = np.argsort(pair_first * len(pair_first) + 

867 pair_second) 

868 pair_first = pair_first[mask] 

869 pair_second = pair_second[mask] 

870 offset_vec = offset_vec[mask] 

871 

872 self.pair_first = pair_first 

873 self.pair_second = pair_second 

874 self.offset_vec = offset_vec 

875 

876 # Compute the index array point to the first neighbor 

877 self.first_neigh = first_neighbors(len(positions), pair_first) 

878 

879 self.nupdates += 1 

880 

881 def get_neighbors(self, a): 

882 """Return neighbors of atom number a. 

883 

884 A list of indices and offsets to neighboring atoms is 

885 returned. The positions of the neighbor atoms can be 

886 calculated like this: 

887 

888 >>> from ase.build import bulk 

889 >>> from ase.neighborlist import NewPrimitiveNeighborList 

890 

891 >>> nl = NewPrimitiveNeighborList([2.3, 1.7]) 

892 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

893 >>> nl.update(atoms.pbc, atoms.get_cell(), atoms.positions) 

894 True 

895 >>> indices, offsets = nl.get_neighbors(0) 

896 >>> for i, offset in zip(indices, offsets): 

897 ... print(atoms.positions[i] + offset @ atoms.get_cell()) 

898 ... # doctest: +SKIP 

899 

900 Notice that if get_neighbors(a) gives atom b as a neighbor, 

901 then get_neighbors(b) will not return a as a neighbor - unless 

902 bothways=True was used.""" 

903 

904 return (self.pair_second[self.first_neigh[a]:self.first_neigh[a + 1]], 

905 self.offset_vec[self.first_neigh[a]:self.first_neigh[a + 1]]) 

906 

907 

908class PrimitiveNeighborList: 

909 """Neighbor list that works without Atoms objects. 

910 

911 This is less fancy, but can be used to avoid conversions between 

912 scaled and non-scaled coordinates which may affect cell offsets 

913 through rounding errors. 

914 

915 Attributes 

916 ---------- 

917 nupdates : int 

918 Number of updated times. 

919 """ 

920 

921 def __init__(self, cutoffs, skin=0.3, sorted=False, self_interaction=True, 

922 bothways=False, use_scaled_positions=False): 

923 self.cutoffs = np.asarray(cutoffs) + skin 

924 self.skin = skin 

925 self.sorted = sorted 

926 self.self_interaction = self_interaction 

927 self.bothways = bothways 

928 self.nupdates = 0 

929 self.use_scaled_positions = use_scaled_positions 

930 

931 def update(self, pbc, cell, coordinates): 

932 """Make sure the list is up to date. 

933 

934 Returns 

935 ------- 

936 bool 

937 True if the neighbor list is updated. 

938 """ 

939 

940 if self.nupdates == 0: 

941 self.build(pbc, cell, coordinates) 

942 return True 

943 

944 if ((self.pbc != pbc).any() or (self.cell != cell).any() or ( 

945 (self.coordinates 

946 - coordinates)**2).sum(1).max() > self.skin**2): 

947 self.build(pbc, cell, coordinates) 

948 return True 

949 

950 return False 

951 

952 def build(self, pbc, cell, coordinates): 

953 """Build the list. 

954 

955 Coordinates are taken to be scaled or not according 

956 to self.use_scaled_positions. 

957 """ 

958 self.pbc = pbc = np.array(pbc, copy=True) 

959 self.cell = cell = Cell(cell) 

960 self.coordinates = coordinates = np.array(coordinates, copy=True) 

961 

962 if len(self.cutoffs) != len(coordinates): 

963 raise ValueError('Wrong number of cutoff radii: {} != {}' 

964 .format(len(self.cutoffs), len(coordinates))) 

965 

966 rcmax = self.cutoffs.max() if len(self.cutoffs) > 0 else 0.0 

967 

968 if self.use_scaled_positions: 

969 positions0 = cell.cartesian_positions(coordinates) 

970 else: 

971 positions0 = coordinates 

972 

973 rcell, op = minkowski_reduce(cell, pbc) 

974 positions = wrap_positions(positions0, rcell, pbc=pbc, eps=0) 

975 

976 natoms = len(positions) 

977 self.nupdates += 1 

978 if natoms == 0: 

979 self.neighbors = [] 

980 self.displacements = [] 

981 return 

982 

983 tree = cKDTree(positions, copy_data=True) 

984 offsets = cell.scaled_positions(positions - positions0) 

985 offsets = offsets.round().astype(int) 

986 

987 N = _calc_expansion(rcell, pbc, rcmax) 

988 

989 neighbor_indices_a = [[] for _ in range(natoms)] 

990 displacements_a = [[] for _ in range(natoms)] 

991 

992 for n1, n2, n3 in itertools.product(range(N[0] + 1), 

993 range(-N[1], N[1] + 1), 

994 range(-N[2], N[2] + 1)): 

995 if n1 == 0 and (n2 < 0 or n2 == 0 and n3 < 0): 

996 continue 

997 

998 displacement = (n1, n2, n3) @ rcell 

999 shift0 = (n1, n2, n3) @ op 

1000 indices_all = tree.query_ball_point( 

1001 positions - displacement, 

1002 r=self.cutoffs + rcmax, 

1003 ) 

1004 

1005 for a in range(natoms): 

1006 indices = indices_all[a] 

1007 

1008 if not indices: 

1009 continue 

1010 

1011 indices = np.array(indices) 

1012 delta = positions[indices] + displacement - positions[a] 

1013 distances = np.sqrt(np.add.reduce(delta**2, axis=1)) 

1014 cutoffs = self.cutoffs[indices] + self.cutoffs[a] 

1015 i = indices[distances < cutoffs] 

1016 if n1 == 0 and n2 == 0 and n3 == 0: 

1017 if self.self_interaction: 

1018 i = i[i >= a] 

1019 else: 

1020 i = i[i > a] 

1021 

1022 neighbor_indices_a[a].append(i) 

1023 

1024 disp = shift0 + offsets[i] - offsets[a] 

1025 displacements_a[a].append(disp) 

1026 

1027 self.neighbors = [np.concatenate(i) for i in neighbor_indices_a] 

1028 self.displacements = [np.concatenate(d) for d in displacements_a] 

1029 

1030 if self.bothways: 

1031 neighbors2 = [[] for a in range(natoms)] 

1032 displacements2 = [[] for a in range(natoms)] 

1033 for a in range(natoms): 

1034 for b, disp in zip(self.neighbors[a], self.displacements[a]): 

1035 # avoid double counting of self interaction 

1036 if a == b and (disp == 0).all(): 

1037 continue 

1038 neighbors2[b].append(a) 

1039 displacements2[b].append(-disp) 

1040 for a in range(natoms): 

1041 nbs = np.concatenate((self.neighbors[a], neighbors2[a])) 

1042 disp = np.array(list(self.displacements[a]) + displacements2[a]) 

1043 # Force correct type and shape for case of no neighbors: 

1044 self.neighbors[a] = nbs.astype(int) 

1045 self.displacements[a] = disp.astype(int).reshape((-1, 3)) 

1046 

1047 if self.sorted: 

1048 _sort_neighbors(self.neighbors, self.displacements) 

1049 

1050 def get_neighbors(self, a): 

1051 """Return neighbors of atom number a. 

1052 

1053 A list of indices and offsets to neighboring atoms is 

1054 returned. The positions of the neighbor atoms can be 

1055 calculated like this: 

1056 

1057 >>> from ase.build import bulk 

1058 >>> from ase.neighborlist import NewPrimitiveNeighborList 

1059 

1060 >>> nl = NewPrimitiveNeighborList([2.3, 1.7]) 

1061 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

1062 >>> nl.update(atoms.pbc, atoms.get_cell(), atoms.positions) 

1063 True 

1064 >>> indices, offsets = nl.get_neighbors(0) 

1065 >>> for i, offset in zip(indices, offsets): 

1066 ... print(atoms.positions[i] + offset @ atoms.get_cell()) 

1067 ... # doctest: +SKIP 

1068 

1069 Notice that if get_neighbors(a) gives atom b as a neighbor, 

1070 then get_neighbors(b) will not return a as a neighbor - unless 

1071 bothways=True was used.""" 

1072 

1073 return self.neighbors[a], self.displacements[a] 

1074 

1075 

1076def _calc_expansion(rcell, pbc, rcmax): 

1077 r"""Calculate expansion to contain a sphere of radius `2.0 * rcmax`. 

1078 

1079 This function determines the minimum supercell (parallelepiped) that 

1080 contains a sphere of radius `2.0 * rcmax`. For this, `a_1` is projected 

1081 onto the unit vector perpendicular to `a_2 \times a_3` (i.e. the unit 

1082 vector along the direction `b_1`) to know how many `a_1`'s the supercell 

1083 takes to contain the sphere. 

1084 """ 

1085 ircell = np.linalg.pinv(rcell) 

1086 vs = np.sqrt(np.add.reduce(ircell**2, axis=0)) 

1087 ns = np.where(pbc, np.ceil(2.0 * rcmax * vs), 0.0) 

1088 return ns.astype(int) 

1089 

1090 

1091def _sort_neighbors(neighbors, offsets): 

1092 """Sort neighbors first by indices and then offsets.""" 

1093 natoms = len(neighbors) 

1094 for a in range(natoms): 

1095 keys = ( 

1096 offsets[a][:, 2], 

1097 offsets[a][:, 1], 

1098 offsets[a][:, 0], 

1099 neighbors[a] 

1100 ) 

1101 mask = np.lexsort(keys) 

1102 neighbors[a] = neighbors[a][mask] 

1103 offsets[a] = offsets[a][mask] 

1104 

1105 

1106class NeighborList: 

1107 """Neighbor list object. 

1108 

1109 cutoffs: list of float 

1110 List of cutoff radii - one for each atom. If the spheres 

1111 (defined by their cutoff radii) of two atoms overlap, they 

1112 will be counted as neighbors. See 

1113 :func:`~ase.neighborlist.natural_cutoffs` for an example on 

1114 how to get such a list. 

1115 

1116 skin: float 

1117 If no atom has moved more than the skin-distance since the 

1118 last call to the 

1119 :meth:`~ase.neighborlist.NeighborList.update()` method, then 

1120 the neighbor list can be reused. This will save some 

1121 expensive rebuilds of the list, but extra neighbors outside 

1122 the cutoff will be returned. 

1123 self_interaction: bool 

1124 Should an atom return itself as a neighbor? 

1125 bothways: bool 

1126 Return all neighbors. Default is to return only "half" of 

1127 the neighbors. 

1128 primitive: class 

1129 Define which implementation to use. Older and quadratically-scaling 

1130 :class:`~ase.neighborlist.PrimitiveNeighborList` or newer and 

1131 linearly-scaling :class:`~ase.neighborlist.NewPrimitiveNeighborList`. 

1132 

1133 Example: 

1134 

1135 >>> from ase.build import molecule 

1136 >>> from ase.neighborlist import NeighborList 

1137 

1138 >>> atoms = molecule("CO") 

1139 >>> nl = NeighborList([0.76, 0.66]) 

1140 >>> nl.update(atoms) 

1141 True 

1142 >>> indices, offsets = nl.get_neighbors(0) 

1143 

1144 """ 

1145 

1146 def __init__(self, cutoffs, skin=0.3, sorted=False, self_interaction=True, 

1147 bothways=False, primitive=PrimitiveNeighborList): 

1148 self.nl = primitive(cutoffs, skin, sorted, 

1149 self_interaction=self_interaction, 

1150 bothways=bothways) 

1151 

1152 def update(self, atoms): 

1153 """ 

1154 See :meth:`ase.neighborlist.PrimitiveNeighborList.update` or 

1155 :meth:`ase.neighborlist.PrimitiveNeighborList.update`. 

1156 """ 

1157 return self.nl.update(atoms.pbc, atoms.get_cell(complete=True), 

1158 atoms.positions) 

1159 

1160 def get_neighbors(self, a): 

1161 """ 

1162 See :meth:`ase.neighborlist.PrimitiveNeighborList.get_neighbors` or 

1163 :meth:`ase.neighborlist.PrimitiveNeighborList.get_neighbors`. 

1164 """ 

1165 if self.nl.nupdates <= 0: 

1166 raise RuntimeError('Must call update(atoms) on your neighborlist ' 

1167 'first!') 

1168 

1169 return self.nl.get_neighbors(a) 

1170 

1171 def get_connectivity_matrix(self, sparse=True): 

1172 """ 

1173 See :func:`~ase.neighborlist.get_connectivity_matrix`. 

1174 """ 

1175 return get_connectivity_matrix(self.nl, sparse) 

1176 

1177 @property 

1178 def nupdates(self): 

1179 """Get number of updates.""" 

1180 return self.nl.nupdates 

1181 

1182 @property 

1183 @deprecated( 

1184 'Use, e.g., `sum(_.size for _ in nl.neighbors)` ' 

1185 'for `bothways=False` and `self_interaction=False`.' 

1186 ) 

1187 def nneighbors(self): 

1188 """Get number of neighbors. 

1189 

1190 .. deprecated:: 3.24.0 

1191 """ 

1192 nneighbors = sum(indices.size for indices in self.nl.neighbors) 

1193 if self.nl.self_interaction: 

1194 nneighbors -= len(self.nl.neighbors) 

1195 return nneighbors // 2 if self.nl.bothways else nneighbors 

1196 

1197 @property 

1198 @deprecated( 

1199 'Use, e.g., `sum(_.any(1).sum() for _ in nl.displacements)` ' 

1200 'for `bothways=False` and `self_interaction=False`.' 

1201 ) 

1202 def npbcneighbors(self): 

1203 """Get number of pbc neighbors. 

1204 

1205 .. deprecated:: 3.24.0 

1206 """ 

1207 nneighbors = sum( 

1208 offsets.any(axis=1).sum() for offsets in self.nl.displacements 

1209 ) # sum up all neighbors that have non-zero supercell offsets 

1210 return nneighbors // 2 if self.nl.bothways else nneighbors