Coverage for ase / vibrations / data.py: 98.33%

180 statements  

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

1"""Storage and analysis for vibrational data""" 

2 

3import collections 

4from functools import cached_property 

5from math import pi, sin, sqrt 

6from numbers import Integral, Real 

7from typing import Any, Iterator, Sequence, TypeVar 

8 

9import numpy as np 

10 

11import ase.io 

12import ase.units as units 

13from ase.atoms import Atoms 

14from ase.calculators.singlepoint import SinglePointCalculator 

15from ase.constraints import FixAtoms, FixCartesian, constrained_indices 

16from ase.spectrum.doscollection import DOSCollection 

17from ase.spectrum.dosdata import RawDOSData 

18from ase.utils import jsonable 

19 

20RealSequence4D = Sequence[Sequence[Sequence[Sequence[Real]]]] 

21VD = TypeVar('VD', bound='VibrationsData') 

22 

23 

24@jsonable('vibrationsdata') 

25class VibrationsData: 

26 """Class for storing and analyzing vibrational data (i.e. Atoms + Hessian) 

27 

28 This class is not responsible for calculating Hessians; the Hessian should 

29 be computed by a Calculator or some other algorithm. 

30 Once :class:`VibrationsData` has been constructed, this class provides some 

31 common processing options; frequency calculation, mode animation, DOS etc. 

32 

33 If the Atoms object is a periodic supercell, :class:`VibrationsData` may be 

34 converted to a PhononData using the VibrationsData.to_phonondata() method. 

35 This provides access to q-point-dependent analyses such as phonon 

36 dispersion plotting. 

37 

38 If the Atoms object has :class:`~ase.constraints.FixAtoms` or 

39 :class:`~.ase.constraints.FixCartesian` constraints, 

40 these will be respected and the Hessian should be sized accordingly. 

41 

42 Parameters 

43 ---------- 

44 atoms : :class:`~ase.Atoms` 

45 Equilibrium geometry of vibrating system. 

46 This will be stored as a full copy. 

47 

48 hessian : np.ndarray 

49 Second-derivative in energy with respect to Cartesian nuclear movements 

50 as an (N, 3, N, 3) array. 

51 

52 indices : Sequence[int] | np.ndarray | None, default: None 

53 Indices of atoms which are included in Hessian. 

54 By default, it includes all freely moving atoms (i.e. not fixed ones). 

55 Leave at None if constraints should be determined automatically from 

56 the atoms object. 

57 

58 """ 

59 

60 def __init__( 

61 self, 

62 atoms: Atoms, 

63 hessian: RealSequence4D | np.ndarray, 

64 indices: Sequence[int] | np.ndarray | None = None, 

65 ) -> None: 

66 if indices is None: 

67 indices = np.asarray( 

68 self.indices_from_constraints(atoms), dtype=int 

69 ) 

70 

71 self._indices = np.array(indices, dtype=int) 

72 

73 n_atoms = self._check_dimensions( 

74 atoms, np.asarray(hessian), indices=self._indices 

75 ) 

76 self._atoms = atoms.copy() 

77 

78 self._hessian2d = ( 

79 np.asarray(hessian).reshape(3 * n_atoms, 3 * n_atoms).copy() 

80 ) 

81 

82 _setter_error = ( 

83 'VibrationsData properties cannot be modified: construct ' 

84 'a new VibrationsData with consistent atoms, Hessian and ' 

85 '(optionally) indices/mask.' 

86 ) 

87 

88 @classmethod 

89 def from_2d( 

90 cls, 

91 atoms: Atoms, 

92 hessian_2d: Sequence[Sequence[Real]] | np.ndarray, 

93 indices: Sequence[int] | None = None, 

94 ) -> 'VibrationsData': 

95 """Instantiate VibrationsData when the Hessian is in a 3Nx3N format 

96 

97 Parameters 

98 ---------- 

99 atoms : :class:`~ase.Atoms` 

100 Equilibrium geometry of vibrating system. 

101 

102 hessian : Sequence[Sequence[Real]] | np.ndarray 

103 Second-derivative in energy with respect to Cartesian nuclear 

104 movements as a (3N, 3N) array. 

105 

106 indices : Sequence[int] | None, default: None 

107 Indices of (non-frozen) atoms included in Hessian. 

108 

109 """ 

110 if indices is None: 

111 indices = range(len(atoms)) 

112 assert indices is not None # Show Mypy that indices is now a sequence 

113 

114 hessian_2d_array = np.asarray(hessian_2d) 

115 n_atoms = cls._check_dimensions( 

116 atoms, hessian_2d_array, indices=indices, two_d=True 

117 ) 

118 

119 return cls( 

120 atoms, 

121 hessian_2d_array.reshape(n_atoms, 3, n_atoms, 3), 

122 indices=indices, 

123 ) 

124 

125 @staticmethod 

126 def indices_from_constraints(atoms: Atoms) -> list[int]: 

127 """Indices corresponding to Atoms Constraints 

128 

129 Deduces the freely moving atoms from the constraints set on the 

130 atoms object. :class:`VibrationsData` only supports: 

131 

132 - :class:`~ase.constraints.FixCartesian` 

133 - :class:`~ase.constraints.FixAtoms` 

134 

135 All others are neglected. 

136 

137 Parameters 

138 ---------- 

139 atoms : :class:`~ase.Atoms` 

140 

141 Returns 

142 ------- 

143 list[int] 

144 Indices of free atoms. 

145 

146 """ 

147 # Only fully fixed atoms supported by VibrationsData 

148 const_indices = constrained_indices( 

149 atoms, only_include=(FixCartesian, FixAtoms) 

150 ) 

151 # Invert the selection to get free atoms 

152 indices = np.setdiff1d( 

153 np.array(range(len(atoms))), const_indices 

154 ).astype(int) 

155 # TODO: use numpy.typing to resolve this error. 

156 return indices.tolist() # type: ignore[return-value] 

157 

158 @staticmethod 

159 def indices_from_mask( 

160 mask: Sequence[bool] | np.ndarray, 

161 ) -> list[int]: 

162 """Indices corresponding to boolean mask 

163 

164 This is provided as a convenience for instantiating 

165 :class:`VibrationsData` with a boolean mask. 

166 For example, if the Hessian data includes only the H 

167 atoms in a structure:: 

168 

169 h_mask = atoms.get_chemical_symbols() == 'H' 

170 vib_data = VibrationsData(atoms, hessian, 

171 VibrationsData.indices_from_mask(h_mask)) 

172 

173 Take care to ensure that the length of the mask corresponds to the full 

174 number of atoms; this function is only aware of the mask it has been 

175 given. 

176 

177 Parameters 

178 ---------- 

179 mask: Sequence[bool] | np.ndarray 

180 Sequence of boolean values. 

181 

182 Returns 

183 ------- 

184 list[int] 

185 Indices of :obj:`True` elements. 

186 

187 """ 

188 # TODO: use numpy.typing to resolve this error. 

189 return np.where(mask)[0].tolist() # type: ignore[return-value] 

190 

191 @staticmethod 

192 def _check_dimensions( 

193 atoms: Atoms, 

194 hessian: np.ndarray, 

195 indices: np.ndarray | Sequence[int], 

196 two_d: bool = False, 

197 ) -> int: 

198 """Sanity check on array shapes from input data 

199 

200 Parameters 

201 ---------- 

202 atoms : :class:`~ase.Atoms` 

203 hessian : np.ndarray 

204 Proposed Hessian array. 

205 indices : np.ndarray | Sequence[int] 

206 Indices of atoms used in Hessian. 

207 

208 Returns 

209 ------- 

210 Number of atoms contributing to Hessian. 

211 

212 Raises 

213 ------ 

214 ValueError 

215 If Hessian dimensions are not (N, 3, N, 3). 

216 

217 """ 

218 

219 n_atoms = len(atoms[indices]) 

220 

221 if two_d: 

222 ref_shape = [n_atoms * 3, n_atoms * 3] 

223 ref_shape_txt = '{n:d}x{n:d}'.format(n=(n_atoms * 3)) 

224 

225 else: 

226 ref_shape = [n_atoms, 3, n_atoms, 3] 

227 ref_shape_txt = '{n:d}x3x{n:d}x3'.format(n=n_atoms) 

228 

229 if isinstance(hessian, np.ndarray) and hessian.shape == tuple( 

230 ref_shape 

231 ): 

232 return n_atoms 

233 else: 

234 raise ValueError( 

235 'Hessian for these atoms should be a {} numpy array.'.format( 

236 ref_shape_txt 

237 ) 

238 ) 

239 

240 def get_atoms(self) -> Atoms: 

241 return self._atoms.copy() 

242 

243 def get_indices(self) -> np.ndarray: 

244 return self._indices.copy() 

245 

246 def get_mask(self) -> np.ndarray: 

247 """Boolean mask of atoms selected by indices""" 

248 return self._mask_from_indices(self._atoms, self.get_indices()) 

249 

250 @staticmethod 

251 def _mask_from_indices( 

252 atoms: Atoms, 

253 indices: Sequence[int] | np.ndarray | None, 

254 ) -> np.ndarray: 

255 """Boolean mask of atoms selected by indices""" 

256 natoms = len(atoms) 

257 

258 # Wrap indices to allow negative values 

259 indices = np.asarray(indices) % natoms 

260 

261 mask = np.full(natoms, False, dtype=bool) 

262 mask[indices] = True 

263 return mask 

264 

265 def get_hessian(self) -> np.ndarray: 

266 """The Hessian; second derivative of energy wrt positions 

267 

268 This format is preferred for iteration over atoms and when 

269 addressing specific elements of the Hessian. 

270 

271 Returns 

272 ------- 

273 np.ndarray 

274 Array with shape (n_atoms, 3, n_atoms, 3) where 

275 

276 - the first and third indices identify atoms in self.get_atoms() 

277 

278 - the second and fourth indices cover the corresponding 

279 Cartesian movements in x, y, z 

280 

281 e.g. the element h[0, 2, 1, 0] gives a harmonic force exerted on 

282 atoms[1] in the x-direction in response to a movement in the 

283 z-direction of atoms[0] 

284 """ 

285 n_atoms = int(self._hessian2d.shape[0] / 3) 

286 return self._hessian2d.reshape(n_atoms, 3, n_atoms, 3).copy() 

287 

288 def get_hessian_2d(self) -> np.ndarray: 

289 """Get the Hessian as a 2-D array 

290 

291 This format may be preferred for use with standard linear algebra 

292 functions 

293 

294 Returns 

295 ------- 

296 np.ndarray 

297 Array with shape (n_atoms * 3, n_atoms * 3) where the elements are 

298 ordered by atom and Cartesian direction:: 

299 

300 >> [[at1x_at1x, at1x_at1y, at1x_at1z, at1x_at2x, ...], 

301 >> [at1y_at1x, at1y_at1y, at1y_at1z, at1y_at2x, ...], 

302 >> [at1z_at1x, at1z_at1y, at1z_at1z, at1z_at2x, ...], 

303 >> [at2x_at1x, at2x_at1y, at2x_at1z, at2x_at2x, ...], 

304 >> ...] 

305 

306 

307 e.g. the element h[2, 3] gives a harmonic force exerted on 

308 atoms[1] in the x-direction in response to a movement in the 

309 z-direction of atoms[0] 

310 """ 

311 return self._hessian2d.copy() 

312 

313 def todict(self) -> dict[str, Any]: 

314 if np.allclose(self._indices, range(len(self._atoms))): 

315 indices = None 

316 else: 

317 indices = self.get_indices() 

318 

319 return { 

320 'atoms': self.get_atoms(), 

321 'hessian': self.get_hessian(), 

322 'indices': indices, 

323 } 

324 

325 @classmethod 

326 def fromdict(cls, data: dict[str, Any]) -> 'VibrationsData': 

327 # mypy is understandably suspicious of data coming from a dict that 

328 # holds mixed types, but it can see if we sanity-check with 'assert' 

329 assert isinstance(data['atoms'], Atoms) 

330 assert isinstance( 

331 data['hessian'], (collections.abc.Sequence, np.ndarray) 

332 ) 

333 if data['indices'] is not None: 

334 assert isinstance( 

335 data['indices'], (collections.abc.Sequence, np.ndarray) 

336 ) 

337 for index in data['indices']: 

338 assert isinstance(index, Integral) 

339 

340 return cls(data['atoms'], data['hessian'], indices=data['indices']) 

341 

342 @cached_property 

343 def _energies_and_modes(self) -> tuple[np.ndarray, np.ndarray]: 

344 """Diagonalise the Hessian to obtain harmonic modes 

345 

346 This method is an internal implementation of get_energies_and_modes(), 

347 see the docstring of that method for more information. 

348 

349 """ 

350 active_atoms = self._atoms[self.get_mask()] 

351 n_atoms = len(active_atoms) 

352 masses = active_atoms.get_masses() 

353 

354 if not np.all(masses): 

355 raise ValueError( 

356 'Zero mass encountered in one or more of ' 

357 'the vibrated atoms. Use Atoms.set_masses()' 

358 ' to set all masses to non-zero values.' 

359 ) 

360 mass_weights = np.repeat(masses**-0.5, 3) 

361 

362 omega2, vectors = np.linalg.eigh( 

363 mass_weights * self.get_hessian_2d() * mass_weights[:, np.newaxis] 

364 ) 

365 

366 unit_conversion = units._hbar * units.m / sqrt(units._e * units._amu) 

367 energies = unit_conversion * omega2.astype(complex) ** 0.5 

368 

369 modes = vectors.T.reshape(n_atoms * 3, n_atoms, 3) 

370 modes = modes * masses[np.newaxis, :, np.newaxis] ** -0.5 

371 

372 return (energies, modes) 

373 

374 def get_energies_and_modes( 

375 self, 

376 all_atoms: bool = False, 

377 ) -> tuple[np.ndarray, np.ndarray]: 

378 """Diagonalise the Hessian to obtain harmonic modes 

379 

380 Results are cached so diagonalization will only be performed once for 

381 this object instance. 

382 

383 Parameters 

384 ---------- 

385 all_atoms : bool 

386 If True, return modes as (3N, [N + N_frozen], 3) array where 

387 the second axis corresponds to the full list of atoms in the 

388 attached atoms object. Atoms that were not included in the 

389 Hessian will have displacement vectors of (0, 0, 0). 

390 

391 Returns 

392 ------- 

393 tuple[np.ndarray, np.ndarray] 

394 

395 Tuple of (energies, modes). 

396 

397 Energies are given in units of eV. (To convert these to frequencies 

398 in cm-1, divide by ase.units.invcm.) 

399 

400 Modes are given in Cartesian coordinates as a (3N, N, 3) array 

401 where indices correspond to the (mode_index, atom, direction). 

402 

403 """ 

404 

405 energies, modes_from_hessian = self._energies_and_modes 

406 

407 if all_atoms: 

408 n_active_atoms = len(self.get_indices()) 

409 n_all_atoms = len(self._atoms) 

410 modes = np.zeros((3 * n_active_atoms, n_all_atoms, 3)) 

411 modes[:, self.get_mask(), :] = modes_from_hessian 

412 else: 

413 modes = modes_from_hessian.copy() 

414 

415 return (energies.copy(), modes) 

416 

417 def get_modes(self, all_atoms: bool = False) -> np.ndarray: 

418 """Diagonalise the Hessian to obtain harmonic modes 

419 

420 Results are cached so diagonalization will only be performed once for 

421 this object instance. 

422 

423 Parameters 

424 ---------- 

425 all_atoms : bool 

426 If True, return modes as (3N, [N + N_frozen], 3) array where 

427 the second axis corresponds to the full list of atoms in the 

428 attached atoms object. Atoms that were not included in the 

429 Hessian will have displacement vectors of (0, 0, 0). 

430 

431 Returns 

432 ------- 

433 np.ndarray 

434 Modes in Cartesian coordinates as a (3N, N, 3) array where indices 

435 correspond to the (mode_index, atom, direction). 

436 

437 """ 

438 return self.get_energies_and_modes(all_atoms=all_atoms)[1] 

439 

440 def get_energies(self) -> np.ndarray: 

441 """Diagonalise the Hessian to obtain eigenvalues 

442 

443 Results are cached so diagonalization will only be performed once for 

444 this object instance. 

445 

446 Returns 

447 ------- 

448 np.ndarray 

449 Harmonic mode energies in units of eV. 

450 

451 """ 

452 return self.get_energies_and_modes()[0] 

453 

454 def get_frequencies(self) -> np.ndarray: 

455 """Diagonalise the Hessian to obtain frequencies in cm^-1 

456 

457 Results are cached so diagonalization will only be performed once for 

458 this object instance. 

459 

460 Returns 

461 ------- 

462 Harmonic mode frequencies in units of cm^-1. 

463 

464 """ 

465 

466 return self.get_energies() / units.invcm 

467 

468 def get_zero_point_energy(self) -> float: 

469 """Diagonalise the Hessian and sum hw/2 to obtain zero-point energy 

470 

471 Returns 

472 ------- 

473 float 

474 Zero-point energy in eV. 

475 

476 """ 

477 return self._calculate_zero_point_energy(self.get_energies()) 

478 

479 @staticmethod 

480 def _calculate_zero_point_energy( 

481 energies: Sequence[complex] | np.ndarray, 

482 ) -> float: 

483 return 0.5 * np.asarray(energies).real.sum() 

484 

485 def tabulate(self, im_tol: float = 1e-8) -> str: 

486 """Get a summary of the vibrational frequencies. 

487 

488 Parameters 

489 ---------- 

490 im_tol : float 

491 Tolerance for imaginary frequency in eV. 

492 If frequency has a larger imaginary component than im_tol, 

493 the imaginary component is shown in the summary table. 

494 

495 Returns 

496 ------- 

497 str 

498 Summary table as formatted text. 

499 

500 """ 

501 

502 energies = self.get_energies() 

503 

504 return ( 

505 '\n'.join(self._tabulate_from_energies(energies, im_tol=im_tol)) 

506 + '\n' 

507 ) 

508 

509 @classmethod 

510 def _tabulate_from_energies( 

511 cls, 

512 energies: Sequence[complex] | np.ndarray, 

513 im_tol: float = 1e-8, 

514 ) -> list[str]: 

515 summary_lines = [ 

516 '---------------------', 

517 ' # meV cm^-1', 

518 '---------------------', 

519 ] 

520 

521 for n, e in enumerate(energies): 

522 if abs(e.imag) > im_tol: 

523 c = 'i' 

524 e = e.imag 

525 else: 

526 c = '' 

527 e = e.real 

528 

529 summary_lines.append( 

530 '{index:3d} {mev:6.1f}{im:1s} {cm:7.1f}{im}'.format( 

531 index=n, mev=(e * 1e3), cm=(e / units.invcm), im=c 

532 ) 

533 ) 

534 summary_lines.append('---------------------') 

535 summary_lines.append( 

536 'Zero-point energy: {:.3f} eV'.format( 

537 cls._calculate_zero_point_energy(energies=energies) 

538 ) 

539 ) 

540 

541 return summary_lines 

542 

543 def iter_animated_mode( 

544 self, 

545 mode_index: int, 

546 temperature: float = units.kB * 300.0, 

547 frames: int = 30, 

548 ) -> Iterator[Atoms]: 

549 """Obtain animated mode as a series of Atoms 

550 

551 Parameters 

552 ---------- 

553 mode_index : int 

554 Selection of mode to animate. 

555 temperature : float 

556 Temperature in energy units - use ``units.kB * T_IN_KELVIN`` 

557 frames : int 

558 Number of image frames in animation. 

559 

560 Yields 

561 ------ 

562 :class:`~ase.Atoms` 

563 Displaced atoms following vibrational mode. 

564 

565 """ 

566 

567 mode = self.get_modes(all_atoms=True)[mode_index] * sqrt( 

568 temperature / abs(self.get_energies()[mode_index]) 

569 ) 

570 

571 for phase in np.linspace(0, 2 * pi, frames, endpoint=False): 

572 atoms = self.get_atoms() 

573 atoms.positions += sin(phase) * mode 

574 

575 yield atoms 

576 

577 def show_as_force( 

578 self, 

579 mode: int, 

580 scale: float = 0.2, 

581 show: bool = True, 

582 ) -> Atoms: 

583 """Illustrate mode as "forces" on atoms 

584 

585 Parameters 

586 ---------- 

587 mode : int 

588 Mode index. 

589 scale : float 

590 Scale factor. 

591 show : bool 

592 If True, open the ASE GUI and show atoms. 

593 

594 Returns 

595 ------- 

596 :class:`~ase.Atoms` 

597 Atoms with scaled forces corresponding to mode eigenvectors (using 

598 attached SinglePointCalculator). 

599 

600 """ 

601 

602 atoms = self.get_atoms() 

603 mode = self.get_modes(all_atoms=True)[mode] * len(atoms) * 3 * scale 

604 atoms.calc = SinglePointCalculator(atoms, forces=mode) 

605 if show: 

606 atoms.edit() 

607 

608 return atoms 

609 

610 def write_jmol( 

611 self, 

612 filename: str = 'vib.xyz', 

613 ir_intensities: Sequence[float] | np.ndarray | None = None, 

614 ) -> None: 

615 """Writes file for viewing of the modes with jmol. 

616 

617 This is an extended XYZ file with eigenvectors given as extra columns 

618 and metadata given in the label/comment line for each image. The format 

619 is not quite human-friendly, but has the advantage that it can be 

620 imported back into ASE with ase.io.read. 

621 

622 Parameters 

623 ---------- 

624 filename : str 

625 Path for output file. 

626 ir_intensities : 

627 If available, IR intensities can be included in the header lines. 

628 This does not affect the visualisation but may be convenient when 

629 comparing to experimental data. 

630 

631 """ 

632 

633 all_images = list( 

634 self._get_jmol_images( 

635 atoms=self.get_atoms(), 

636 energies=self.get_energies(), 

637 modes=self.get_modes(all_atoms=True), 

638 ir_intensities=ir_intensities, 

639 ) 

640 ) 

641 ase.io.write(filename, all_images, format='extxyz') 

642 

643 @staticmethod 

644 def _get_jmol_images( 

645 atoms: Atoms, 

646 energies: np.ndarray, 

647 modes: np.ndarray, 

648 ir_intensities: Sequence[float] | np.ndarray | None = None, 

649 ) -> Iterator[Atoms]: 

650 """Get vibrational modes as a series of Atoms with attached data 

651 

652 For each image (Atoms object): 

653 

654 - eigenvalues are attached to image.arrays['mode'] 

655 - "mode#" and "frequency_cm-1" are set in image.info 

656 - "IR_intensity" is set if provided in ir_intensities 

657 - "masses" is removed 

658 

659 This is intended to set up the object for JMOL-compatible export using 

660 ase.io.extxyz. 

661 

662 Parameters 

663 ---------- 

664 atoms : :class:`~ase.Atoms` 

665 The base atoms object; all images have the same positions 

666 energies : np.ndarray 

667 Complex vibrational energies in eV. 

668 modes : np.ndarray 

669 Eigenvectors array corresponding to atoms and energies. 

670 This should cover the full set of atoms (i.e. modes = 

671 vib.get_modes(all_atoms=True)). 

672 ir_intensities: Sequence[float] | np.ndarray | None = None 

673 If available, IR intensities can be included in the header lines. 

674 This does not affect the visualisation but may be convenient when 

675 comparing to experimental data. 

676 

677 Returns 

678 ------- 

679 Iterator[Atoms] 

680 

681 """ 

682 for i, (energy, mode) in enumerate(zip(energies, modes)): 

683 # write imaginary frequencies as negative numbers 

684 if energy.imag > energy.real: 

685 energy = float(-energy.imag) 

686 else: 

687 energy = energy.real 

688 

689 image = atoms.copy() 

690 image.info.update( 

691 { 

692 'mode#': str(i), 

693 'frequency_cm-1': energy / units.invcm, 

694 } 

695 ) 

696 image.arrays['mode'] = mode 

697 

698 # Custom masses are quite useful in vibration analysis, but will 

699 # show up in the xyz file unless we remove them 

700 if image.has('masses'): 

701 del image.arrays['masses'] 

702 

703 if ir_intensities is not None: 

704 image.info['IR_intensity'] = float(ir_intensities[i]) 

705 

706 yield image 

707 

708 def get_dos(self) -> RawDOSData: 

709 """Total phonon DOS""" 

710 energies = self.get_energies() 

711 return RawDOSData(energies, np.ones_like(energies)) 

712 

713 def get_pdos(self) -> DOSCollection: 

714 """Phonon DOS, including atomic contributions""" 

715 energies = self.get_energies() 

716 masses = self._atoms[self.get_mask()].get_masses() 

717 

718 # Get weights as N_moving_atoms x N_modes array 

719 vectors = self.get_modes() / masses[np.newaxis, :, np.newaxis] ** -0.5 

720 all_weights = (np.linalg.norm(vectors, axis=-1) ** 2).T 

721 

722 mask = self.get_mask() 

723 all_info = [ 

724 {'index': i, 'symbol': a.symbol} 

725 for i, a in enumerate(self._atoms) 

726 if mask[i] 

727 ] 

728 

729 return DOSCollection( 

730 [ 

731 RawDOSData(energies, weights, info=info) 

732 for weights, info in zip(all_weights, all_info) 

733 ] 

734 ) 

735 

736 def with_new_masses( 

737 self: VD, 

738 masses: Sequence[float] | np.ndarray, 

739 ) -> VD: 

740 """Get a copy of vibrations with modified masses and the same Hessian 

741 

742 Parameters 

743 ---------- 

744 masses: Sequence[float] | np.ndarray 

745 New sequence of masses corresponding to the atom order in 

746 ``self.get_atoms()``. 

747 

748 Returns 

749 ------- 

750 :class:`VibrationalData` 

751 A copy of the data with new masses for the same Hessian. 

752 

753 """ 

754 

755 new_atoms = self.get_atoms() 

756 new_atoms.set_masses(masses) 

757 return self.__class__( 

758 new_atoms, self.get_hessian(), indices=self.get_indices() 

759 )