Coverage for /builds/ase/ase/ase/vibrations/raman.py: 89.47%
190 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
5import ase.units as u
6from ase.dft import monkhorst_pack
7from ase.parallel import world
8from ase.phonons import Phonons
9from ase.utils import IOContext
10from ase.vibrations.vibrations import AtomicDisplacements, Vibrations
13class RamanCalculatorBase(IOContext):
14 def __init__(self, atoms, # XXX do we need atoms at this stage ?
15 *args,
16 name='raman',
17 exext='.alpha',
18 txt='-',
19 verbose=False,
20 comm=world,
21 **kwargs):
22 """
23 Parameters
24 ----------
25 atoms: ase Atoms object
26 exext: string
27 Extension for excitation filenames
28 txt:
29 Output stream
30 verbose:
31 Verbosity level of output
32 comm:
33 Communicator, default world
34 """
35 kwargs['name'] = name
36 self.exname = kwargs.pop('exname', name)
38 super().__init__(atoms, *args, **kwargs)
40 self.exext = exext
42 self.txt = self.openfile(file=txt, comm=comm)
43 self.verbose = verbose
45 self.comm = comm
48class StaticRamanCalculatorBase(RamanCalculatorBase):
49 """Base class for Raman intensities derived from
50 static polarizabilities"""
52 def __init__(self, atoms, exobj, exkwargs=None, *args, **kwargs):
53 self.exobj = exobj
54 if exkwargs is None:
55 exkwargs = {}
56 self.exkwargs = exkwargs
57 super().__init__(atoms, *args, **kwargs)
59 def _new_exobj(self):
60 return self.exobj(**self.exkwargs)
62 def calculate(self, atoms, disp):
63 returnvalue = super().calculate(atoms, disp)
64 disp.calculate_and_save_static_polarizability(atoms)
65 return returnvalue
68class StaticRamanCalculator(StaticRamanCalculatorBase, Vibrations):
69 pass
72class StaticRamanPhononsCalculator(StaticRamanCalculatorBase, Phonons):
73 pass
76class RamanBase(AtomicDisplacements, IOContext):
77 def __init__(self, atoms, # XXX do we need atoms at this stage ?
78 *args,
79 name='raman',
80 exname=None,
81 exext='.alpha',
82 txt='-',
83 verbose=False,
84 comm=world,
85 **kwargs):
86 """
87 Parameters
88 ----------
89 atoms: ase Atoms object
90 exext: string
91 Extension for excitation filenames
92 txt:
93 Output stream
94 verbose:
95 Verbosity level of output
96 comm:
97 Communicator, default world
98 """
99 self.atoms = atoms
101 self.name = name
102 if exname is None:
103 self.exname = name
104 else:
105 self.exname = exname
106 self.exext = exext
108 self.txt = self.openfile(file=txt, comm=comm)
109 self.verbose = verbose
111 self.comm = comm
114class RamanData(RamanBase):
115 """Base class to evaluate Raman spectra from pre-computed data"""
117 def __init__(self, atoms, # XXX do we need atoms at this stage ?
118 *args,
119 exname=None, # name for excited state calculations
120 **kwargs):
121 """
122 Parameters
123 ----------
124 atoms: ase Atoms object
125 exname: string
126 name for excited state calculations (defaults to name),
127 used for reading excitations
128 """
129 super().__init__(atoms, *args, **kwargs)
131 if exname is None:
132 exname = kwargs.get('name', self.name)
133 self.exname = exname
134 self._already_read = False
136 def get_energies(self):
137 self.calculate_energies_and_modes()
138 return self.om_Q
140 def init_parallel_read(self):
141 """Initialize variables for parallel read"""
142 rank = self.comm.rank
143 indices = self.indices
144 myn = -(-self.ndof // self.comm.size) # ceil divide
145 self.slize = s = slice(myn * rank, myn * (rank + 1))
146 self.myindices = np.repeat(indices, 3)[s]
147 self.myxyz = ('xyz' * len(indices))[s]
148 self.myr = range(self.ndof)[s]
149 self.mynd = len(self.myr)
151 def read(self, *args, **kwargs):
152 """Read data from a pre-performed calculation."""
153 if self._already_read:
154 return
156 self.vibrations.read(*args, **kwargs)
157 self.init_parallel_read()
158 self.read_excitations()
160 self._already_read = True
162 @staticmethod
163 def m2(z):
164 return (z * z.conj()).real
166 def map_to_modes(self, V_rcc):
167 V_qcc = (V_rcc.T * self.im_r).T # units Angstrom^2 / sqrt(amu)
168 V_Qcc = np.dot(V_qcc.T, self.modes_Qq.T).T
169 return V_Qcc
171 def me_Qcc(self, *args, **kwargs):
172 """Full matrix element
174 Returns
175 -------
176 Matrix element in e^2 Angstrom^2 / eV
177 """
178 # Angstrom^2 / sqrt(amu)
179 elme_Qcc = self.electronic_me_Qcc(*args, **kwargs)
180 # Angstrom^3 -> e^2 Angstrom^2 / eV
181 elme_Qcc /= u.Hartree * u.Bohr # e^2 Angstrom / eV / sqrt(amu)
182 return elme_Qcc * self.vib01_Q[:, None, None]
184 def get_absolute_intensities(self, delta=0, **kwargs):
185 """Absolute Raman intensity or Raman scattering factor
187 Parameter
188 ---------
189 delta: float
190 pre-factor for asymmetric anisotropy, default 0
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)
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(**kwargs))
203 return 45 * alpha2_r + delta * delta2_r + 7 * gamma2_r
205 def intensity(self, *args, **kwargs):
206 """Raman intensity
208 Returns
209 -------
210 unit e^4 Angstrom^4 / eV^2
211 """
212 self.calculate_energies_and_modes()
214 m2 = Raman.m2
215 alpha_Qcc = self.me_Qcc(*args, **kwargs)
216 if not self.observation: # XXXX remove
217 """Simple sum, maybe too simple"""
218 return m2(alpha_Qcc).sum(axis=1).sum(axis=1)
219 # XXX enable when appropriate
220 # if self.observation['orientation'].lower() != 'random':
221 # raise NotImplementedError('not yet')
223 # random orientation of the molecular frame
224 # Woodward & Long,
225 # Guthmuller, J. J. Chem. Phys. 2016, 144 (6), 64106
226 alpha2_r, gamma2_r, delta2_r = self._invariants(alpha_Qcc)
228 if self.observation['geometry'] == '-Z(XX)Z': # Porto's notation
229 return (45 * alpha2_r + 5 * delta2_r + 4 * gamma2_r) / 45.
230 elif self.observation['geometry'] == '-Z(XY)Z': # Porto's notation
231 return gamma2_r / 15.
232 elif self.observation['scattered'] == 'Z':
233 # scattered light in direction of incoming light
234 return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45.
235 elif self.observation['scattered'] == 'parallel':
236 # scattered light perendicular and
237 # polarization in plane
238 return 6 * gamma2_r / 45.
239 elif self.observation['scattered'] == 'perpendicular':
240 # scattered light perendicular and
241 # polarization out of plane
242 return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45.
243 else:
244 raise NotImplementedError
246 def _invariants(self, alpha_Qcc):
247 """Raman invariants
249 Parameter
250 ---------
251 alpha_Qcc: array
252 Matrix element or polarizability tensor
254 Reference
255 ---------
256 Derek A. Long, The Raman Effect, ISBN 0-471-49028-8
258 Returns
259 -------
260 mean polarizability, anisotropy, asymmetric anisotropy
261 """
262 m2 = Raman.m2
263 alpha2_r = m2(alpha_Qcc[:, 0, 0] + alpha_Qcc[:, 1, 1] +
264 alpha_Qcc[:, 2, 2]) / 9.
265 delta2_r = 3 / 4. * (
266 m2(alpha_Qcc[:, 0, 1] - alpha_Qcc[:, 1, 0]) +
267 m2(alpha_Qcc[:, 0, 2] - alpha_Qcc[:, 2, 0]) +
268 m2(alpha_Qcc[:, 1, 2] - alpha_Qcc[:, 2, 1]))
269 gamma2_r = (3 / 4. * (m2(alpha_Qcc[:, 0, 1] + alpha_Qcc[:, 1, 0]) +
270 m2(alpha_Qcc[:, 0, 2] + alpha_Qcc[:, 2, 0]) +
271 m2(alpha_Qcc[:, 1, 2] + alpha_Qcc[:, 2, 1])) +
272 (m2(alpha_Qcc[:, 0, 0] - alpha_Qcc[:, 1, 1]) +
273 m2(alpha_Qcc[:, 0, 0] - alpha_Qcc[:, 2, 2]) +
274 m2(alpha_Qcc[:, 1, 1] - alpha_Qcc[:, 2, 2])) / 2)
275 return alpha2_r, gamma2_r, delta2_r
277 def summary(self, log='-'):
278 """Print summary for given omega [eV]"""
279 with IOContext() as io:
280 log = io.openfile(file=log, mode='a', comm=self.comm)
281 return self._summary(log)
283 def _summary(self, log):
284 hnu = self.get_energies()
285 intensities = self.get_absolute_intensities()
286 te = int(np.log10(intensities.max())) - 2
287 scale = 10**(-te)
289 if not te:
290 ts = ''
291 elif te > -2 and te < 3:
292 ts = str(10**te)
293 else:
294 ts = f'10^{te}'
296 print('-------------------------------------', file=log)
297 print(' Mode Frequency Intensity', file=log)
298 print(f' # meV cm^-1 [{ts}A^4/amu]', file=log)
299 print('-------------------------------------', file=log)
300 for n, e in enumerate(hnu):
301 if e.imag != 0:
302 c = 'i'
303 e = e.imag
304 else:
305 c = ' '
306 e = e.real
307 print('%3d %6.1f%s %7.1f%s %9.2f' %
308 (n, 1000 * e, c, e / u.invcm, c, intensities[n] * scale),
309 file=log)
310 print('-------------------------------------', file=log)
311 # XXX enable this in phonons
312 # parprint('Zero-point energy: %.3f eV' %
313 # self.vibrations.get_zero_point_energy(),
314 # file=log)
317class Raman(RamanData):
318 def __init__(self, atoms, *args, **kwargs):
319 super().__init__(atoms, *args, **kwargs)
321 for key in ['txt', 'exext', 'exname']:
322 kwargs.pop(key, None)
323 kwargs['name'] = kwargs.get('name', self.name)
324 self.vibrations = Vibrations(atoms, *args, **kwargs)
326 self.delta = self.vibrations.delta
327 self.indices = self.vibrations.indices
329 def calculate_energies_and_modes(self):
330 if hasattr(self, 'im_r'):
331 return
333 self.read()
335 self.im_r = self.vibrations.im
336 self.modes_Qq = self.vibrations.modes
337 self.om_Q = self.vibrations.hnu.real # energies in eV
338 self.H = self.vibrations.H # XXX used in albrecht.py
339 # pre-factors for one vibrational excitation
340 with np.errstate(divide='ignore'):
341 self.vib01_Q = np.where(self.om_Q > 0,
342 1. / np.sqrt(2 * self.om_Q), 0)
343 # -> sqrt(amu) * Angstrom
344 self.vib01_Q *= np.sqrt(u.Ha * u._me / u._amu) * u.Bohr
347class RamanPhonons(RamanData):
348 def __init__(self, atoms, *args, **kwargs):
349 RamanData.__init__(self, atoms, *args, **kwargs)
351 for key in ['txt', 'exext', 'exname']:
352 kwargs.pop(key, None)
353 kwargs['name'] = kwargs.get('name', self.name)
354 self.vibrations = Phonons(atoms, *args, **kwargs)
356 self.delta = self.vibrations.delta
357 self.indices = self.vibrations.indices
359 self.kpts = (1, 1, 1)
361 @property
362 def kpts(self):
363 return self._kpts
365 @kpts.setter
366 def kpts(self, kpts):
367 if not hasattr(self, '_kpts') or kpts != self._kpts:
368 self._kpts = kpts
369 self.kpts_kc = monkhorst_pack(self.kpts)
370 if hasattr(self, 'im_r'):
371 del self.im_r # we'll have to recalculate
373 def calculate_energies_and_modes(self):
374 if not self._already_read:
375 if hasattr(self, 'im_r'):
376 del self.im_r
377 self.read()
379 if not hasattr(self, 'im_r'):
380 omega_kl, u_kl = self.vibrations.band_structure(
381 self.kpts_kc, modes=True, verbose=self.verbose)
383 self.im_r = self.vibrations.m_inv_x
384 self.om_Q = omega_kl.ravel().real # energies in eV
385 self.modes_Qq = u_kl.reshape(len(self.om_Q),
386 3 * len(self.atoms))
387 self.modes_Qq /= self.im_r
388 self.om_v = self.om_Q
390 # pre-factors for one vibrational excitation
391 with np.errstate(divide='ignore', invalid='ignore'):
392 self.vib01_Q = np.where(
393 self.om_Q > 0, 1. / np.sqrt(2 * self.om_Q), 0)
394 # -> sqrt(amu) * Angstrom
395 self.vib01_Q *= np.sqrt(u.Ha * u._me / u._amu) * u.Bohr