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

181 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 15:52 +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 unconstrained_indices = self.indices_from_constraints(self._atoms) 

315 if ( 

316 len(self._indices) == len(unconstrained_indices) 

317 and (self._indices == unconstrained_indices).all() 

318 ): 

319 indices = None 

320 else: 

321 indices = self.get_indices() 

322 

323 return { 

324 'atoms': self.get_atoms(), 

325 'hessian': self.get_hessian(), 

326 'indices': indices, 

327 } 

328 

329 @classmethod 

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

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

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

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

334 assert isinstance( 

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

336 ) 

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

338 assert isinstance( 

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

340 ) 

341 for index in data['indices']: 

342 assert isinstance(index, Integral) 

343 

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

345 

346 @cached_property 

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

348 """Diagonalise the Hessian to obtain harmonic modes 

349 

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

351 see the docstring of that method for more information. 

352 

353 """ 

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

355 n_atoms = len(active_atoms) 

356 masses = active_atoms.get_masses() 

357 

358 if not np.all(masses): 

359 raise ValueError( 

360 'Zero mass encountered in one or more of ' 

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

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

363 ) 

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

365 

366 omega2, vectors = np.linalg.eigh( 

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

368 ) 

369 

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

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

372 

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

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

375 

376 return (energies, modes) 

377 

378 def get_energies_and_modes( 

379 self, 

380 all_atoms: bool = False, 

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

382 """Diagonalise the Hessian to obtain harmonic modes 

383 

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

385 this object instance. 

386 

387 Parameters 

388 ---------- 

389 all_atoms : bool 

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

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

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

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

394 

395 Returns 

396 ------- 

397 tuple[np.ndarray, np.ndarray] 

398 

399 Tuple of (energies, modes). 

400 

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

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

403 

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

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

406 

407 """ 

408 

409 energies, modes_from_hessian = self._energies_and_modes 

410 

411 if all_atoms: 

412 n_active_atoms = len(self.get_indices()) 

413 n_all_atoms = len(self._atoms) 

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

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

416 else: 

417 modes = modes_from_hessian.copy() 

418 

419 return (energies.copy(), modes) 

420 

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

422 """Diagonalise the Hessian to obtain harmonic modes 

423 

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

425 this object instance. 

426 

427 Parameters 

428 ---------- 

429 all_atoms : bool 

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

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

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

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

434 

435 Returns 

436 ------- 

437 np.ndarray 

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

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

440 

441 """ 

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

443 

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

445 """Diagonalise the Hessian to obtain eigenvalues 

446 

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

448 this object instance. 

449 

450 Returns 

451 ------- 

452 np.ndarray 

453 Harmonic mode energies in units of eV. 

454 

455 """ 

456 return self.get_energies_and_modes()[0] 

457 

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

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

460 

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

462 this object instance. 

463 

464 Returns 

465 ------- 

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

467 

468 """ 

469 

470 return self.get_energies() / units.invcm 

471 

472 def get_zero_point_energy(self) -> float: 

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

474 

475 Returns 

476 ------- 

477 float 

478 Zero-point energy in eV. 

479 

480 """ 

481 return self._calculate_zero_point_energy(self.get_energies()) 

482 

483 @staticmethod 

484 def _calculate_zero_point_energy( 

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

486 ) -> float: 

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

488 

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

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

491 

492 Parameters 

493 ---------- 

494 im_tol : float 

495 Tolerance for imaginary frequency in eV. 

496 If frequency has a larger imaginary component than im_tol, 

497 the imaginary component is shown in the summary table. 

498 

499 Returns 

500 ------- 

501 str 

502 Summary table as formatted text. 

503 

504 """ 

505 

506 energies = self.get_energies() 

507 

508 return ( 

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

510 + '\n' 

511 ) 

512 

513 @classmethod 

514 def _tabulate_from_energies( 

515 cls, 

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

517 im_tol: float = 1e-8, 

518 ) -> list[str]: 

519 summary_lines = [ 

520 '---------------------', 

521 ' # meV cm^-1', 

522 '---------------------', 

523 ] 

524 

525 for n, e in enumerate(energies): 

526 if abs(e.imag) > im_tol: 

527 c = 'i' 

528 e = e.imag 

529 else: 

530 c = '' 

531 e = e.real 

532 

533 summary_lines.append( 

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

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

536 ) 

537 ) 

538 summary_lines.append('---------------------') 

539 summary_lines.append( 

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

541 cls._calculate_zero_point_energy(energies=energies) 

542 ) 

543 ) 

544 

545 return summary_lines 

546 

547 def iter_animated_mode( 

548 self, 

549 mode_index: int, 

550 temperature: float = units.kB * 300.0, 

551 frames: int = 30, 

552 ) -> Iterator[Atoms]: 

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

554 

555 Parameters 

556 ---------- 

557 mode_index : int 

558 Selection of mode to animate. 

559 temperature : float 

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

561 frames : int 

562 Number of image frames in animation. 

563 

564 Yields 

565 ------ 

566 :class:`~ase.Atoms` 

567 Displaced atoms following vibrational mode. 

568 

569 """ 

570 

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

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

573 ) 

574 

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

576 atoms = self.get_atoms() 

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

578 

579 yield atoms 

580 

581 def show_as_force( 

582 self, 

583 mode: int, 

584 scale: float = 0.2, 

585 show: bool = True, 

586 ) -> Atoms: 

587 """Illustrate mode as "forces" on atoms 

588 

589 Parameters 

590 ---------- 

591 mode : int 

592 Mode index. 

593 scale : float 

594 Scale factor. 

595 show : bool 

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

597 

598 Returns 

599 ------- 

600 :class:`~ase.Atoms` 

601 Atoms with scaled forces corresponding to mode eigenvectors (using 

602 attached SinglePointCalculator). 

603 

604 """ 

605 

606 atoms = self.get_atoms() 

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

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

609 if show: 

610 atoms.edit() 

611 

612 return atoms 

613 

614 def write_jmol( 

615 self, 

616 filename: str = 'vib.xyz', 

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

618 ) -> None: 

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

620 

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

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

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

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

625 

626 Parameters 

627 ---------- 

628 filename : str 

629 Path for output file. 

630 ir_intensities : 

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

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

633 comparing to experimental data. 

634 

635 """ 

636 

637 all_images = list( 

638 self._get_jmol_images( 

639 atoms=self.get_atoms(), 

640 energies=self.get_energies(), 

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

642 ir_intensities=ir_intensities, 

643 ) 

644 ) 

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

646 

647 @staticmethod 

648 def _get_jmol_images( 

649 atoms: Atoms, 

650 energies: np.ndarray, 

651 modes: np.ndarray, 

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

653 ) -> Iterator[Atoms]: 

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

655 

656 For each image (Atoms object): 

657 

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

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

660 - "IR_intensity" is set if provided in ir_intensities 

661 - "masses" is removed 

662 

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

664 ase.io.extxyz. 

665 

666 Parameters 

667 ---------- 

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

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

670 energies : np.ndarray 

671 Complex vibrational energies in eV. 

672 modes : np.ndarray 

673 Eigenvectors array corresponding to atoms and energies. 

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

675 vib.get_modes(all_atoms=True)). 

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

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

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

679 comparing to experimental data. 

680 

681 Returns 

682 ------- 

683 Iterator[Atoms] 

684 

685 """ 

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

687 # write imaginary frequencies as negative numbers 

688 if energy.imag > energy.real: 

689 energy = float(-energy.imag) 

690 else: 

691 energy = energy.real 

692 

693 image = atoms.copy() 

694 image.info.update( 

695 { 

696 'mode#': str(i), 

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

698 } 

699 ) 

700 image.arrays['mode'] = mode 

701 

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

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

704 if image.has('masses'): 

705 del image.arrays['masses'] 

706 

707 if ir_intensities is not None: 

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

709 

710 yield image 

711 

712 def get_dos(self) -> RawDOSData: 

713 """Total phonon DOS""" 

714 energies = self.get_energies() 

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

716 

717 def get_pdos(self) -> DOSCollection: 

718 """Phonon DOS, including atomic contributions""" 

719 energies = self.get_energies() 

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

721 

722 # Get weights as N_moving_atoms x N_modes array 

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

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

725 

726 mask = self.get_mask() 

727 all_info = [ 

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

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

730 if mask[i] 

731 ] 

732 

733 return DOSCollection( 

734 [ 

735 RawDOSData(energies, weights, info=info) 

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

737 ] 

738 ) 

739 

740 def with_new_masses( 

741 self: VD, 

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

743 ) -> VD: 

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

745 

746 Parameters 

747 ---------- 

748 masses: Sequence[float] | np.ndarray 

749 New sequence of masses corresponding to the atom order in 

750 ``self.get_atoms()``. 

751 

752 Returns 

753 ------- 

754 :class:`VibrationalData` 

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

756 

757 """ 

758 

759 new_atoms = self.get_atoms() 

760 new_atoms.set_masses(masses) 

761 return self.__class__( 

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

763 )