Coverage for /builds/ase/ase/ase/vibrations/resonant_raman.py: 65.27%

311 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""Resonant Raman intensities""" 

4 

5import sys 

6from pathlib import Path 

7 

8import numpy as np 

9 

10import ase.units as u 

11from ase.parallel import paropen, parprint, world 

12from ase.vibrations import Vibrations 

13from ase.vibrations.raman import Raman, RamanCalculatorBase 

14 

15 

16class ResonantRamanCalculator(RamanCalculatorBase, Vibrations): 

17 """Base class for resonant Raman calculators using finite differences. 

18 """ 

19 

20 def __init__(self, atoms, ExcitationsCalculator, *args, 

21 exkwargs=None, exext='.ex.gz', overlap=False, 

22 **kwargs): 

23 """ 

24 Parameters 

25 ---------- 

26 atoms: Atoms 

27 The Atoms object 

28 ExcitationsCalculator: object 

29 Calculator for excited states 

30 exkwargs: dict 

31 Arguments given to the ExcitationsCalculator object 

32 exext: string 

33 Extension for filenames of Excitation lists (results of 

34 the ExcitationsCalculator). 

35 overlap : function or False 

36 Function to calculate overlaps between excitation at 

37 equilibrium and at a displaced position. Calculators are 

38 given as first and second argument, respectively. 

39 

40 Example 

41 ------- 

42 

43 >>> from ase.calculators.h2morse import (H2Morse, 

44 ... H2MorseExcitedStatesCalculator) 

45 >>> from ase.vibrations.resonant_raman import ResonantRamanCalculator 

46 >>> 

47 >>> atoms = H2Morse() 

48 >>> rmc = ResonantRamanCalculator(atoms, H2MorseExcitedStatesCalculator) 

49 >>> rmc.run() 

50 

51 This produces all necessary data for further analysis. 

52 """ 

53 self.exobj = ExcitationsCalculator 

54 if exkwargs is None: 

55 exkwargs = {} 

56 self.exkwargs = exkwargs 

57 self.overlap = overlap 

58 

59 super().__init__(atoms, *args, exext=exext, **kwargs) 

60 

61 def _new_exobj(self): 

62 # XXXX I have to duplicate this because there are two objects 

63 # which have exkwargs, why are they not unified? 

64 return self.exobj(**self.exkwargs) 

65 

66 def calculate(self, atoms, disp): 

67 """Call ground and excited state calculation""" 

68 assert atoms == self.atoms # XXX action required 

69 forces = self.atoms.get_forces() 

70 

71 if self.overlap: 

72 """Overlap is determined as 

73 

74 ov_ij = int dr displaced*_i(r) eqilibrium_j(r) 

75 """ 

76 ov_nn = self.overlap(self.atoms.calc, 

77 self.eq_calculator) 

78 if world.rank == 0: 

79 disp.save_ov_nn(ov_nn) 

80 

81 disp.calculate_and_save_exlist(atoms) 

82 return {'forces': forces} 

83 

84 def run(self): 

85 if self.overlap: 

86 # XXXX stupid way to make a copy 

87 self.atoms.get_potential_energy() 

88 self.eq_calculator = self.atoms.calc 

89 Path(self.name).mkdir(parents=True, exist_ok=True) 

90 fname = Path(self.name) / 'tmp.gpw' 

91 self.eq_calculator.write(fname, 'all') 

92 self.eq_calculator = self.eq_calculator.__class__(restart=fname) 

93 try: 

94 # XXX GPAW specific 

95 self.eq_calculator.converge_wave_functions() 

96 except AttributeError: 

97 pass 

98 Vibrations.run(self) 

99 

100 

101class ResonantRaman(Raman): 

102 """Base Class for resonant Raman intensities using finite differences. 

103 """ 

104 

105 def __init__(self, atoms, Excitations, *args, 

106 observation=None, 

107 form='v', # form of the dipole operator 

108 exkwargs=None, # kwargs to be passed to Excitations 

109 exext='.ex.gz', # extension for Excitation names 

110 overlap=False, 

111 minoverlap=0.02, 

112 minrep=0.8, 

113 comm=world, 

114 **kwargs): 

115 """ 

116 Parameters 

117 ---------- 

118 atoms: ase Atoms object 

119 Excitations: class 

120 Type of the excitation list object. The class object is 

121 initialized as:: 

122 

123 Excitations(atoms.calc) 

124 

125 or by reading form a file as:: 

126 

127 Excitations('filename', **exkwargs) 

128 

129 The file is written by calling the method 

130 Excitations.write('filename'). 

131 

132 Excitations should work like a list of ex obejects, where: 

133 ex.get_dipole_me(form='v'): 

134 gives the velocity form dipole matrix element in 

135 units |e| * Angstrom 

136 ex.energy: 

137 is the transition energy in Hartrees 

138 approximation: string 

139 Level of approximation used. 

140 observation: dict 

141 Polarization settings 

142 form: string 

143 Form of the dipole operator, 'v' for velocity form (default) 

144 and 'r' for length form. 

145 overlap: bool or function 

146 Use wavefunction overlaps. 

147 minoverlap: float ord dict 

148 Minimal absolute overlap to consider. Defaults to 0.02 to avoid 

149 numerical garbage. 

150 minrep: float 

151 Minimal representation to consider derivative, defaults to 0.8 

152 """ 

153 

154 if observation is None: 

155 observation = {'geometry': '-Z(XX)Z'} 

156 

157 kwargs['exext'] = exext 

158 Raman.__init__(self, atoms, *args, **kwargs) 

159 assert self.vibrations.nfree == 2 

160 

161 self.exobj = Excitations 

162 if exkwargs is None: 

163 exkwargs = {} 

164 self.exkwargs = exkwargs 

165 self.observation = observation 

166 self.dipole_form = form 

167 

168 self.overlap = overlap 

169 if not isinstance(minoverlap, dict): 

170 # assume it's a number 

171 self.minoverlap = {'orbitals': minoverlap, 

172 'excitations': minoverlap} 

173 else: 

174 self.minoverlap = minoverlap 

175 self.minrep = minrep 

176 

177 def read_exobj(self, filename): 

178 return self.exobj.read(filename, **self.exkwargs) 

179 

180 def get_absolute_intensities(self, omega, gamma=0.1, delta=0, **kwargs): 

181 """Absolute Raman intensity or Raman scattering factor 

182 

183 Parameter 

184 --------- 

185 omega: float 

186 incoming laser energy, unit eV 

187 gamma: float 

188 width (imaginary energy), unit eV 

189 delta: float 

190 pre-factor for asymmetric anisotropy, default 0 

191 

192 References 

193 ---------- 

194 Porezag and Pederson, PRB 54 (1996) 7830-7836 (delta=0) 

195 Baiardi and Barone, JCTC 11 (2015) 3267-3280 (delta=5) 

196 

197 Returns 

198 ------- 

199 raman intensity, unit Ang**4/amu 

200 """ 

201 alpha2_r, gamma2_r, delta2_r = self._invariants( 

202 self.electronic_me_Qcc(omega, gamma, **kwargs)) 

203 return 45 * alpha2_r + delta * delta2_r + 7 * gamma2_r 

204 

205 @property 

206 def approximation(self): 

207 return self._approx 

208 

209 @approximation.setter 

210 def approximation(self, value): 

211 self.set_approximation(value) 

212 

213 def read_excitations(self): 

214 """Read all finite difference excitations and select matching.""" 

215 if self.overlap: 

216 return self.read_excitations_overlap() 

217 

218 disp = self._eq_disp() 

219 ex0_object = disp.read_exobj() 

220 eu = ex0_object.energy_to_eV_scale 

221 matching = frozenset(ex0_object) 

222 

223 def append(lst, disp, matching): 

224 exo = disp.read_exobj() 

225 lst.append(exo) 

226 matching = matching.intersection(exo) 

227 return matching 

228 

229 exm_object_list = [] 

230 exp_object_list = [] 

231 for a, i in zip(self.myindices, self.myxyz): 

232 mdisp = self._disp(a, i, -1) 

233 pdisp = self._disp(a, i, 1) 

234 matching = append(exm_object_list, 

235 mdisp, matching) 

236 matching = append(exp_object_list, 

237 pdisp, matching) 

238 

239 def select(exl, matching): 

240 mlst = [ex for ex in exl if ex in matching] 

241 assert len(mlst) == len(matching) 

242 return mlst 

243 

244 ex0 = select(ex0_object, matching) 

245 exm = [] 

246 exp = [] 

247 r = 0 

248 for a, i in zip(self.myindices, self.myxyz): 

249 exm.append(select(exm_object_list[r], matching)) 

250 exp.append(select(exp_object_list[r], matching)) 

251 r += 1 

252 

253 self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) 

254 self.ex0m_pc = (np.array( 

255 [ex.get_dipole_me(form=self.dipole_form) for ex in ex0]) * 

256 u.Bohr) 

257 exmE_rp = [] 

258 expE_rp = [] 

259 exF_rp = [] 

260 exmm_rpc = [] 

261 expm_rpc = [] 

262 r = 0 

263 for a, i in zip(self.myindices, self.myxyz): 

264 exmE_rp.append([em.energy for em in exm[r]]) 

265 expE_rp.append([ep.energy for ep in exp[r]]) 

266 exF_rp.append( 

267 [(em.energy - ep.energy) 

268 for ep, em in zip(exp[r], exm[r])]) 

269 exmm_rpc.append( 

270 [ex.get_dipole_me(form=self.dipole_form) 

271 for ex in exm[r]]) 

272 expm_rpc.append( 

273 [ex.get_dipole_me(form=self.dipole_form) 

274 for ex in exp[r]]) 

275 r += 1 

276 # indicees: r=coordinate, p=excitation 

277 # energies in eV 

278 self.exmE_rp = np.array(exmE_rp) * eu 

279 self.expE_rp = np.array(expE_rp) * eu 

280 # forces in eV / Angstrom 

281 self.exF_rp = np.array(exF_rp) * eu / 2 / self.delta 

282 # matrix elements in e * Angstrom 

283 self.exmm_rpc = np.array(exmm_rpc) * u.Bohr 

284 self.expm_rpc = np.array(expm_rpc) * u.Bohr 

285 

286 def read_excitations_overlap(self): 

287 """Read all finite difference excitations and wf overlaps. 

288 

289 We assume that the wave function overlaps are determined as 

290 

291 ov_ij = int dr displaced*_i(r) eqilibrium_j(r) 

292 """ 

293 ex0 = self._eq_disp().read_exobj() 

294 eu = ex0.energy_to_eV_scale 

295 rep0_p = np.ones((len(ex0)), dtype=float) 

296 

297 def load(disp, rep0_p): 

298 ex_p = disp.read_exobj() 

299 ov_nn = disp.load_ov_nn() 

300 # remove numerical garbage 

301 ov_nn = np.where(np.abs(ov_nn) > self.minoverlap['orbitals'], 

302 ov_nn, 0) 

303 ov_pp = ex_p.overlap(ov_nn, ex0) 

304 ov_pp = np.where(np.abs(ov_pp) > self.minoverlap['excitations'], 

305 ov_pp, 0) 

306 rep0_p *= (ov_pp.real**2 + ov_pp.imag**2).sum(axis=0) 

307 return ex_p, ov_pp 

308 

309 def rotate(ex_p, ov_pp): 

310 e_p = np.array([ex.energy for ex in ex_p]) 

311 m_pc = np.array( 

312 [ex.get_dipole_me(form=self.dipole_form) for ex in ex_p]) 

313 r_pp = ov_pp.T 

314 return ((r_pp.real**2 + r_pp.imag**2).dot(e_p), 

315 r_pp.dot(m_pc)) 

316 

317 exmE_rp = [] 

318 expE_rp = [] 

319 exF_rp = [] 

320 exmm_rpc = [] 

321 expm_rpc = [] 

322 exdmdr_rpc = [] 

323 for a, i in zip(self.myindices, self.myxyz): 

324 mdisp = self._disp(a, i, -1) 

325 pdisp = self._disp(a, i, 1) 

326 ex, ov = load(mdisp, rep0_p) 

327 exmE_p, exmm_pc = rotate(ex, ov) 

328 ex, ov = load(pdisp, rep0_p) 

329 expE_p, expm_pc = rotate(ex, ov) 

330 exmE_rp.append(exmE_p) 

331 expE_rp.append(expE_p) 

332 exF_rp.append(exmE_p - expE_p) 

333 exmm_rpc.append(exmm_pc) 

334 expm_rpc.append(expm_pc) 

335 exdmdr_rpc.append(expm_pc - exmm_pc) 

336 

337 # select only excitations that are sufficiently represented 

338 self.comm.product(rep0_p) 

339 select = np.where(rep0_p > self.minrep)[0] 

340 

341 self.ex0E_p = np.array([ex.energy * eu for ex in ex0])[select] 

342 self.ex0m_pc = (np.array( 

343 [ex.get_dipole_me(form=self.dipole_form) 

344 for ex in ex0])[select] * u.Bohr) 

345 

346 if len(self.myr): 

347 # indicees: r=coordinate, p=excitation 

348 # energies in eV 

349 self.exmE_rp = np.array(exmE_rp)[:, select] * eu 

350 self.expE_rp = np.array(expE_rp)[:, select] * eu 

351 # forces in eV / Angstrom 

352 self.exF_rp = np.array(exF_rp)[:, select] * eu / 2 / self.delta 

353 # matrix elements in e * Angstrom 

354 self.exmm_rpc = np.array(exmm_rpc)[:, select, :] * u.Bohr 

355 self.expm_rpc = np.array(expm_rpc)[:, select, :] * u.Bohr 

356 # matrix element derivatives in e 

357 self.exdmdr_rpc = (np.array(exdmdr_rpc)[:, select, :] * 

358 u.Bohr / 2 / self.delta) 

359 else: 

360 # did not read 

361 self.exmE_rp = self.expE_rp = self.exF_rp = np.empty(0) 

362 self.exmm_rpc = self.expm_rpc = self.exdmdr_rpc = np.empty(0) 

363 

364 def read(self, *args, **kwargs): 

365 """Read data from a pre-performed calculation.""" 

366 self.vibrations.read(*args, **kwargs) 

367 self.init_parallel_read() 

368 if not hasattr(self, 'ex0E_p'): 

369 if self.overlap: 

370 self.read_excitations_overlap() 

371 else: 

372 self.read_excitations() 

373 

374 self._already_read = True 

375 

376 def get_cross_sections(self, omega, gamma): 

377 """Returns Raman cross sections for each vibration.""" 

378 I_v = self.intensity(omega, gamma) 

379 pre = 1. / 16 / np.pi**2 / u._eps0**2 / u._c**4 

380 # frequency of scattered light 

381 omS_v = omega - self.om_Q 

382 return pre * omega * omS_v**3 * I_v 

383 

384 def get_spectrum(self, omega, gamma=0.1, 

385 start=None, end=None, npts=None, width=20, 

386 type='Gaussian', 

387 intensity_unit='????', normalize=False): 

388 """Get resonant Raman spectrum. 

389 

390 The method returns wavenumbers in cm^-1 with corresponding 

391 Raman cross section. 

392 Start and end point, and width of the Gaussian/Lorentzian should 

393 be given in cm^-1. 

394 """ 

395 

396 self.type = type.lower() 

397 assert self.type in ['gaussian', 'lorentzian'] 

398 

399 frequencies = self.get_energies().real / u.invcm 

400 intensities = self.get_cross_sections(omega, gamma) 

401 if width is None: 

402 return [frequencies, intensities] 

403 

404 if start is None: 

405 start = min(self.om_Q) / u.invcm - 3 * width 

406 if end is None: 

407 end = max(self.om_Q) / u.invcm + 3 * width 

408 

409 if not npts: 

410 npts = int((end - start) / width * 10 + 1) 

411 

412 prefactor = 1 

413 if self.type == 'lorentzian': 

414 intensities = intensities * width * np.pi / 2. 

415 if normalize: 

416 prefactor = 2. / width / np.pi 

417 else: 

418 sigma = width / 2. / np.sqrt(2. * np.log(2.)) 

419 if normalize: 

420 prefactor = 1. / sigma / np.sqrt(2 * np.pi) 

421 # Make array with spectrum data 

422 spectrum = np.empty(npts) 

423 energies = np.linspace(start, end, npts) 

424 for i, energy in enumerate(energies): 

425 energies[i] = energy 

426 if self.type == 'lorentzian': 

427 spectrum[i] = (intensities * 0.5 * width / np.pi / 

428 ((frequencies - energy)**2 + 

429 0.25 * width**2)).sum() 

430 else: 

431 spectrum[i] = (intensities * 

432 np.exp(-(frequencies - energy)**2 / 

433 2. / sigma**2)).sum() 

434 return [energies, prefactor * spectrum] 

435 

436 def write_spectrum(self, omega, gamma, 

437 out='resonant-raman-spectra.dat', 

438 start=200, end=4000, 

439 npts=None, width=10, 

440 type='Gaussian'): 

441 """Write out spectrum to file. 

442 

443 Start and end 

444 point, and width of the Gaussian/Lorentzian should be given 

445 in cm^-1.""" 

446 energies, spectrum = self.get_spectrum(omega, gamma, 

447 start, end, npts, width, 

448 type) 

449 

450 # Write out spectrum in file. First column is absolute intensities. 

451 outdata = np.empty([len(energies), 3]) 

452 outdata.T[0] = energies 

453 outdata.T[1] = spectrum 

454 

455 with paropen(out, 'w') as fd: 

456 fd.write('# Resonant Raman spectrum\n') 

457 if hasattr(self, '_approx'): 

458 fd.write(f'# approximation: {self._approx}\n') 

459 for key in self.observation: 

460 fd.write(f'# {key}: {self.observation[key]}\n') 

461 fd.write('# omega={:g} eV, gamma={:g} eV\n' 

462 .format(omega, gamma)) 

463 if width is not None: 

464 fd.write('# %s folded, width=%g cm^-1\n' 

465 % (type.title(), width)) 

466 fd.write('# [cm^-1] [a.u.]\n') 

467 

468 for row in outdata: 

469 fd.write('%.3f %15.5g\n' % 

470 (row[0], row[1])) 

471 

472 def summary(self, omega, gamma=0.1, 

473 method='standard', direction='central', 

474 log=sys.stdout): 

475 """Print summary for given omega [eV]""" 

476 self.read(method, direction) 

477 hnu = self.get_energies() 

478 intensities = self.get_absolute_intensities(omega, gamma) 

479 te = int(np.log10(intensities.max())) - 2 

480 scale = 10**(-te) 

481 if not te: 

482 ts = '' 

483 elif te > -2 and te < 3: 

484 ts = str(10**te) 

485 else: 

486 ts = f'10^{te}' 

487 

488 if isinstance(log, str): 

489 log = paropen(log, 'a') 

490 

491 parprint('-------------------------------------', file=log) 

492 parprint(' excitation at ' + str(omega) + ' eV', file=log) 

493 parprint(' gamma ' + str(gamma) + ' eV', file=log) 

494 parprint(' method:', self.vibrations.method, file=log) 

495 parprint(' approximation:', self.approximation, file=log) 

496 parprint(' Mode Frequency Intensity', file=log) 

497 parprint(f' # meV cm^-1 [{ts}A^4/amu]', file=log) 

498 parprint('-------------------------------------', file=log) 

499 for n, e in enumerate(hnu): 

500 if e.imag != 0: 

501 c = 'i' 

502 e = e.imag 

503 else: 

504 c = ' ' 

505 e = e.real 

506 parprint('%3d %6.1f%s %7.1f%s %9.2f' % 

507 (n, 1000 * e, c, e / u.invcm, c, intensities[n] * scale), 

508 file=log) 

509 parprint('-------------------------------------', file=log) 

510 parprint('Zero-point energy: %.3f eV' % 

511 self.vibrations.get_zero_point_energy(), 

512 file=log) 

513 

514 

515class LrResonantRaman(ResonantRaman): 

516 """Resonant Raman for linear response 

517 

518 Quick and dirty approach to enable loading of LrTDDFT calculations 

519 """ 

520 

521 def read_excitations(self): 

522 eq_disp = self._eq_disp() 

523 ex0_object = eq_disp.read_exobj() 

524 eu = ex0_object.energy_to_eV_scale 

525 matching = frozenset(ex0_object.kss) 

526 

527 def append(lst, disp, matching): 

528 exo = disp.read_exobj() 

529 lst.append(exo) 

530 matching = matching.intersection(exo.kss) 

531 return matching 

532 

533 exm_object_list = [] 

534 exp_object_list = [] 

535 for a in self.indices: 

536 for i in 'xyz': 

537 disp1 = self._disp(a, i, -1) 

538 disp2 = self._disp(a, i, 1) 

539 

540 matching = append(exm_object_list, 

541 disp1, 

542 matching) 

543 matching = append(exp_object_list, 

544 disp2, 

545 matching) 

546 

547 def select(exl, matching): 

548 exl.diagonalize(**self.exkwargs) 

549 mlist = list(exl) 

550# mlst = [ex for ex in exl if ex in matching] 

551# assert(len(mlst) == len(matching)) 

552 return mlist 

553 ex0 = select(ex0_object, matching) 

554 exm = [] 

555 exp = [] 

556 r = 0 

557 for a in self.indices: 

558 for i in 'xyz': 

559 exm.append(select(exm_object_list[r], matching)) 

560 exp.append(select(exp_object_list[r], matching)) 

561 r += 1 

562 

563 self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) 

564# self.exmE_p = np.array([ex.energy * eu for ex in exm]) 

565# self.expE_p = np.array([ex.energy * eu for ex in exp]) 

566 self.ex0m_pc = (np.array( 

567 [ex.get_dipole_me(form=self.dipole_form) for ex in ex0]) * 

568 u.Bohr) 

569 self.exF_rp = [] 

570 exmE_rp = [] 

571 expE_rp = [] 

572 exmm_rpc = [] 

573 expm_rpc = [] 

574 r = 0 

575 for a in self.indices: 

576 for i in 'xyz': 

577 exmE_rp.append([em.energy for em in exm[r]]) 

578 expE_rp.append([ep.energy for ep in exp[r]]) 

579 self.exF_rp.append( 

580 [(em.energy - ep.energy) 

581 for ep, em in zip(exp[r], exm[r])]) 

582 exmm_rpc.append( 

583 [ex.get_dipole_me(form=self.dipole_form) for ex in exm[r]]) 

584 expm_rpc.append( 

585 [ex.get_dipole_me(form=self.dipole_form) for ex in exp[r]]) 

586 r += 1 

587 self.exmE_rp = np.array(exmE_rp) * eu 

588 self.expE_rp = np.array(expE_rp) * eu 

589 self.exF_rp = np.array(self.exF_rp) * eu / 2 / self.delta 

590 self.exmm_rpc = np.array(exmm_rpc) * u.Bohr 

591 self.expm_rpc = np.array(expm_rpc) * u.Bohr