Coverage for /builds/kinetik161/ase/ase/vibrations/raman.py: 89.95%
189 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1import numpy as np
3import ase.units as u
4from ase.dft import monkhorst_pack
5from ase.parallel import world
6from ase.phonons import Phonons
7from ase.utils import IOContext
8from ase.vibrations.vibrations import AtomicDisplacements, Vibrations
11class RamanCalculatorBase(IOContext):
12 def __init__(self, atoms, # XXX do we need atoms at this stage ?
13 *args,
14 name='raman',
15 exext='.alpha',
16 txt='-',
17 verbose=False,
18 comm=world,
19 **kwargs):
20 """
21 Parameters
22 ----------
23 atoms: ase Atoms object
24 exext: string
25 Extension for excitation filenames
26 txt:
27 Output stream
28 verbose:
29 Verbosity level of output
30 comm:
31 Communicator, default world
32 """
33 kwargs['name'] = name
34 self.exname = kwargs.pop('exname', name)
36 super().__init__(atoms, *args, **kwargs)
38 self.exext = exext
40 self.txt = self.openfile(txt, comm)
41 self.verbose = verbose
43 self.comm = comm
46class StaticRamanCalculatorBase(RamanCalculatorBase):
47 """Base class for Raman intensities derived from
48 static polarizabilities"""
50 def __init__(self, atoms, exobj, exkwargs=None, *args, **kwargs):
51 self.exobj = exobj
52 if exkwargs is None:
53 exkwargs = {}
54 self.exkwargs = exkwargs
55 super().__init__(atoms, *args, **kwargs)
57 def _new_exobj(self):
58 return self.exobj(**self.exkwargs)
60 def calculate(self, atoms, disp):
61 returnvalue = super().calculate(atoms, disp)
62 disp.calculate_and_save_static_polarizability(atoms)
63 return returnvalue
66class StaticRamanCalculator(StaticRamanCalculatorBase, Vibrations):
67 pass
70class StaticRamanPhononsCalculator(StaticRamanCalculatorBase, Phonons):
71 pass
74class RamanBase(AtomicDisplacements, IOContext):
75 def __init__(self, atoms, # XXX do we need atoms at this stage ?
76 *args,
77 name='raman',
78 exname=None,
79 exext='.alpha',
80 txt='-',
81 verbose=False,
82 comm=world,
83 **kwargs):
84 """
85 Parameters
86 ----------
87 atoms: ase Atoms object
88 exext: string
89 Extension for excitation filenames
90 txt:
91 Output stream
92 verbose:
93 Verbosity level of output
94 comm:
95 Communicator, default world
96 """
97 self.atoms = atoms
99 self.name = name
100 if exname is None:
101 self.exname = name
102 else:
103 self.exname = exname
104 self.exext = exext
106 self.txt = self.openfile(txt, comm)
107 self.verbose = verbose
109 self.comm = comm
112class RamanData(RamanBase):
113 """Base class to evaluate Raman spectra from pre-computed data"""
115 def __init__(self, atoms, # XXX do we need atoms at this stage ?
116 *args,
117 exname=None, # name for excited state calculations
118 **kwargs):
119 """
120 Parameters
121 ----------
122 atoms: ase Atoms object
123 exname: string
124 name for excited state calculations (defaults to name),
125 used for reading excitations
126 """
127 super().__init__(atoms, *args, **kwargs)
129 if exname is None:
130 exname = kwargs.get('name', self.name)
131 self.exname = exname
132 self._already_read = False
134 def get_energies(self):
135 self.calculate_energies_and_modes()
136 return self.om_Q
138 def init_parallel_read(self):
139 """Initialize variables for parallel read"""
140 rank = self.comm.rank
141 indices = self.indices
142 myn = -(-self.ndof // self.comm.size) # ceil divide
143 self.slize = s = slice(myn * rank, myn * (rank + 1))
144 self.myindices = np.repeat(indices, 3)[s]
145 self.myxyz = ('xyz' * len(indices))[s]
146 self.myr = range(self.ndof)[s]
147 self.mynd = len(self.myr)
149 def read(self, *args, **kwargs):
150 """Read data from a pre-performed calculation."""
151 if self._already_read:
152 return
154 self.vibrations.read(*args, **kwargs)
155 self.init_parallel_read()
156 self.read_excitations()
158 self._already_read = True
160 @staticmethod
161 def m2(z):
162 return (z * z.conj()).real
164 def map_to_modes(self, V_rcc):
165 V_qcc = (V_rcc.T * self.im_r).T # units Angstrom^2 / sqrt(amu)
166 V_Qcc = np.dot(V_qcc.T, self.modes_Qq.T).T
167 return V_Qcc
169 def me_Qcc(self, *args, **kwargs):
170 """Full matrix element
172 Returns
173 -------
174 Matrix element in e^2 Angstrom^2 / eV
175 """
176 # Angstrom^2 / sqrt(amu)
177 elme_Qcc = self.electronic_me_Qcc(*args, **kwargs)
178 # Angstrom^3 -> e^2 Angstrom^2 / eV
179 elme_Qcc /= u.Hartree * u.Bohr # e^2 Angstrom / eV / sqrt(amu)
180 return elme_Qcc * self.vib01_Q[:, None, None]
182 def get_absolute_intensities(self, delta=0, **kwargs):
183 """Absolute Raman intensity or Raman scattering factor
185 Parameter
186 ---------
187 delta: float
188 pre-factor for asymmetric anisotropy, default 0
190 References
191 ----------
192 Porezag and Pederson, PRB 54 (1996) 7830-7836 (delta=0)
193 Baiardi and Barone, JCTC 11 (2015) 3267-3280 (delta=5)
195 Returns
196 -------
197 raman intensity, unit Ang**4/amu
198 """
199 alpha2_r, gamma2_r, delta2_r = self._invariants(
200 self.electronic_me_Qcc(**kwargs))
201 return 45 * alpha2_r + delta * delta2_r + 7 * gamma2_r
203 def intensity(self, *args, **kwargs):
204 """Raman intensity
206 Returns
207 -------
208 unit e^4 Angstrom^4 / eV^2
209 """
210 self.calculate_energies_and_modes()
212 m2 = Raman.m2
213 alpha_Qcc = self.me_Qcc(*args, **kwargs)
214 if not self.observation: # XXXX remove
215 """Simple sum, maybe too simple"""
216 return m2(alpha_Qcc).sum(axis=1).sum(axis=1)
217 # XXX enable when appropriate
218 # if self.observation['orientation'].lower() != 'random':
219 # raise NotImplementedError('not yet')
221 # random orientation of the molecular frame
222 # Woodward & Long,
223 # Guthmuller, J. J. Chem. Phys. 2016, 144 (6), 64106
224 alpha2_r, gamma2_r, delta2_r = self._invariants(alpha_Qcc)
226 if self.observation['geometry'] == '-Z(XX)Z': # Porto's notation
227 return (45 * alpha2_r + 5 * delta2_r + 4 * gamma2_r) / 45.
228 elif self.observation['geometry'] == '-Z(XY)Z': # Porto's notation
229 return gamma2_r / 15.
230 elif self.observation['scattered'] == 'Z':
231 # scattered light in direction of incoming light
232 return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45.
233 elif self.observation['scattered'] == 'parallel':
234 # scattered light perendicular and
235 # polarization in plane
236 return 6 * gamma2_r / 45.
237 elif self.observation['scattered'] == 'perpendicular':
238 # scattered light perendicular and
239 # polarization out of plane
240 return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45.
241 else:
242 raise NotImplementedError
244 def _invariants(self, alpha_Qcc):
245 """Raman invariants
247 Parameter
248 ---------
249 alpha_Qcc: array
250 Matrix element or polarizability tensor
252 Reference
253 ---------
254 Derek A. Long, The Raman Effect, ISBN 0-471-49028-8
256 Returns
257 -------
258 mean polarizability, anisotropy, asymmetric anisotropy
259 """
260 m2 = Raman.m2
261 alpha2_r = m2(alpha_Qcc[:, 0, 0] + alpha_Qcc[:, 1, 1] +
262 alpha_Qcc[:, 2, 2]) / 9.
263 delta2_r = 3 / 4. * (
264 m2(alpha_Qcc[:, 0, 1] - alpha_Qcc[:, 1, 0]) +
265 m2(alpha_Qcc[:, 0, 2] - alpha_Qcc[:, 2, 0]) +
266 m2(alpha_Qcc[:, 1, 2] - alpha_Qcc[:, 2, 1]))
267 gamma2_r = (3 / 4. * (m2(alpha_Qcc[:, 0, 1] + alpha_Qcc[:, 1, 0]) +
268 m2(alpha_Qcc[:, 0, 2] + alpha_Qcc[:, 2, 0]) +
269 m2(alpha_Qcc[:, 1, 2] + alpha_Qcc[:, 2, 1])) +
270 (m2(alpha_Qcc[:, 0, 0] - alpha_Qcc[:, 1, 1]) +
271 m2(alpha_Qcc[:, 0, 0] - alpha_Qcc[:, 2, 2]) +
272 m2(alpha_Qcc[:, 1, 1] - alpha_Qcc[:, 2, 2])) / 2)
273 return alpha2_r, gamma2_r, delta2_r
275 def summary(self, log='-'):
276 """Print summary for given omega [eV]"""
277 with IOContext() as io:
278 log = io.openfile(log, comm=self.comm, mode='a')
279 return self._summary(log)
281 def _summary(self, log):
282 hnu = self.get_energies()
283 intensities = self.get_absolute_intensities()
284 te = int(np.log10(intensities.max())) - 2
285 scale = 10**(-te)
287 if not te:
288 ts = ''
289 elif te > -2 and te < 3:
290 ts = str(10**te)
291 else:
292 ts = f'10^{te}'
294 print('-------------------------------------', file=log)
295 print(' Mode Frequency Intensity', file=log)
296 print(f' # meV cm^-1 [{ts}A^4/amu]', file=log)
297 print('-------------------------------------', file=log)
298 for n, e in enumerate(hnu):
299 if e.imag != 0:
300 c = 'i'
301 e = e.imag
302 else:
303 c = ' '
304 e = e.real
305 print('%3d %6.1f%s %7.1f%s %9.2f' %
306 (n, 1000 * e, c, e / u.invcm, c, intensities[n] * scale),
307 file=log)
308 print('-------------------------------------', file=log)
309 # XXX enable this in phonons
310 # parprint('Zero-point energy: %.3f eV' %
311 # self.vibrations.get_zero_point_energy(),
312 # file=log)
315class Raman(RamanData):
316 def __init__(self, atoms, *args, **kwargs):
317 super().__init__(atoms, *args, **kwargs)
319 for key in ['txt', 'exext', 'exname']:
320 kwargs.pop(key, None)
321 kwargs['name'] = kwargs.get('name', self.name)
322 self.vibrations = Vibrations(atoms, *args, **kwargs)
324 self.delta = self.vibrations.delta
325 self.indices = self.vibrations.indices
327 def calculate_energies_and_modes(self):
328 if hasattr(self, 'im_r'):
329 return
331 self.read()
333 self.im_r = self.vibrations.im
334 self.modes_Qq = self.vibrations.modes
335 self.om_Q = self.vibrations.hnu.real # energies in eV
336 self.H = self.vibrations.H # XXX used in albrecht.py
337 # pre-factors for one vibrational excitation
338 with np.errstate(divide='ignore'):
339 self.vib01_Q = np.where(self.om_Q > 0,
340 1. / np.sqrt(2 * self.om_Q), 0)
341 # -> sqrt(amu) * Angstrom
342 self.vib01_Q *= np.sqrt(u.Ha * u._me / u._amu) * u.Bohr
345class RamanPhonons(RamanData):
346 def __init__(self, atoms, *args, **kwargs):
347 RamanData.__init__(self, atoms, *args, **kwargs)
349 for key in ['txt', 'exext', 'exname']:
350 kwargs.pop(key, None)
351 kwargs['name'] = kwargs.get('name', self.name)
352 self.vibrations = Phonons(atoms, *args, **kwargs)
354 self.delta = self.vibrations.delta
355 self.indices = self.vibrations.indices
357 self.kpts = (1, 1, 1)
359 @property
360 def kpts(self):
361 return self._kpts
363 @kpts.setter
364 def kpts(self, kpts):
365 if not hasattr(self, '_kpts') or kpts != self._kpts:
366 self._kpts = kpts
367 self.kpts_kc = monkhorst_pack(self.kpts)
368 if hasattr(self, 'im_r'):
369 del self.im_r # we'll have to recalculate
371 def calculate_energies_and_modes(self):
372 if not self._already_read:
373 if hasattr(self, 'im_r'):
374 del self.im_r
375 self.read()
377 if not hasattr(self, 'im_r'):
378 omega_kl, u_kl = self.vibrations.band_structure(
379 self.kpts_kc, modes=True, verbose=self.verbose)
381 self.im_r = self.vibrations.m_inv_x
382 self.om_Q = omega_kl.ravel().real # energies in eV
383 self.modes_Qq = u_kl.reshape(len(self.om_Q),
384 3 * len(self.atoms))
385 self.modes_Qq /= self.im_r
386 self.om_v = self.om_Q
388 # pre-factors for one vibrational excitation
389 with np.errstate(divide='ignore', invalid='ignore'):
390 self.vib01_Q = np.where(
391 self.om_Q > 0, 1. / np.sqrt(2 * self.om_Q), 0)
392 # -> sqrt(amu) * Angstrom
393 self.vib01_Q *= np.sqrt(u.Ha * u._me / u._amu) * u.Bohr