Coverage for /builds/kinetik161/ase/ase/geometry/analysis.py: 70.72%

263 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1# flake8: noqa 

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

3""" 

4 

5from typing import List, Optional 

6 

7import numpy as np 

8 

9from ase import Atoms 

10from ase.geometry.rdf import get_containing_cell_length, get_rdf 

11from ase.neighborlist import (build_neighbor_list, get_distance_indices, 

12 get_distance_matrix) 

13 

14__all__ = ['Analysis'] 

15 

16 

17def get_max_containing_cell_length(images: List[Atoms]): 

18 i2diff = np.zeros(3) 

19 for image in images: 

20 np.maximum(get_containing_cell_length(image), i2diff, out=i2diff) 

21 return i2diff 

22 

23 

24def get_max_volume_estimate(images: List[Atoms]) -> float: 

25 return np.prod(get_max_containing_cell_length(images)) 

26 

27 

28class Analysis: 

29 """Analysis class 

30 

31 Parameters for initialization: 

32 

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

34 Images to analyze. 

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

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

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

38 kwargs: options, dict 

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

40 

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

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

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

44 duplicates. 

45 """ 

46 

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

48 self.images = images 

49 

50 if isinstance(nl, list): 

51 assert len(nl) == self.nImages 

52 self._nl = nl 

53 elif nl is not None: 

54 self._nl = [nl] 

55 else: 

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

57 

58 self._cache = {} 

59 

60 def _get_slice(self, imageIdx): 

61 """Return a slice from user input. 

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

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

64 """ 

65 # get slice from imageIdx 

66 if isinstance(imageIdx, int): 

67 sl = slice(imageIdx, imageIdx + 1) 

68 elif isinstance(imageIdx, slice): 

69 sl = imageIdx 

70 elif imageIdx is None: 

71 sl = slice(0, None) 

72 else: 

73 raise ValueError( 

74 "Unsupported type for imageIdx in ase.geometry.analysis.Analysis._get_slice") 

75 return sl 

76 

77 @property 

78 def images(self): 

79 """Images. 

80 

81 Set during initialization but can also be set later. 

82 """ 

83 return self._images 

84 

85 @images.setter 

86 def images(self, images): 

87 """Set images""" 

88 if isinstance(images, list): 

89 self._images = images 

90 else: 

91 self._images = [images] 

92 

93 @images.deleter 

94 def images(self): 

95 """Delete images""" 

96 self._images = None 

97 

98 @property 

99 def nImages(self): 

100 """Number of Images in this instance. 

101 

102 Cannot be set, is determined automatically. 

103 """ 

104 return len(self.images) 

105 

106 @property 

107 def nl(self): 

108 """Neighbor Lists in this instance. 

109 

110 Set during initialization. 

111 

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

113 """ 

114 return self._nl 

115 

116 def _get_all_x(self, distance): 

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

118 maxIter = self.nImages 

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

120 maxIter = 1 

121 

122 xList = [] 

123 for i in range(maxIter): 

124 xList.append(get_distance_indices( 

125 self.distance_matrix[i], distance)) 

126 

127 return xList 

128 

129 @property 

130 def all_bonds(self): 

131 """All Bonds. 

132 

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

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

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

136 

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

138 """ 

139 if not 'allBonds' in self._cache: 

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

141 

142 return self._cache['allBonds'] 

143 

144 @property 

145 def all_angles(self): 

146 """All angles 

147 

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

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

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

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

152 

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

154 """ 

155 if not 'allAngles' in self._cache: 

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

157 distList = self._get_all_x(2) 

158 

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

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

161 # iterate over second neighbors of all atoms 

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

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

164 if len(secNeighs) == 0: 

165 continue 

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

167 # iterate over second neighbors of iAtom 

168 for kAtom in secNeighs: 

169 relevantFirstNeighs = [ 

170 idx for idx in firstNeighs if kAtom in self.all_bonds[imI][idx]] 

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

172 for jAtom in relevantFirstNeighs: 

173 self._cache['allAngles'][-1][-1].append( 

174 (jAtom, kAtom)) 

175 

176 return self._cache['allAngles'] 

177 

178 @property 

179 def all_dihedrals(self): 

180 """All dihedrals 

181 

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

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

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

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

186 

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

188 """ 

189 if not 'allDihedrals' in self._cache: 

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

191 distList = self._get_all_x(3) 

192 

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

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

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

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

197 if len(thirdNeighs) == 0: 

198 continue 

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

200 # iterate over third neighbors of iAtom 

201 for lAtom in thirdNeighs: 

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

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

204 relevantSecondNeighs = [ 

205 idx for idx in secondNeighs if lAtom in self.all_bonds[imI][idx]] 

206 relevantFirstNeighs = [ 

207 firstNeighs[secondNeighs.index(idx)] for idx in relevantSecondNeighs] 

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

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

210 # remove dihedrals in circles 

211 tupl = (jAtom, kAtom, lAtom) 

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

213 continue 

214 # avoid duplicates 

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

216 continue 

217 elif iAtom in tupl: 

218 raise RuntimeError( 

219 "Something is wrong in analysis.all_dihedrals!") 

220 self._cache['allDihedrals'][-1][-1].append( 

221 (jAtom, kAtom, lAtom)) 

222 

223 return self._cache['allDihedrals'] 

224 

225 @property 

226 def adjacency_matrix(self): 

227 """The adjacency/connectivity matrix. 

228 

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

230 

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

232 """ 

233 

234 if not 'adjacencyMatrix' in self._cache: 

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

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

237 self._cache['adjacencyMatrix'].append( 

238 self.nl[i].get_connectivity_matrix()) 

239 

240 return self._cache['adjacencyMatrix'] 

241 

242 @property 

243 def distance_matrix(self): 

244 """The distance matrix. 

245 

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

247 :meth:`ase.neighborlist.get_distance_matrix`. 

248 

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

250 """ 

251 

252 if not 'distanceMatrix' in self._cache: 

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

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

255 self._cache['distanceMatrix'].append( 

256 get_distance_matrix(self.adjacency_matrix[i])) 

257 

258 return self._cache['distanceMatrix'] 

259 

260 @property 

261 def unique_bonds(self): 

262 """Get Unique Bonds. 

263 

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

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

266 

267 """ 

268 bonds = [] 

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

270 bonds.append([]) 

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

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

273 

274 return bonds 

275 

276 def _filter_unique(self, l): 

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

278 that also contains the reversed items. 

279 """ 

280 r = [] 

281 # iterate over images 

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

283 r.append([]) 

284 # iterate over atoms 

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

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

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

288 return r 

289 

290 def clear_cache(self): 

291 """Delete all cached information.""" 

292 self._cache = {} 

293 

294 @property 

295 def unique_angles(self): 

296 """Get Unique Angles. 

297 

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

299 

300 """ 

301 return self._filter_unique(self.all_angles) 

302 

303 @property 

304 def unique_dihedrals(self): 

305 """Get Unique Dihedrals. 

306 

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

308 

309 """ 

310 return self._filter_unique(self.all_dihedrals) 

311 

312 def _get_symbol_idxs(self, imI, sym): 

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

314 if isinstance(imI, int): 

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

316 else: 

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

318 

319 def _idxTuple2SymbolTuple(self, imI, tup): 

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

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

322 

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

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

325 

326 Parameters: 

327 

328 A, B: str 

329 Get Bonds between elements A and B 

330 unique: bool 

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

332 

333 Returns: 

334 

335 return: list of lists of tuples 

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

337 

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

339 """ 

340 r = [] 

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

342 r.append([]) 

343 aIdxs = self._get_symbol_idxs(imI, A) 

344 if A != B: 

345 bIdxs = self._get_symbol_idxs(imI, B) 

346 for idx in aIdxs: 

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

348 if A == B: 

349 r[-1].extend([(idx, x) 

350 for x in bonded if (x in aIdxs) and (x > idx)]) 

351 else: 

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

353 

354 if not unique: 

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

356 

357 return r 

358 

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

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

361 

362 Parameters: 

363 

364 A, B, C: str 

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

366 unique: bool 

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

368 

369 Returns: 

370 

371 return: list of lists of tuples 

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

373 

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

375 """ 

376 from itertools import combinations, product 

377 r = [] 

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

379 r.append([]) 

380 # Middle Atom is fixed 

381 bIdxs = self._get_symbol_idxs(imI, B) 

382 for bIdx in bIdxs: 

383 bondedA = [idx for idx in self.all_bonds[imI] 

384 [bIdx] if self.images[imI][idx].symbol == A] 

385 if len(bondedA) == 0: 

386 continue 

387 

388 if A != C: 

389 bondedC = [idx for idx in self.all_bonds[imI] 

390 [bIdx] if self.images[imI][idx].symbol == C] 

391 if len(bondedC) == 0: 

392 continue 

393 

394 if A == C: 

395 extend = [(x[0], bIdx, x[1]) 

396 for x in list(combinations(bondedA, 2))] 

397 else: 

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

399 

400 if not unique: 

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

402 

403 r[-1].extend(extend) 

404 return r 

405 

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

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

408 

409 Parameters: 

410 

411 A, B, C, D: str 

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

413 unique: bool 

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

415 

416 Returns: 

417 

418 return: list of lists of tuples 

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

420 

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

422 """ 

423 r = [] 

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

425 r.append([]) 

426 # get indices of elements 

427 aIdxs = self._get_symbol_idxs(imI, A) 

428 bIdxs = self._get_symbol_idxs(imI, B) 

429 cIdxs = self._get_symbol_idxs(imI, C) 

430 dIdxs = self._get_symbol_idxs(imI, D) 

431 for aIdx in aIdxs: 

432 dihedrals = [(aIdx, ) + d for d in self.all_dihedrals[imI][aIdx] 

433 if (d[0] in bIdxs) and (d[1] in cIdxs) and (d[2] in dIdxs)] 

434 if not unique: 

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

436 r[-1].extend(dihedrals) 

437 

438 return r 

439 

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

441 """Get bond length. 

442 

443 Parameters: 

444 

445 imIdx: int 

446 Index of Image to get value from. 

447 idxs: tuple or list of integers 

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

449 mic: bool 

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

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

452 kwargs: options or dict 

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

454 

455 Returns: 

456 

457 return: float 

458 Value returned by image.get_distance. 

459 """ 

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

461 

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

463 """Get angle. 

464 

465 Parameters: 

466 

467 imIdx: int 

468 Index of Image to get value from. 

469 idxs: tuple or list of integers 

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

471 mic: bool 

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

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

474 kwargs: options or dict 

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

476 

477 Returns: 

478 

479 return: float 

480 Value returned by image.get_angle. 

481 """ 

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

483 

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

485 """Get dihedral. 

486 

487 Parameters: 

488 

489 imIdx: int 

490 Index of Image to get value from. 

491 idxs: tuple or list of integers 

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

493 mic: bool 

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

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

496 kwargs: options or dict 

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

498 

499 Returns: 

500 

501 return: float 

502 Value returned by image.get_dihedral. 

503 """ 

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

505 

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

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

508 

509 Parameters: 

510 

511 inputList: list of lists of tuples 

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

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

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

515 imageIdx: integer or slice 

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

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

518 mic: bool 

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

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

521 kwargs: options or dict 

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

523 

524 Returns: 

525 

526 return: list of lists of floats 

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

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

529 

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

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

532 """ 

533 

534 sl = self._get_slice(imageIdx) 

535 

536 # get method to call from length of inputList 

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

538 get = self.get_bond_value 

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

540 get = self.get_angle_value 

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

542 get = self.get_dihedral_value 

543 else: 

544 raise ValueError( 

545 "inputList in ase.geometry.analysis.Analysis.get_values has a bad shape.") 

546 

547 # check if length of slice and inputList match 

548 singleNL = False 

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

550 # only one nl for all images 

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

552 singleNL = True 

553 else: 

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

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

556 

557 r = [] 

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

559 imageIdx = self.images.index(image) 

560 r.append([]) 

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

562 if singleNL: 

563 inputIdx = 0 

564 for tupl in inputList[inputIdx]: 

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

566 

567 return r 

568 

569 def get_max_volume_estimate(self): 

570 return get_max_volume_estimate(self.images) 

571 

572 def get_rdf(self, rmax, nbins, imageIdx=None, elements=None, return_dists=False, 

573 volume: Optional[float] = None): 

574 """Get RDF. 

575 

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

577 

578 Parameters: 

579 

580 rmax: float 

581 Maximum distance of RDF. 

582 nbins: int 

583 Number of bins to divide RDF. 

584 imageIdx: int/slice/None 

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

586 elements: str/int/list/tuple 

587 Make partial RDFs. 

588 

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

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

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

592 

593 Returns: 

594 

595 return: list of lists / list of tuples of lists 

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

597 only rdfs for each image are returned. 

598 """ 

599 

600 sl = self._get_slice(imageIdx) 

601 

602 ls_rdf = [] 

603 el = None 

604 

605 for image in self.images[sl]: 

606 if elements is None: 

607 tmp_image = image 

608 # integers 

609 elif isinstance(elements, int): 

610 tmp_image = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

611 tmp_image.append(image[elements]) 

612 # strings 

613 elif isinstance(elements, str): 

614 tmp_image = Atoms(cell=image.get_cell(), pbc=image.get_pbc()) 

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

616 tmp_image.append(image[idx]) 

617 # lists 

618 elif isinstance(elements, (list, tuple)): 

619 # list of ints 

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

621 if len(elements) == 2: 

622 # use builtin get_rdf mask 

623 el = elements 

624 tmp_image = image 

625 else: 

626 # create dummy image 

627 tmp_image = Atoms( 

628 cell=image.get_cell(), pbc=image.get_pbc()) 

629 for idx in elements: 

630 tmp_image.append(image[idx]) 

631 # list of strings 

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

633 tmp_image = Atoms(cell=image.get_cell(), 

634 pbc=image.get_pbc()) 

635 for element in elements: 

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

637 tmp_image.append(image[idx]) 

638 else: 

639 raise ValueError( 

640 "Unsupported type of elements given in ase.geometry.analysis.Analysis.get_rdf!") 

641 else: 

642 raise ValueError( 

643 "Unsupported type of elements given in ase.geometry.analysis.Analysis.get_rdf!") 

644 

645 rdf = get_rdf(tmp_image, rmax, nbins, elements=el, no_dists=(not return_dists), 

646 volume=volume) 

647 ls_rdf.append(rdf) 

648 

649 return ls_rdf