Source code for ase.vibrations.resonant_raman

"""Resonant Raman intensities"""

import sys
from pathlib import Path

import numpy as np

import ase.units as u
from ase.parallel import paropen, parprint, world
from ase.vibrations import Vibrations
from ase.vibrations.raman import Raman, RamanCalculatorBase


[docs]class ResonantRamanCalculator(RamanCalculatorBase, Vibrations): """Base class for resonant Raman calculators using finite differences. """ def __init__(self, atoms, ExcitationsCalculator, *args, exkwargs=None, exext='.ex.gz', overlap=False, **kwargs): """ Parameters ---------- atoms: Atoms The Atoms object ExcitationsCalculator: object Calculator for excited states exkwargs: dict Arguments given to the ExcitationsCalculator object exext: string Extension for filenames of Excitation lists (results of the ExcitationsCalculator). overlap : function or False Function to calculate overlaps between excitation at equilibrium and at a displaced position. Calculators are given as first and second argument, respectively. Example ------- >>> from ase.calculators.h2morse import (H2Morse, ... H2MorseExcitedStatesCalculator) >>> from ase.vibrations.resonant_raman import ResonantRamanCalculator >>> >>> atoms = H2Morse() >>> rmc = ResonantRamanCalculator(atoms, H2MorseExcitedStatesCalculator) >>> rmc.run() This produces all necessary data for further analysis. """ self.exobj = ExcitationsCalculator if exkwargs is None: exkwargs = {} self.exkwargs = exkwargs self.overlap = overlap super().__init__(atoms, *args, exext=exext, **kwargs) def _new_exobj(self): # XXXX I have to duplicate this because there are two objects # which have exkwargs, why are they not unified? return self.exobj(**self.exkwargs) def calculate(self, atoms, disp): """Call ground and excited state calculation""" assert atoms == self.atoms # XXX action required forces = self.atoms.get_forces() if self.overlap: """Overlap is determined as ov_ij = int dr displaced*_i(r) eqilibrium_j(r) """ ov_nn = self.overlap(self.atoms.calc, self.eq_calculator) if world.rank == 0: disp.save_ov_nn(ov_nn) disp.calculate_and_save_exlist(atoms) return {'forces': forces} def run(self): if self.overlap: # XXXX stupid way to make a copy self.atoms.get_potential_energy() self.eq_calculator = self.atoms.calc Path(self.name).mkdir(parents=True, exist_ok=True) fname = Path(self.name) / 'tmp.gpw' self.eq_calculator.write(fname, 'all') self.eq_calculator = self.eq_calculator.__class__(restart=fname) try: # XXX GPAW specific self.eq_calculator.converge_wave_functions() except AttributeError: pass Vibrations.run(self)
class ResonantRaman(Raman): """Base Class for resonant Raman intensities using finite differences. """ def __init__(self, atoms, Excitations, *args, observation=None, form='v', # form of the dipole operator exkwargs=None, # kwargs to be passed to Excitations exext='.ex.gz', # extension for Excitation names overlap=False, minoverlap=0.02, minrep=0.8, comm=world, **kwargs): """ Parameters ---------- atoms: ase Atoms object Excitations: class Type of the excitation list object. The class object is initialized as:: Excitations(atoms.calc) or by reading form a file as:: Excitations('filename', **exkwargs) The file is written by calling the method Excitations.write('filename'). Excitations should work like a list of ex obejects, where: ex.get_dipole_me(form='v'): gives the velocity form dipole matrix element in units |e| * Angstrom ex.energy: is the transition energy in Hartrees approximation: string Level of approximation used. observation: dict Polarization settings form: string Form of the dipole operator, 'v' for velocity form (default) and 'r' for length form. overlap: bool or function Use wavefunction overlaps. minoverlap: float ord dict Minimal absolute overlap to consider. Defaults to 0.02 to avoid numerical garbage. minrep: float Minimal representation to consider derivative, defaults to 0.8 """ if observation is None: observation = {'geometry': '-Z(XX)Z'} kwargs['exext'] = exext Raman.__init__(self, atoms, *args, **kwargs) assert self.vibrations.nfree == 2 self.exobj = Excitations if exkwargs is None: exkwargs = {} self.exkwargs = exkwargs self.observation = observation self.dipole_form = form self.overlap = overlap if not isinstance(minoverlap, dict): # assume it's a number self.minoverlap = {'orbitals': minoverlap, 'excitations': minoverlap} else: self.minoverlap = minoverlap self.minrep = minrep def read_exobj(self, filename): return self.exobj.read(filename, **self.exkwargs) def get_absolute_intensities(self, omega, gamma=0.1, delta=0, **kwargs): """Absolute Raman intensity or Raman scattering factor Parameter --------- omega: float incoming laser energy, unit eV gamma: float width (imaginary energy), unit eV delta: float pre-factor for asymmetric anisotropy, default 0 References ---------- Porezag and Pederson, PRB 54 (1996) 7830-7836 (delta=0) Baiardi and Barone, JCTC 11 (2015) 3267-3280 (delta=5) Returns ------- raman intensity, unit Ang**4/amu """ alpha2_r, gamma2_r, delta2_r = self._invariants( self.electronic_me_Qcc(omega, gamma, **kwargs)) return 45 * alpha2_r + delta * delta2_r + 7 * gamma2_r @property def approximation(self): return self._approx @approximation.setter def approximation(self, value): self.set_approximation(value) def read_excitations(self): """Read all finite difference excitations and select matching.""" if self.overlap: return self.read_excitations_overlap() disp = self._eq_disp() ex0_object = disp.read_exobj() eu = ex0_object.energy_to_eV_scale matching = frozenset(ex0_object) def append(lst, disp, matching): exo = disp.read_exobj() lst.append(exo) matching = matching.intersection(exo) return matching exm_object_list = [] exp_object_list = [] for a, i in zip(self.myindices, self.myxyz): mdisp = self._disp(a, i, -1) pdisp = self._disp(a, i, 1) matching = append(exm_object_list, mdisp, matching) matching = append(exp_object_list, pdisp, matching) def select(exl, matching): mlst = [ex for ex in exl if ex in matching] assert len(mlst) == len(matching) return mlst ex0 = select(ex0_object, matching) exm = [] exp = [] r = 0 for a, i in zip(self.myindices, self.myxyz): exm.append(select(exm_object_list[r], matching)) exp.append(select(exp_object_list[r], matching)) r += 1 self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) self.ex0m_pc = (np.array( [ex.get_dipole_me(form=self.dipole_form) for ex in ex0]) * u.Bohr) exmE_rp = [] expE_rp = [] exF_rp = [] exmm_rpc = [] expm_rpc = [] r = 0 for a, i in zip(self.myindices, self.myxyz): exmE_rp.append([em.energy for em in exm[r]]) expE_rp.append([ep.energy for ep in exp[r]]) exF_rp.append( [(em.energy - ep.energy) for ep, em in zip(exp[r], exm[r])]) exmm_rpc.append( [ex.get_dipole_me(form=self.dipole_form) for ex in exm[r]]) expm_rpc.append( [ex.get_dipole_me(form=self.dipole_form) for ex in exp[r]]) r += 1 # indicees: r=coordinate, p=excitation # energies in eV self.exmE_rp = np.array(exmE_rp) * eu self.expE_rp = np.array(expE_rp) * eu # forces in eV / Angstrom self.exF_rp = np.array(exF_rp) * eu / 2 / self.delta # matrix elements in e * Angstrom self.exmm_rpc = np.array(exmm_rpc) * u.Bohr self.expm_rpc = np.array(expm_rpc) * u.Bohr def read_excitations_overlap(self): """Read all finite difference excitations and wf overlaps. We assume that the wave function overlaps are determined as ov_ij = int dr displaced*_i(r) eqilibrium_j(r) """ ex0 = self._eq_disp().read_exobj() eu = ex0.energy_to_eV_scale rep0_p = np.ones((len(ex0)), dtype=float) def load(disp, rep0_p): ex_p = disp.read_exobj() ov_nn = disp.load_ov_nn() # remove numerical garbage ov_nn = np.where(np.abs(ov_nn) > self.minoverlap['orbitals'], ov_nn, 0) ov_pp = ex_p.overlap(ov_nn, ex0) ov_pp = np.where(np.abs(ov_pp) > self.minoverlap['excitations'], ov_pp, 0) rep0_p *= (ov_pp.real**2 + ov_pp.imag**2).sum(axis=0) return ex_p, ov_pp def rotate(ex_p, ov_pp): e_p = np.array([ex.energy for ex in ex_p]) m_pc = np.array( [ex.get_dipole_me(form=self.dipole_form) for ex in ex_p]) r_pp = ov_pp.T return ((r_pp.real**2 + r_pp.imag**2).dot(e_p), r_pp.dot(m_pc)) exmE_rp = [] expE_rp = [] exF_rp = [] exmm_rpc = [] expm_rpc = [] exdmdr_rpc = [] for a, i in zip(self.myindices, self.myxyz): mdisp = self._disp(a, i, -1) pdisp = self._disp(a, i, 1) ex, ov = load(mdisp, rep0_p) exmE_p, exmm_pc = rotate(ex, ov) ex, ov = load(pdisp, rep0_p) expE_p, expm_pc = rotate(ex, ov) exmE_rp.append(exmE_p) expE_rp.append(expE_p) exF_rp.append(exmE_p - expE_p) exmm_rpc.append(exmm_pc) expm_rpc.append(expm_pc) exdmdr_rpc.append(expm_pc - exmm_pc) # select only excitations that are sufficiently represented self.comm.product(rep0_p) select = np.where(rep0_p > self.minrep)[0] self.ex0E_p = np.array([ex.energy * eu for ex in ex0])[select] self.ex0m_pc = (np.array( [ex.get_dipole_me(form=self.dipole_form) for ex in ex0])[select] * u.Bohr) if len(self.myr): # indicees: r=coordinate, p=excitation # energies in eV self.exmE_rp = np.array(exmE_rp)[:, select] * eu self.expE_rp = np.array(expE_rp)[:, select] * eu # forces in eV / Angstrom self.exF_rp = np.array(exF_rp)[:, select] * eu / 2 / self.delta # matrix elements in e * Angstrom self.exmm_rpc = np.array(exmm_rpc)[:, select, :] * u.Bohr self.expm_rpc = np.array(expm_rpc)[:, select, :] * u.Bohr # matrix element derivatives in e self.exdmdr_rpc = (np.array(exdmdr_rpc)[:, select, :] * u.Bohr / 2 / self.delta) else: # did not read self.exmE_rp = self.expE_rp = self.exF_rp = np.empty(0) self.exmm_rpc = self.expm_rpc = self.exdmdr_rpc = np.empty(0) def read(self, *args, **kwargs): """Read data from a pre-performed calculation.""" self.vibrations.read(*args, **kwargs) self.init_parallel_read() if not hasattr(self, 'ex0E_p'): if self.overlap: self.read_excitations_overlap() else: self.read_excitations() self._already_read = True def get_cross_sections(self, omega, gamma): """Returns Raman cross sections for each vibration.""" I_v = self.intensity(omega, gamma) pre = 1. / 16 / np.pi**2 / u._eps0**2 / u._c**4 # frequency of scattered light omS_v = omega - self.om_Q return pre * omega * omS_v**3 * I_v def get_spectrum(self, omega, gamma=0.1, start=None, end=None, npts=None, width=20, type='Gaussian', intensity_unit='????', normalize=False): """Get resonant Raman spectrum. The method returns wavenumbers in cm^-1 with corresponding Raman cross section. Start and end point, and width of the Gaussian/Lorentzian should be given in cm^-1. """ self.type = type.lower() assert self.type in ['gaussian', 'lorentzian'] frequencies = self.get_energies().real / u.invcm intensities = self.get_cross_sections(omega, gamma) if width is None: return [frequencies, intensities] if start is None: start = min(self.om_Q) / u.invcm - 3 * width if end is None: end = max(self.om_Q) / u.invcm + 3 * width if not npts: npts = int((end - start) / width * 10 + 1) prefactor = 1 if self.type == 'lorentzian': intensities = intensities * width * np.pi / 2. if normalize: prefactor = 2. / width / np.pi else: sigma = width / 2. / np.sqrt(2. * np.log(2.)) if normalize: prefactor = 1. / sigma / np.sqrt(2 * np.pi) # Make array with spectrum data spectrum = np.empty(npts) energies = np.linspace(start, end, npts) for i, energy in enumerate(energies): energies[i] = energy if self.type == 'lorentzian': spectrum[i] = (intensities * 0.5 * width / np.pi / ((frequencies - energy)**2 + 0.25 * width**2)).sum() else: spectrum[i] = (intensities * np.exp(-(frequencies - energy)**2 / 2. / sigma**2)).sum() return [energies, prefactor * spectrum] def write_spectrum(self, omega, gamma, out='resonant-raman-spectra.dat', start=200, end=4000, npts=None, width=10, type='Gaussian'): """Write out spectrum to file. Start and end point, and width of the Gaussian/Lorentzian should be given in cm^-1.""" energies, spectrum = self.get_spectrum(omega, gamma, start, end, npts, width, type) # Write out spectrum in file. First column is absolute intensities. outdata = np.empty([len(energies), 3]) outdata.T[0] = energies outdata.T[1] = spectrum with paropen(out, 'w') as fd: fd.write('# Resonant Raman spectrum\n') if hasattr(self, '_approx'): fd.write(f'# approximation: {self._approx}\n') for key in self.observation: fd.write(f'# {key}: {self.observation[key]}\n') fd.write('# omega={:g} eV, gamma={:g} eV\n' .format(omega, gamma)) if width is not None: fd.write('# %s folded, width=%g cm^-1\n' % (type.title(), width)) fd.write('# [cm^-1] [a.u.]\n') for row in outdata: fd.write('%.3f %15.5g\n' % (row[0], row[1])) def summary(self, omega, gamma=0.1, method='standard', direction='central', log=sys.stdout): """Print summary for given omega [eV]""" self.read(method, direction) hnu = self.get_energies() intensities = self.get_absolute_intensities(omega, gamma) te = int(np.log10(intensities.max())) - 2 scale = 10**(-te) if not te: ts = '' elif te > -2 and te < 3: ts = str(10**te) else: ts = f'10^{te}' if isinstance(log, str): log = paropen(log, 'a') parprint('-------------------------------------', file=log) parprint(' excitation at ' + str(omega) + ' eV', file=log) parprint(' gamma ' + str(gamma) + ' eV', file=log) parprint(' method:', self.vibrations.method, file=log) parprint(' approximation:', self.approximation, file=log) parprint(' Mode Frequency Intensity', file=log) parprint(f' # meV cm^-1 [{ts}A^4/amu]', file=log) parprint('-------------------------------------', file=log) for n, e in enumerate(hnu): if e.imag != 0: c = 'i' e = e.imag else: c = ' ' e = e.real parprint('%3d %6.1f%s %7.1f%s %9.2f' % (n, 1000 * e, c, e / u.invcm, c, intensities[n] * scale), file=log) parprint('-------------------------------------', file=log) parprint('Zero-point energy: %.3f eV' % self.vibrations.get_zero_point_energy(), file=log) class LrResonantRaman(ResonantRaman): """Resonant Raman for linear response Quick and dirty approach to enable loading of LrTDDFT calculations """ def read_excitations(self): eq_disp = self._eq_disp() ex0_object = eq_disp.read_exobj() eu = ex0_object.energy_to_eV_scale matching = frozenset(ex0_object.kss) def append(lst, disp, matching): exo = disp.read_exobj() lst.append(exo) matching = matching.intersection(exo.kss) return matching exm_object_list = [] exp_object_list = [] for a in self.indices: for i in 'xyz': disp1 = self._disp(a, i, -1) disp2 = self._disp(a, i, 1) matching = append(exm_object_list, disp1, matching) matching = append(exp_object_list, disp2, matching) def select(exl, matching): exl.diagonalize(**self.exkwargs) mlist = list(exl) # mlst = [ex for ex in exl if ex in matching] # assert(len(mlst) == len(matching)) return mlist ex0 = select(ex0_object, matching) exm = [] exp = [] r = 0 for a in self.indices: for i in 'xyz': exm.append(select(exm_object_list[r], matching)) exp.append(select(exp_object_list[r], matching)) r += 1 self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) # self.exmE_p = np.array([ex.energy * eu for ex in exm]) # self.expE_p = np.array([ex.energy * eu for ex in exp]) self.ex0m_pc = (np.array( [ex.get_dipole_me(form=self.dipole_form) for ex in ex0]) * u.Bohr) self.exF_rp = [] exmE_rp = [] expE_rp = [] exmm_rpc = [] expm_rpc = [] r = 0 for a in self.indices: for i in 'xyz': exmE_rp.append([em.energy for em in exm[r]]) expE_rp.append([ep.energy for ep in exp[r]]) self.exF_rp.append( [(em.energy - ep.energy) for ep, em in zip(exp[r], exm[r])]) exmm_rpc.append( [ex.get_dipole_me(form=self.dipole_form) for ex in exm[r]]) expm_rpc.append( [ex.get_dipole_me(form=self.dipole_form) for ex in exp[r]]) r += 1 self.exmE_rp = np.array(exmE_rp) * eu self.expE_rp = np.array(expE_rp) * eu self.exF_rp = np.array(self.exF_rp) * eu / 2 / self.delta self.exmm_rpc = np.array(exmm_rpc) * u.Bohr self.expm_rpc = np.array(expm_rpc) * u.Bohr