Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# flake8: noqa 

2"""Tools for analyzing instances of :class:`~ase.Atoms` 

3""" 

4 

5from ase.neighborlist import build_neighbor_list, get_distance_matrix, get_distance_indices 

6from ase.ga.utilities import get_rdf 

7from ase import Atoms 

8 

9 

10__all__ = ['Analysis'] 

11 

12 

13class Analysis: 

14 """Analysis class 

15 

16 Parameters for initialization: 

17 

18 images: :class:`~ase.Atoms` object or list of such 

19 Images to analyze. 

20 nl: None, :class:`~ase.neighborlist.NeighborList` object or list of such 

21 Neighborlist(s) for the given images. One or nImages, depending if bonding 

22 pattern changes or is constant. Using one Neigborlist greatly improves speed. 

23 kwargs: options, dict 

24 Arguments for constructing :class:`~ase.neighborlist.NeighborList` object if :data:`nl` is None. 

25 

26 The choice of ``bothways=True`` for the :class:`~ase.neighborlist.NeighborList` object 

27 will not influence the amount of bonds/angles/dihedrals you get, all are reported 

28 in both directions. Use the *unique*-labeled properties to get lists without 

29 duplicates. 

30 """ 

31 

32 def __init__(self, images, nl=None, **kwargs): 

33 self.images = images 

34 

35 if isinstance(nl, list): 

36 assert len(nl) == self.nImages 

37 self._nl = nl 

38 elif nl is not None: 

39 self._nl = [ nl ] 

40 else: 

41 self._nl = [ build_neighbor_list(self.images[0], **kwargs) ] 

42 

43 self._cache = {} 

44 

45 def _get_slice(self, imageIdx): 

46 """Return a slice from user input. 

47 Using *imageIdx* (can be integer or slice) the analyzed frames can be specified. 

48 If *imageIdx* is None, all frames will be analyzed. 

49 """ 

50 #get slice from imageIdx 

51 if isinstance(imageIdx, int): 

52 sl = slice(imageIdx, imageIdx+1) 

53 elif isinstance(imageIdx, slice): 

54 sl = imageIdx 

55 elif imageIdx is None: 

56 sl = slice(0, None) 

57 else: 

58 raise ValueError("Unsupported type for imageIdx in ase.geometry.analysis.Analysis._get_slice") 

59 return sl 

60 

61 @property 

62 def images(self): 

63 """Images. 

64 

65 Set during initialization but can also be set later. 

66 """ 

67 return self._images 

68 

69 @images.setter 

70 def images(self, images): 

71 """Set images""" 

72 if isinstance(images, list): 

73 self._images = images 

74 else: 

75 self._images = [ images ] 

76 

77 

78 @images.deleter 

79 def images(self): 

80 """Delete images""" 

81 self._images = None 

82 

83 @property 

84 def nImages(self): 

85 """Number of Images in this instance. 

86 

87 Cannot be set, is determined automatically. 

88 """ 

89 return len(self.images) 

90 

91 @property 

92 def nl(self): 

93 """Neighbor Lists in this instance. 

94 

95 Set during initialization. 

96 

97 **No setter or deleter, only getter** 

98 """ 

99 return self._nl 

100 

101 def _get_all_x(self, distance): 

102 """Helper function to get bonds, angles, dihedrals""" 

103 maxIter = self.nImages 

104 if len(self.nl) == 1: 

105 maxIter = 1 

106 

107 xList = [] 

108 for i in range(maxIter): 

109 xList.append(get_distance_indices(self.distance_matrix[i], distance)) 

110 

111 return xList 

112 

113 @property 

114 def all_bonds(self): 

115 """All Bonds. 

116 

117 A list with indices of bonded atoms for each neighborlist in *self*. 

118 Atom i is connected to all atoms inside result[i]. Duplicates from PBCs are 

119 removed. See also :data:`unique_bonds`. 

120 

121 **No setter or deleter, only getter** 

122 """ 

123 if not 'allBonds' in self._cache: 

124 self._cache['allBonds'] = self._get_all_x(1) 

125 

126 return self._cache['allBonds'] 

127 

128 @property 

129 def all_angles(self): 

130 """All angles 

131 

132 A list with indices of atoms in angles for each neighborlist in *self*. 

133 Atom i forms an angle to the atoms inside the tuples in result[i]: 

134 i -- result[i][x][0] -- result[i][x][1] 

135 where x is in range(number of angles from i). See also :data:`unique_angles`. 

136 

137 **No setter or deleter, only getter** 

138 """ 

139 if not 'allAngles' in self._cache: 

140 self._cache['allAngles'] = [] 

141 distList = self._get_all_x(2) 

142 

143 for imI in range(len(distList)): 

144 self._cache['allAngles'].append([]) 

145 #iterate over second neighbors of all atoms 

146 for iAtom, secNeighs in enumerate(distList[imI]): 

147 self._cache['allAngles'][-1].append([]) 

148 if len(secNeighs) == 0: 

149 continue 

150 firstNeighs = self.all_bonds[imI][iAtom] 

151 #iterate over second neighbors of iAtom 

152 for kAtom in secNeighs: 

153 relevantFirstNeighs = [ idx for idx in firstNeighs if kAtom in self.all_bonds[imI][idx] ] 

154 #iterate over all atoms that are connected to iAtom and kAtom 

155 for jAtom in relevantFirstNeighs: 

156 self._cache['allAngles'][-1][-1].append((jAtom, kAtom)) 

157 

158 return self._cache['allAngles'] 

159 

160 @property 

161 def all_dihedrals(self): 

162 """All dihedrals 

163 

164 Returns a list with indices of atoms in dihedrals for each neighborlist in this instance. 

165 Atom i forms a dihedral to the atoms inside the tuples in result[i]: 

166 i -- result[i][x][0] -- result[i][x][1] -- result[i][x][2] 

167 where x is in range(number of dihedrals from i). See also :data:`unique_dihedrals`. 

168 

169 **No setter or deleter, only getter** 

170 """ 

171 if not 'allDihedrals' in self._cache: 

172 self._cache['allDihedrals'] = [] 

173 distList = self._get_all_x(3) 

174 

175 for imI in range(len(distList)): 

176 self._cache['allDihedrals'].append([]) 

177 for iAtom, thirdNeighs in enumerate(distList[imI]): 

178 self._cache['allDihedrals'][-1].append([]) 

179 if len(thirdNeighs) == 0: 

180 continue 

181 anglesI = self.all_angles[imI][iAtom] 

182 #iterate over third neighbors of iAtom 

183 for lAtom in thirdNeighs: 

184 secondNeighs = [ angle[-1] for angle in anglesI ] 

185 firstNeighs = [ angle[0] for angle in anglesI ] 

186 relevantSecondNeighs = [ idx for idx in secondNeighs if lAtom in self.all_bonds[imI][idx] ] 

187 relevantFirstNeighs = [ firstNeighs[secondNeighs.index(idx)] for idx in relevantSecondNeighs ] 

188 #iterate over all atoms that are connected to iAtom and lAtom 

189 for jAtom, kAtom in zip(relevantFirstNeighs, relevantSecondNeighs): 

190 #remove dihedrals in circles 

191 tupl = (jAtom, kAtom, lAtom) 

192 if len(set((iAtom, ) + tupl)) != 4: 

193 continue 

194 #avoid duplicates 

195 elif tupl in self._cache['allDihedrals'][-1][-1]: 

196 continue 

197 elif iAtom in tupl: 

198 raise RuntimeError("Something is wrong in analysis.all_dihedrals!") 

199 self._cache['allDihedrals'][-1][-1].append((jAtom, kAtom, lAtom)) 

200 

201 return self._cache['allDihedrals'] 

202 

203 @property 

204 def adjacency_matrix(self): 

205 """The adjacency/connectivity matrix. 

206 

207 If not already done, build a list of adjacency matrices for all :data:`nl`. 

208 

209 **No setter or deleter, only getter** 

210 """ 

211 

212 if not 'adjacencyMatrix' in self._cache: 

213 self._cache['adjacencyMatrix'] = [] 

214 for i in range(len(self.nl)): 

215 self._cache['adjacencyMatrix'].append(self.nl[i].get_connectivity_matrix()) 

216 

217 return self._cache['adjacencyMatrix'] 

218 

219 @property 

220 def distance_matrix(self): 

221 """The distance matrix. 

222 

223 If not already done, build a list of distance matrices for all :data:`nl`. See 

224 :meth:`ase.neighborlist.get_distance_matrix`. 

225 

226 **No setter or deleter, only getter** 

227 """ 

228 

229 if not 'distanceMatrix' in self._cache: 

230 self._cache['distanceMatrix'] = [] 

231 for i in range(len(self.nl)): 

232 self._cache['distanceMatrix'].append(get_distance_matrix(self.adjacency_matrix[i])) 

233 

234 return self._cache['distanceMatrix'] 

235 

236 

237 @property 

238 def unique_bonds(self): 

239 """Get Unique Bonds. 

240 

241 :data:`all_bonds` i-j without j-i. This is the upper triangle of the 

242 connectivity matrix (i,j), `i < j` 

243 

244 """ 

245 bonds = [] 

246 for imI in range(len(self.all_bonds)): 

247 bonds.append([]) 

248 for iAtom, bonded in enumerate(self.all_bonds[imI]): 

249 bonds[-1].append([ jAtom for jAtom in bonded if jAtom > iAtom ]) 

250 

251 return bonds 

252 

253 

254 def _filter_unique(self, l): 

255 """Helper function to filter for unique lists in a list 

256 that also contains the reversed items. 

257 """ 

258 r = [] 

259 #iterate over images 

260 for imI in range(len(l)): 

261 r.append([]) 

262 #iterate over atoms 

263 for i, tuples in enumerate(l[imI]): 

264 #add the ones where i is smaller than the last element 

265 r[-1].append([ x for x in tuples if i < x[-1] ]) 

266 return r 

267 

268 def clear_cache(self): 

269 """Delete all cached information.""" 

270 self._cache = {} 

271 

272 @property 

273 def unique_angles(self): 

274 """Get Unique Angles. 

275 

276 :data:`all_angles` i-j-k without k-j-i. 

277 

278 """ 

279 return self._filter_unique(self.all_angles) 

280 

281 @property 

282 def unique_dihedrals(self): 

283 """Get Unique Dihedrals. 

284 

285 :data:`all_dihedrals` i-j-k-l without l-k-j-i. 

286 

287 """ 

288 return self._filter_unique(self.all_dihedrals) 

289 

290 

291 def _get_symbol_idxs(self, imI, sym): 

292 """Get list of indices of element *sym*""" 

293 if isinstance(imI, int): 

294 return [ idx for idx in range(len(self.images[imI])) if self.images[imI][idx].symbol == sym ] 

295 else: 

296 return [ idx for idx in range(len(imI)) if imI[idx].symbol == sym ] 

297 

298 

299 def _idxTuple2SymbolTuple(self, imI, tup): 

300 """Converts a tuple of indices to their symbols""" 

301 return ( self.images[imI][idx].symbol for idx in tup ) 

302 

303 

304 def get_bonds(self, A, B, unique=True): 

305 """Get bonds from element A to element B. 

306 

307 Parameters: 

308 

309 A, B: str 

310 Get Bonds between elements A and B 

311 unique: bool 

312 Return the bonds both ways or just one way (A-B and B-A or only A-B) 

313 

314 Returns: 

315 

316 return: list of lists of tuples 

317 return[imageIdx][atomIdx][bondI], each tuple starts with atomIdx. 

318 

319 Use :func:`get_values` to convert the returned list to values. 

320 """ 

321 r = [] 

322 for imI in range(len(self.all_bonds)): 

323 r.append([]) 

324 aIdxs = self._get_symbol_idxs(imI, A) 

325 if A != B: 

326 bIdxs = self._get_symbol_idxs(imI, B) 

327 for idx in aIdxs: 

328 bonded = self.all_bonds[imI][idx] 

329 if A == B: 

330 r[-1].extend([ (idx, x) for x in bonded if ( x in aIdxs ) and ( x > idx ) ]) 

331 else: 

332 r[-1].extend([ (idx, x) for x in bonded if x in bIdxs ]) 

333 

334 if not unique: 

335 r[-1] += [ x[::-1] for x in r[-1] ] 

336 

337 return r 

338 

339 

340 def get_angles(self, A, B, C, unique=True): 

341 """Get angles from given elements A-B-C. 

342 

343 Parameters: 

344 

345 A, B, C: str 

346 Get Angles between elements A, B and C. **B will be the central atom**. 

347 unique: bool 

348 Return the angles both ways or just one way (A-B-C and C-B-A or only A-B-C) 

349 

350 Returns: 

351 

352 return: list of lists of tuples 

353 return[imageIdx][atomIdx][angleI], each tuple starts with atomIdx. 

354 

355 Use :func:`get_values` to convert the returned list to values. 

356 """ 

357 from itertools import product, combinations 

358 r = [] 

359 for imI in range(len(self.all_angles)): 

360 r.append([]) 

361 #Middle Atom is fixed 

362 bIdxs = self._get_symbol_idxs(imI, B) 

363 for bIdx in bIdxs: 

364 bondedA = [ idx for idx in self.all_bonds[imI][bIdx] if self.images[imI][idx].symbol == A ] 

365 if len(bondedA) == 0: 

366 continue 

367 

368 if A != C: 

369 bondedC = [ idx for idx in self.all_bonds[imI][bIdx] if self.images[imI][idx].symbol == C ] 

370 if len(bondedC) == 0: 

371 continue 

372 

373 if A == C: 

374 extend = [ (x[0], bIdx, x[1]) for x in list(combinations(bondedA, 2)) ] 

375 else: 

376 extend = list(product(bondedA, [bIdx], bondedC)) 

377 

378 if not unique: 

379 extend += [ x[::-1] for x in extend ] 

380 

381 r[-1].extend(extend) 

382 return r 

383 

384 

385 def get_dihedrals(self, A, B, C, D, unique=True): 

386 """Get dihedrals A-B-C-D. 

387 

388 Parameters: 

389 

390 A, B, C, D: str 

391 Get Dihedralss between elements A, B, C and D. **B-C will be the central axis**. 

392 unique: bool 

393 Return the dihedrals both ways or just one way (A-B-C-D and D-C-B-A or only A-B-C-D) 

394 

395 Returns: 

396 

397 return: list of lists of tuples 

398 return[imageIdx][atomIdx][dihedralI], each tuple starts with atomIdx. 

399 

400 Use :func:`get_values` to convert the returned list to values. 

401 """ 

402 r = [] 

403 for imI in range(len(self.all_dihedrals)): 

404 r.append([]) 

405 #get indices of elements 

406 aIdxs = self._get_symbol_idxs(imI, A) 

407 bIdxs = self._get_symbol_idxs(imI, B) 

408 cIdxs = self._get_symbol_idxs(imI, C) 

409 dIdxs = self._get_symbol_idxs(imI, D) 

410 for aIdx in aIdxs: 

411 dihedrals = [ (aIdx, ) + d for d in self.all_dihedrals[imI][aIdx] if ( d[0] in bIdxs ) and ( d[1] in cIdxs ) and ( d[2] in dIdxs ) ] 

412 if not unique: 

413 dihedrals += [ d[::-1] for d in dihedrals ] 

414 r[-1].extend(dihedrals) 

415 

416 return r 

417 

418 

419 def get_bond_value(self, imIdx, idxs, mic=True, **kwargs): 

420 """Get bond length. 

421 

422 Parameters: 

423 

424 imIdx: int 

425 Index of Image to get value from. 

426 idxs: tuple or list of integers 

427 Get distance between atoms idxs[0]-idxs[1]. 

428 mic: bool 

429 Passed on to :func:`ase.Atoms.get_distance` for retrieving the value, defaults to True. 

430 If the cell of the image is correctly set, there should be no reason to change this. 

431 kwargs: options or dict 

432 Passed on to :func:`ase.Atoms.get_distance`. 

433 

434 Returns: 

435 

436 return: float 

437 Value returned by image.get_distance. 

438 """ 

439 return self.images[imIdx].get_distance(idxs[0], idxs[1], mic=mic, **kwargs) 

440 

441 def get_angle_value(self, imIdx, idxs, mic=True, **kwargs): 

442 """Get angle. 

443 

444 Parameters: 

445 

446 imIdx: int 

447 Index of Image to get value from. 

448 idxs: tuple or list of integers 

449 Get angle between atoms idxs[0]-idxs[1]-idxs[2]. 

450 mic: bool 

451 Passed on to :func:`ase.Atoms.get_angle` for retrieving the value, defaults to True. 

452 If the cell of the image is correctly set, there should be no reason to change this. 

453 kwargs: options or dict 

454 Passed on to :func:`ase.Atoms.get_angle`. 

455 

456 Returns: 

457 

458 return: float 

459 Value returned by image.get_angle. 

460 """ 

461 return self.images[imIdx].get_angle(idxs[0], idxs[1], idxs[2], mic=True, **kwargs) 

462 

463 def get_dihedral_value(self, imIdx, idxs, mic=True, **kwargs): 

464 """Get dihedral. 

465 

466 Parameters: 

467 

468 imIdx: int 

469 Index of Image to get value from. 

470 idxs: tuple or list of integers 

471 Get angle between atoms idxs[0]-idxs[1]-idxs[2]-idxs[3]. 

472 mic: bool 

473 Passed on to :func:`ase.Atoms.get_dihedral` for retrieving the value, defaults to True. 

474 If the cell of the image is correctly set, there should be no reason to change this. 

475 kwargs: options or dict 

476 Passed on to :func:`ase.Atoms.get_dihedral`. 

477 

478 Returns: 

479 

480 return: float 

481 Value returned by image.get_dihedral. 

482 """ 

483 return self.images[imIdx].get_dihedral(idxs[0], idxs[1], idxs[2], idxs[3], mic=mic, **kwargs) 

484 

485 def get_values(self, inputList, imageIdx=None, mic=True, **kwargs): 

486 """Get Bond/Angle/Dihedral values. 

487 

488 Parameters: 

489 

490 inputList: list of lists of tuples 

491 Can be any list provided by :meth:`~ase.geometry.analysis.Analysis.get_bonds`, 

492 :meth:`~ase.geometry.analysis.Analysis.get_angles` or 

493 :meth:`~ase.geometry.analysis.Analysis.get_dihedrals`. 

494 imageIdx: integer or slice 

495 The images from :data:`images` to be analyzed. If None, all frames will be analyzed. 

496 See :func:`~ase.geometry.analysis.Analysis._get_slice` for details. 

497 mic: bool 

498 Passed on to :class:`~ase.Atoms` for retrieving the values, defaults to True. 

499 If the cells of the images are correctly set, there should be no reason to change this. 

500 kwargs: options or dict 

501 Passed on to the :class:`~ase.Atoms` classes functions for retrieving the values. 

502 

503 Returns: 

504 

505 return: list of lists of floats 

506 return[imageIdx][valueIdx]. Has the same shape as the *inputList*, instead of each 

507 tuple there is a float with the value this tuple yields. 

508 

509 The type of value requested is determined from the length of the tuple inputList[0][0]. 

510 The methods from the :class:`~ase.Atoms` class are used. 

511 """ 

512 

513 sl = self._get_slice(imageIdx) 

514 

515 #get method to call from length of inputList 

516 if len(inputList[0][0]) == 2: 

517 get = self.get_bond_value 

518 elif len(inputList[0][0]) == 3: 

519 get = self.get_angle_value 

520 elif len(inputList[0][0]) == 4: 

521 get = self.get_dihedral_value 

522 else: 

523 raise ValueError("inputList in ase.geometry.analysis.Analysis.get_values has a bad shape.") 

524 

525 #check if length of slice and inputList match 

526 singleNL = False 

527 if len(inputList) != len(self.images[sl]): 

528 #only one nl for all images 

529 if len(inputList) == 1 and len(self.nl) == 1: 

530 singleNL = True 

531 else: 

532 raise RuntimeError("Length of inputList does not match length of \ 

533 images requested, but it also is not one item long.") 

534 

535 r = [] 

536 for inputIdx, image in enumerate(self.images[sl]): 

537 imageIdx = self.images.index(image) 

538 r.append([]) 

539 #always use first list from input if only a single neighborlist was used 

540 if singleNL: 

541 inputIdx = 0 

542 for tupl in inputList[inputIdx]: 

543 r[-1].append(get(imageIdx, tupl, mic=mic, **kwargs)) 

544 

545 return r 

546 

547 

548 def get_rdf(self, rmax, nbins, imageIdx=None, elements=None, return_dists=False): 

549 """Get RDF. 

550 

551 Wrapper for :meth:`ase.ga.utilities.get_rdf` with more selection possibilities. 

552 

553 Parameters: 

554 

555 rmax: float 

556 Maximum distance of RDF. 

557 nbins: int 

558 Number of bins to divide RDF. 

559 imageIdx: int/slice/None 

560 Images to analyze, see :func:`_get_slice` for details. 

561 elements: str/int/list/tuple 

562 Make partial RDFs. 

563 

564 If elements is *None*, a full RDF is calculated. If elements is an *integer* or a *list/tuple 

565 of integers*, only those atoms will contribute to the RDF (like a mask). If elements 

566 is a *string* or a *list/tuple of strings*, only Atoms of those elements will contribute. 

567 

568 Returns: 

569 

570 return: list of lists / list of tuples of lists 

571 If return_dists is True, the returned tuples contain (rdf, distances). Otherwise 

572 only rdfs for each image are returned. 

573 """ 

574 

575 sl = self._get_slice(imageIdx) 

576 

577 r = [] 

578 el = None 

579 

580 for image in self.images[sl]: 

581 if elements is None: 

582 tmpImage = image 

583 #integers 

584 elif isinstance(elements, int): 

585 tmpImage = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

586 tmpImage.append(image[elements]) 

587 #strings 

588 elif isinstance(elements, str): 

589 tmpImage = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

590 for idx in self._get_symbol_idxs(image, elements): 

591 tmpImage.append(image[idx]) 

592 #lists 

593 elif isinstance(elements, list) or isinstance(elements, tuple): 

594 #list of ints 

595 if all(isinstance(x, int) for x in elements): 

596 if len(elements) == 2: 

597 #use builtin get_rdf mask 

598 el = elements 

599 tmpImage = image 

600 else: 

601 #create dummy image 

602 tmpImage = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

603 for idx in elements: 

604 tmpImage.append(image[idx]) 

605 #list of strings 

606 elif all(isinstance(x, str) for x in elements): 

607 tmpImage = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

608 for element in elements: 

609 for idx in self._get_symbol_idxs(image, element): 

610 tmpImage.append(image[idx]) 

611 else: 

612 raise ValueError("Unsupported type of elements given in ase.geometry.analysis.Analysis.get_rdf!") 

613 else: 

614 raise ValueError("Unsupported type of elements given in ase.geometry.analysis.Analysis.get_rdf!") 

615 

616 r.append(get_rdf(tmpImage, rmax, nbins, elements=el, no_dists=(not return_dists))) 

617 return r