def test_franck_condon(testdir): # FCOverlap fco = FranckCondonOverlap() fcr = FranckCondonRecursive() # check factorial assert (fco.factorial(8) == factorial(8)) # the second test is useful according to the implementation assert (fco.factorial(5) == factorial(5)) assert (fco.factorial.inv(5) == 1. / factorial(5)) # check T=0 and n=0 equality S = np.array([1, 2.1, 34]) m = 5 assert (((fco.directT0(m, S) - fco.direct(0, m, S)) / fco.directT0(m, S) < 1e-15).all()) # check symmetry S = 2 n = 3 assert (fco.direct(n, m, S) == fco.direct(m, n, S)) # --------------------------- # specials S = np.array([0, 1.5]) delta = np.sqrt(2 * S) for m in [2, 7]: equal( fco.direct0mm1(m, S)**2, fco.direct(1, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm1(m, S), fcr.ov0mm1(m, delta), 1.e-15) equal(fcr.ov0mm1(m, delta), fcr.ov0m(m, delta) * fcr.ov1m(m, delta), 1.e-15) equal(fcr.ov0mm1(m, -delta), fcr.direct0mm1(m, -delta), 1.e-15) equal(fcr.ov0mm1(m, delta), -fcr.direct0mm1(m, -delta), 1.e-15) equal( fco.direct0mm2(m, S)**2, fco.direct(2, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm2(m, S), fcr.ov0mm2(m, delta), 1.e-15) equal(fcr.ov0mm2(m, delta), fcr.ov0m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fco.direct0mm2(m, S), fcr.direct0mm2(m, delta), 1.e-15) equal(fcr.direct0mm3(m, delta), fcr.ov0m(m, delta) * fcr.ov3m(m, delta), 1.e-15) equal(fcr.ov1mm2(m, delta), fcr.ov1m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fcr.direct1mm2(m, delta), fcr.ov1mm2(m, delta), 1.e-15)
def test_franck_condon(): import sys import numpy as np from ase.vibrations.franck_condon import FranckCondonOverlap, FranckCondonRecursive from math import factorial def equal(x, y, tolerance=0, fail=True, msg=''): """Compare x and y.""" if not np.isfinite(x - y).any() or (np.abs(x - y) > tolerance).any(): msg = (msg + '%s != %s (error: |%s| > %.9g)' % (x, y, x - y, tolerance)) if fail: raise AssertionError(msg) else: sys.stderr.write('WARNING: %s\n' % msg) # FCOverlap fco = FranckCondonOverlap() fcr = FranckCondonRecursive() # check factorial assert(fco.factorial(8) == factorial(8)) # the second test is useful according to the implementation assert(fco.factorial(5) == factorial(5)) assert(fco.factorial.inv(5) == 1. / factorial(5)) # check T=0 and n=0 equality S = np.array([1, 2.1, 34]) m = 5 assert(((fco.directT0(m, S) - fco.direct(0, m, S)) / fco.directT0(m, S) < 1e-15).all()) # check symmetry S = 2 n = 3 assert(fco.direct(n, m, S) == fco.direct(m, n, S)) # --------------------------- # specials S = np.array([0, 1.5]) delta = np.sqrt(2 * S) for m in [2, 7]: equal(fco.direct0mm1(m, S)**2, fco.direct(1, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm1(m, S), fcr.ov0mm1(m, delta), 1.e-15) equal(fcr.ov0mm1(m, delta), fcr.ov0m(m, delta) * fcr.ov1m(m, delta), 1.e-15) equal(fcr.ov0mm1(m, -delta), fcr.direct0mm1(m, -delta), 1.e-15) equal(fcr.ov0mm1(m, delta), - fcr.direct0mm1(m, -delta), 1.e-15) equal(fco.direct0mm2(m, S)**2, fco.direct(2, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm2(m, S), fcr.ov0mm2(m, delta), 1.e-15) equal(fcr.ov0mm2(m, delta), fcr.ov0m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fco.direct0mm2(m, S), fcr.direct0mm2(m, delta), 1.e-15) equal(fcr.direct0mm3(m, delta), fcr.ov0m(m, delta) * fcr.ov3m(m, delta), 1.e-15) equal(fcr.ov1mm2(m, delta), fcr.ov1m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fcr.direct1mm2(m, delta), fcr.ov1mm2(m, delta), 1.e-15)
class Albrecht(ResonantRaman): def __init__(self, *args, **kwargs): """ Parameters ---------- all from ResonantRaman.__init__ combinations: int Combinations to consider for multiple excitations. Default is 1, possible 2 skip: int Number of first transitions to exclude. Default 0, recommended: 5 for linear molecules, 6 for other molecules nm: int Number of intermediate m levels to consider, default 20 """ self.combinations = kwargs.pop('combinations', 1) self.skip = kwargs.pop('skip', 0) self.nm = kwargs.pop('nm', 20) approximation = kwargs.pop('approximation', 'Albrecht') ResonantRaman.__init__(self, *args, **kwargs) self.set_approximation(approximation) def set_approximation(self, value): approx = value.lower() if approx in ['albrecht', 'albrecht b', 'albrecht c', 'albrecht bc']: if not self.overlap: raise ValueError('Overlaps are needed') elif not approx == 'albrecht a': raise ValueError('Please use "Albrecht" or "Albrecht A/B/C/BC"') self._approx = value def calculate_energies_and_modes(self): if hasattr(self, 'im_r'): return ResonantRaman.calculate_energies_and_modes(self) # single transitions and their occupation om_Q = self.om_Q[self.skip:] om_v = om_Q ndof = len(om_Q) n_vQ = np.eye(ndof, dtype=int) l_Q = range(ndof) ind_v = list(combinations_with_replacement(l_Q, 1)) if self.combinations > 1: if not self.combinations == 2: raise NotImplementedError for c in range(2, self.combinations + 1): ind_v += list(combinations_with_replacement(l_Q, c)) nv = len(ind_v) n_vQ = np.zeros((nv, ndof), dtype=int) om_v = np.zeros((nv), dtype=float) for j, wt in enumerate(ind_v): for i in wt: n_vQ[j, i] += 1 om_v = n_vQ.dot(om_Q) self.ind_v = ind_v self.om_v = om_v self.n_vQ = n_vQ # how many of each self.d_vQ = np.where(n_vQ > 0, 1, 0) # do we have them ? def get_energies(self): self.calculate_energies_and_modes() return self.om_v def _collect_r(self, arr_ro, oshape, dtype): """Collect an array that is distributed.""" if len(self.myr) == self.ndof: # serial return arr_ro data_ro = np.zeros([self.ndof] + oshape, dtype) if len(arr_ro): data_ro[self.slize] = arr_ro self.comm.sum(data_ro) return data_ro def Huang_Rhys_factors(self, forces_r): """Evaluate Huang-Rhys factors derived from forces.""" return 0.5 * self.unitless_displacements(forces_r)**2 def unitless_displacements(self, forces_r, mineigv=1e-12): """Evaluate unitless displacements from forces Parameters ---------- forces_r: array Forces in cartesian coordinates mineigv: float Minimal Eigenvalue to consider in matrix inversion to handle numerical noise. Default 1e-12 Returns ------- Unitless displacements in Eigenmode coordinates """ assert (len(forces_r.flat) == self.ndof) if not hasattr(self, 'Dm1_q'): self.eigv_q, self.eigw_rq = np.linalg.eigh(self.im_r[:, None] * self.H * self.im_r) # there might be zero or nearly zero eigenvalues self.Dm1_q = np.divide(1, self.eigv_q, out=np.zeros_like(self.eigv_q), where=np.abs(self.eigv_q) > mineigv) X_r = self.eigw_rq @ np.diag( self.Dm1_q) @ self.eigw_rq.T @ (forces_r.flat * self.im_r) d_Q = np.dot(self.modes_Qq, X_r) s = 1.e-20 / u.kg / u.C / u._hbar**2 d_Q *= np.sqrt(s * self.om_Q) return d_Q def omegaLS(self, omega, gamma): omL = omega + 1j * gamma omS_Q = omL - self.om_Q return omL, omS_Q def init_parallel_excitations(self): """Init for paralellization over excitations.""" n_p = len(self.ex0E_p) # collect excited state forces exF_pr = self._collect_r(self.exF_rp, [n_p], self.ex0E_p.dtype).T # select your work load myn = -(-n_p // self.comm.size) # ceil divide rank = self.comm.rank s = slice(myn * rank, myn * (rank + 1)) return n_p, range(n_p)[s], exF_pr def meA(self, omega, gamma=0.1): """Evaluate Albrecht A term. Returns ------- Full Albrecht A matrix element. Unit: e^2 Angstrom^2 / eV """ self.read() if not hasattr(self, 'fcr'): self.fcr = FranckCondonRecursive() omL = omega + 1j * gamma omS_Q = omL - self.om_Q n_p, myp, exF_pr = self.init_parallel_excitations() exF_pr = np.where(np.abs(exF_pr) > 1e-2, exF_pr, 0) m_Qcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p in myp: energy = self.ex0E_p[p] d_Q = self.unitless_displacements(exF_pr[p]) energy_Q = energy - self.om_Q * d_Q**2 / 2. me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) wm_Q = np.zeros((self.ndof), dtype=complex) wp_Q = np.zeros((self.ndof), dtype=complex) for m in range(self.nm): fco_Q = self.fcr.direct0mm1(m, d_Q) e_Q = energy_Q + m * self.om_Q wm_Q += fco_Q / (e_Q - omL) wp_Q += fco_Q / (e_Q + omS_Q) m_Qcc += np.einsum('a,bc->abc', wm_Q, me_cc) m_Qcc += np.einsum('a,bc->abc', wp_Q, me_cc.conj()) self.comm.sum(m_Qcc) return m_Qcc # e^2 Angstrom^2 / eV def meAmult(self, omega, gamma=0.1): """Evaluate Albrecht A term. Returns ------- Full Albrecht A matrix element. Unit: e^2 Angstrom^2 / eV """ self.read() if not hasattr(self, 'fcr'): self.fcr = FranckCondonRecursive() omL = omega + 1j * gamma omS_v = omL - self.om_v nv = len(self.om_v) om_Q = self.om_Q[self.skip:] nQ = len(om_Q) # n_v: # how many FC factors are involved # nvib_ov: # delta functions to switch contributions depending on order o # ind_ov: # Q indicees # n_ov: # # of vibrational excitations n_v = self.d_vQ.sum(axis=1) # multiplicity nvib_ov = np.empty((self.combinations, nv), dtype=int) om_ov = np.zeros((self.combinations, nv), dtype=float) n_ov = np.zeros((self.combinations, nv), dtype=int) d_ovQ = np.zeros((self.combinations, nv, nQ), dtype=int) for o in range(self.combinations): nvib_ov[o] = np.array(n_v == (o + 1)) for v in range(nv): try: om_ov[o, v] = om_Q[self.ind_v[v][o]] d_ovQ[o, v, self.ind_v[v][o]] = 1 except IndexError: pass # XXXX change ???? n_ov[0] = self.n_vQ.max(axis=1) n_ov[1] = nvib_ov[1] n_p, myp, exF_pr = self.init_parallel_excitations() m_vcc = np.zeros((nv, 3, 3), dtype=complex) for p in myp: energy = self.ex0E_p[p] d_Q = self.unitless_displacements(exF_pr[p])[self.skip:] S_Q = d_Q**2 / 2. energy_v = energy - self.d_vQ.dot(om_Q * S_Q) me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) fco1_mQ = np.empty((self.nm, nQ), dtype=float) fco2_mQ = np.empty((self.nm, nQ), dtype=float) for m in range(self.nm): fco1_mQ[m] = self.fcr.direct0mm1(m, d_Q) fco2_mQ[m] = self.fcr.direct0mm2(m, d_Q) wm_v = np.zeros((nv), dtype=complex) wp_v = np.zeros((nv), dtype=complex) for m in range(self.nm): fco1_v = np.where(n_ov[0] == 2, d_ovQ[0].dot(fco2_mQ[m]), d_ovQ[0].dot(fco1_mQ[m])) em_v = energy_v + m * om_ov[0] # multiples of same kind fco_v = nvib_ov[0] * fco1_v wm_v += fco_v / (em_v - omL) wp_v += fco_v / (em_v + omS_v) if nvib_ov[1].any(): # multiples of mixed type for n in range(self.nm): fco2_v = d_ovQ[1].dot(fco1_mQ[n]) e_v = em_v + n * om_ov[1] fco_v = nvib_ov[1] * fco1_v * fco2_v wm_v += fco_v / (e_v - omL) wp_v += fco_v / (e_v + omS_v) m_vcc += np.einsum('a,bc->abc', wm_v, me_cc) m_vcc += np.einsum('a,bc->abc', wp_v, me_cc.conj()) self.comm.sum(m_vcc) return m_vcc # e^2 Angstrom^2 / eV def meBC(self, omega, gamma=0.1, term='BC'): """Evaluate Albrecht BC term. Returns ------- Full Albrecht BC matrix element. Unit: e^2 Angstrom / eV / sqrt(amu) """ self.read() if not hasattr(self, 'fco'): self.fco = FranckCondonOverlap() omL = omega + 1j * gamma omS_Q = omL - self.om_Q # excited state forces n_p, myp, exF_pr = self.init_parallel_excitations() # derivatives after normal coordinates exdmdr_rpc = self._collect_r(self.exdmdr_rpc, [n_p, 3], self.ex0m_pc.dtype) dmdq_qpc = (exdmdr_rpc.T * self.im_r).T # unit e / sqrt(amu) dmdQ_Qpc = np.dot(dmdq_qpc.T, self.modes_Qq.T).T # unit e / sqrt(amu) me_Qcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p in myp: energy = self.ex0E_p[p] S_Q = self.Huang_Rhys_factors(exF_pr[p]) # relaxed excited state energy # # n_vQ = np.where(self.n_vQ > 0, 1, 0) # # energy_v = energy - n_vQ.dot(self.om_Q * S_Q) energy_Q = energy - self.om_Q * S_Q # # me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) m_c = self.ex0m_pc[p] # e Angstrom dmdQ_Qc = dmdQ_Qpc[:, p] # e / sqrt(amu) wBLS_Q = np.zeros((self.ndof), dtype=complex) wBSL_Q = np.zeros((self.ndof), dtype=complex) wCLS_Q = np.zeros((self.ndof), dtype=complex) wCSL_Q = np.zeros((self.ndof), dtype=complex) for m in range(self.nm): f0mmQ1_Q = (self.fco.directT0(m, S_Q) + np.sqrt(2) * self.fco.direct0mm2(m, S_Q)) f0Qmm1_Q = self.fco.direct(1, m, S_Q) em_Q = energy_Q + m * self.om_Q wBLS_Q += f0mmQ1_Q / (em_Q - omL) wBSL_Q += f0Qmm1_Q / (em_Q - omL) wCLS_Q += f0mmQ1_Q / (em_Q + omS_Q) wCSL_Q += f0Qmm1_Q / (em_Q + omS_Q) # unit e^2 Angstrom / sqrt(amu) mdmdQ_Qcc = np.einsum('a,bc->bac', m_c, dmdQ_Qc.conj()) dmdQm_Qcc = np.einsum('ab,c->abc', dmdQ_Qc, m_c.conj()) if 'B' in term: me_Qcc += np.multiply(wBLS_Q, mdmdQ_Qcc.T).T me_Qcc += np.multiply(wBSL_Q, dmdQm_Qcc.T).T if 'C' in term: me_Qcc += np.multiply(wCLS_Q, mdmdQ_Qcc.T).T me_Qcc += np.multiply(wCSL_Q, dmdQm_Qcc.T).T self.comm.sum(me_Qcc) return me_Qcc # unit e^2 Angstrom / eV / sqrt(amu) def electronic_me_Qcc(self, omega, gamma): self.calculate_energies_and_modes() approx = self.approximation.lower() assert (self.combinations == 1) Vel_Qcc = np.zeros((len(self.om_Q), 3, 3), dtype=complex) if approx == 'albrecht a' or approx == 'albrecht': Vel_Qcc += self.meA(omega, gamma) # e^2 Angstrom^2 / eV # divide through pre-factor with np.errstate(divide='ignore'): Vel_Qcc *= np.where(self.vib01_Q > 0, 1. / self.vib01_Q, 0)[:, None, None] # -> e^2 Angstrom / eV / sqrt(amu) if approx == 'albrecht bc' or approx == 'albrecht': Vel_Qcc += self.meBC(omega, gamma) # e^2 Angstrom / eV / sqrt(amu) if approx == 'albrecht b': Vel_Qcc += self.meBC(omega, gamma, term='B') if approx == 'albrecht c': Vel_Qcc = self.meBC(omega, gamma, term='C') Vel_Qcc *= u.Hartree * u.Bohr # e^2 Angstrom^2 / eV -> Angstrom^3 return Vel_Qcc # Angstrom^2 / sqrt(amu) def me_Qcc(self, omega, gamma): """Full matrix element""" self.read() approx = self.approximation.lower() nv = len(self.om_v) V_vcc = np.zeros((nv, 3, 3), dtype=complex) if approx == 'albrecht a' or approx == 'albrecht': if self.combinations == 1: # e^2 Angstrom^2 / eV V_vcc += self.meA(omega, gamma)[self.skip:] else: V_vcc += self.meAmult(omega, gamma) if approx == 'albrecht bc' or approx == 'albrecht': if self.combinations == 1: vel_vcc = self.meBC(omega, gamma) V_vcc += vel_vcc * self.vib01_Q[:, None, None] else: vel_vcc = self.meBCmult(omega, gamma) V_vcc = 0 elif approx == 'albrecht b': assert (self.combinations == 1) vel_vcc = self.meBC(omega, gamma, term='B') V_vcc = vel_vcc * self.vib01_Q[:, None, None] if approx == 'albrecht c': assert (self.combinations == 1) vel_vcc = self.meBC(omega, gamma, term='C') V_vcc = vel_vcc * self.vib01_Q[:, None, None] return V_vcc # e^2 Angstrom^2 / eV def summary(self, omega=0, gamma=0, method='standard', direction='central', log=sys.stdout): """Print summary for given omega [eV]""" if self.combinations > 1: return self.extended_summary() om_v = self.get_energies() intensities = self.get_absolute_intensities(omega, gamma)[self.skip:] 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(' approximation:', self.approximation, file=log) parprint(' Mode Frequency Intensity', file=log) parprint(' # meV cm^-1 [A^4/amu]', file=log) parprint('-------------------------------------', file=log) for n, e in enumerate(om_v): if e.imag != 0: c = 'i' e = e.imag else: c = ' ' e = e.real parprint('%3d %6.1f %7.1f%s %9.1f' % (n, 1000 * e, e / u.invcm, c, intensities[n]), file=log) parprint('-------------------------------------', file=log) parprint('Zero-point energy: %.3f eV' % self.vibrations.get_zero_point_energy(), file=log) def extended_summary(self, omega=0, gamma=0, method='standard', direction='central', log=sys.stdout): """Print summary for given omega [eV]""" self.read(method, direction) om_v = self.get_energies() intens_v = self.intensity(omega, gamma) 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(' approximation:', self.approximation, file=log) parprint(' observation:', self.observation, file=log) parprint(' Mode Frequency Intensity', file=log) parprint(' # meV cm^-1 [e^4A^4/eV^2]', file=log) parprint('-------------------------------------', file=log) for v, e in enumerate(om_v): parprint(self.ind_v[v], '{0:6.1f} {1:7.1f} {2:9.1f}'.format( 1000 * e, e / u.invcm, 1e9 * intens_v[v]), file=log) parprint('-------------------------------------', file=log) parprint('Zero-point energy: %.3f eV' % self.vibrations.get_zero_point_energy(), file=log)
assert (fco.direct(n, m, S) == fco.direct(m, n, S)) # --------------------------- # specials S = np.array([0, 1.5]) delta = np.sqrt(2 * S) for m in [2, 7]: equal( fco.direct0mm1(m, S)**2, fco.direct(1, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm1(m, S), fcr.ov0mm1(m, delta), 1.e-15) equal(fcr.ov0mm1(m, delta), fcr.ov0m(m, delta) * fcr.ov1m(m, delta), 1.e-15) equal(fcr.ov0mm1(m, -delta), fcr.direct0mm1(m, -delta), 1.e-15) equal(fcr.ov0mm1(m, delta), -fcr.direct0mm1(m, -delta), 1.e-15) equal( fco.direct0mm2(m, S)**2, fco.direct(2, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm2(m, S), fcr.ov0mm2(m, delta), 1.e-15) equal(fcr.ov0mm2(m, delta), fcr.ov0m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fco.direct0mm2(m, S), fcr.direct0mm2(m, delta), 1.e-15) equal(fcr.direct0mm3(m, delta), fcr.ov0m(m, delta) * fcr.ov3m(m, delta), 1.e-15) equal(fcr.ov1mm2(m, delta), fcr.ov1m(m, delta) * fcr.ov2m(m, delta), 1.e-15) equal(fcr.direct1mm2(m, delta), fcr.ov1mm2(m, delta), 1.e-15)
class ResonantRaman(Vibrations): """Class for calculating vibrational modes and resonant Raman intensities using finite difference. atoms: Atoms object Excitations: Class to calculate the excitations. The class object is initialized as:: Excitations(atoms.get_calculator()) 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 dipole matrix element in |e| * Angstrom ex.energy: is the transition energy in Hartrees """ def __init__( self, atoms, Excitations, indices=None, gsname='rraman', # name for ground state calculations exname=None, # name for excited state calculations delta=0.01, nfree=2, directions=None, approximation='Profeta', observation={'geometry': '-Z(XX)Z'}, exkwargs={}, # kwargs to be passed to Excitations exext='.ex.gz', # extension for Excitation names txt='-', verbose=False, ): assert (nfree == 2) Vibrations.__init__(self, atoms, indices, gsname, delta, nfree) self.name = gsname + '-d%.3f' % delta if exname is None: exname = gsname self.exname = exname + '-d%.3f' % delta self.exext = exext if directions is None: self.directions = np.array([0, 1, 2]) else: self.directions = np.array(directions) self.approximation = approximation self.observation = observation self.exobj = Excitations self.exkwargs = exkwargs self.timer = Timer() self.txt = convert_string_to_fd(txt) self.verbose = verbose @staticmethod def m2(z): return (z * z.conj()).real def log(self, message, pre='# ', end='\n'): if self.verbose: self.txt.write(pre + message + end) self.txt.flush() def calculate(self, filename, fd): """Call ground and excited state calculation""" self.timer.start('Ground state') forces = self.atoms.get_forces() if rank == 0: pickle.dump(forces, fd, protocol=2) fd.close() self.timer.stop('Ground state') self.timer.start('Excitations') basename, _ = os.path.splitext(filename) excitations = self.exobj(self.atoms.get_calculator(), **self.exkwargs) excitations.write(basename + self.exext) self.timer.stop('Excitations') def read_excitations(self): self.timer.start('read excitations') self.timer.start('really read') self.log('reading ' + self.exname + '.eq' + self.exext) ex0_object = self.exobj(self.exname + '.eq' + self.exext, **self.exkwargs) self.timer.stop('really read') self.timer.start('index') matching = frozenset(ex0_object) self.timer.stop('index') def append(lst, exname, matching): self.timer.start('really read') self.log('reading ' + exname, end=' ') exo = self.exobj(exname, **self.exkwargs) lst.append(exo) self.timer.stop('really read') self.timer.start('index') matching = matching.intersection(exo) self.log('len={0}, matching={1}'.format(len(exo), len(matching)), pre='') self.timer.stop('index') return matching exm_object_list = [] exp_object_list = [] for a in self.indices: for i in 'xyz': name = '%s.%d%s' % (self.exname, a, i) matching = append(exm_object_list, name + '-' + self.exext, matching) matching = append(exp_object_list, name + '+' + self.exext, matching) self.ndof = 3 * len(self.indices) self.nex = len(matching) self.timer.stop('read excitations') self.timer.start('select') 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 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.timer.stop('select') self.timer.start('me and energy') eu = units.Hartree self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) self.ex0m_pc = np.array([ex.get_dipole_me(form='v') for ex in ex0]) exmE_rp = [] expE_rp = [] exF_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]]) exF_rp.append([(ep.energy - em.energy) for ep, em in zip(exp[r], exm[r])]) exmm_rpc.append([ex.get_dipole_me(form='v') for ex in exm[r]]) expm_rpc.append([ex.get_dipole_me(form='v') 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(exF_rp) * eu / 2 / self.delta self.exmm_rpc = np.array(exmm_rpc) self.expm_rpc = np.array(expm_rpc) self.timer.stop('me and energy') def read(self, method='standard', direction='central'): """Read data from a pre-performed calculation.""" if not hasattr(self, 'modes'): self.timer.start('read vibrations') Vibrations.read(self, method, direction) # we now have: # self.H : Hessian matrix # self.im : 1./sqrt(masses) # self.modes : Eigenmodes of the mass weighted H self.om_r = self.hnu.real # energies in eV self.timer.stop('read vibrations') if not hasattr(self, 'ex0E_p'): self.read_excitations() def get_Huang_Rhys_factors(self, forces_r): """Evaluate Huang-Rhys factors derived from forces.""" self.timer.start('Huang-Rhys') assert (len(forces_r.flat) == self.ndof) # solve the matrix equation for the equilibrium displacements # XXX why are the forces mass weighted ??? X_r = np.linalg.solve(self.im[:, None] * self.H * self.im, forces_r.flat * self.im) d_r = np.dot(self.modes, X_r) # Huang-Rhys factors S s = 1.e-20 / units.kg / units.C / units._hbar**2 # SI units self.timer.stop('Huang-Rhys') return s * d_r**2 * self.om_r / 2. def get_matrix_element_AlbrechtA(self, omega, gamma=0.1, ml=range(16)): """Evaluate Albrecht A term. Unit: |e|^2Angstrom^2/eV """ self.read() self.timer.start('AlbrechtA') if not hasattr(self, 'fco'): self.fco = FranckCondonOverlap() # excited state forces F_pr = self.exF_rp.T m_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p, energy in enumerate(self.ex0E_p): S_r = self.get_Huang_Rhys_factors(F_pr[p]) me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) for m in ml: self.timer.start('0mm1') fco_r = self.fco.direct0mm1(m, S_r) self.timer.stop('0mm1') self.timer.start('einsum') m_rcc += np.einsum( 'a,bc->abc', fco_r / (energy + m * self.om_r - omega - 1j * gamma), me_cc) m_rcc += np.einsum( 'a,bc->abc', fco_r / (energy + (m - 1) * self.om_r + omega + 1j * gamma), me_cc) self.timer.stop('einsum') self.timer.stop('AlbrechtA') return m_rcc def get_matrix_element_AlbrechtBC(self, omega, gamma=0.1, ml=[1], term='BC'): """Evaluate Albrecht B and/or C term(s).""" self.read() self.timer.start('AlbrechtBC') if not hasattr(self, 'fco'): self.fco = FranckCondonOverlap() # excited state forces F_pr = self.exF_rp.T m_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p, energy in enumerate(self.ex0E_p): S_r = self.get_Huang_Rhys_factors(F_pr[p]) for m in ml: self.timer.start('Franck-Condon overlaps') fc1mm1_r = self.fco.direct(1, m, S_r) fc0mm02_r = self.fco.direct(0, m, S_r) fc0mm02_r += np.sqrt(2) * self.fco.direct0mm2(m, S_r) # XXXXX fc1mm1_r[-1] = 1 fc0mm02_r[-1] = 1 print(m, fc1mm1_r[-1], fc0mm02_r[-1]) self.timer.stop('Franck-Condon overlaps') self.timer.start('me dervivatives') dm_rc = [] r = 0 for a in self.indices: for i in 'xyz': dm_rc.append( (self.expm_rpc[r, p] - self.exmm_rpc[r, p]) * self.im[r]) print('pm=', self.expm_rpc[r, p], self.exmm_rpc[r, p]) r += 1 dm_rc = np.array(dm_rc) / (2 * self.delta) self.timer.stop('me dervivatives') self.timer.start('map to modes') # print('dm_rc[2], dm_rc[5]', dm_rc[2], dm_rc[5]) print('dm_rc=', dm_rc) dm_rc = np.dot(dm_rc.T, self.modes.T).T print('dm_rc[-1][2]', dm_rc[-1][2]) self.timer.stop('map to modes') self.timer.start('multiply') # me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) for r in range(self.ndof): if 'B' in term: # XXXX denom = (1. / (energy + m * 0 * self.om_r[r] - omega - 1j * gamma)) # ok print('denom=', denom) m_rcc[r] += ( np.outer(dm_rc[r], self.ex0m_pc[p].conj()) * fc1mm1_r[r] * denom) if r == 5: print('m_rcc[r]=', m_rcc[r][2, 2]) m_rcc[r] += ( np.outer(self.ex0m_pc[p], dm_rc[r].conj()) * fc0mm02_r[r] * denom) if 'C' in term: denom = (1. / (energy + (m - 1) * self.om_r[r] + omega + 1j * gamma)) m_rcc[r] += ( np.outer(self.ex0m_pc[p], dm_rc[r].conj()) * fc1mm1_r[r] * denom) m_rcc[r] += ( np.outer(dm_rc[r], self.ex0m_pc[p].conj()) * fc0mm02_r[r] * denom) self.timer.stop('multiply') print('m_rcc[-1]=', m_rcc[-1][2, 2]) self.timer.start('pre_r') with np.errstate(divide='ignore'): pre_r = np.where(self.om_r > 0, np.sqrt(units._hbar**2 / 2. / self.om_r), 0) # print('BC: pre_r=', pre_r) for r, p in enumerate(pre_r): m_rcc[r] *= p self.timer.stop('pre_r') self.timer.stop('AlbrechtBC') return m_rcc def get_matrix_element_Profeta(self, omega, gamma=0.1, energy_derivative=False): """Evaluate Albrecht B+C term in Profeta and Mauri approximation""" self.read() self.timer.start('amplitudes') self.timer.start('init') V_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) pre = 1. / (2 * self.delta) self.timer.stop('init') def kappa(me_pc, e_p, omega, gamma, form='v'): """Kappa tensor after Profeta and Mauri PRB 63 (2001) 245415""" me_ccp = np.empty((3, 3, len(e_p)), dtype=complex) for p, me_c in enumerate(me_pc): me_ccp[:, :, p] = np.outer(me_pc[p], me_pc[p].conj()) # print('kappa: me_ccp=', me_ccp[2,2,0]) # ok print('kappa: den=', 1./(e_p - omega - 1j * gamma)) kappa_ccp = (me_ccp / (e_p - omega - 1j * gamma) + me_ccp.conj() / (e_p + omega + 1j * gamma)) return kappa_ccp.sum(2) self.timer.start('kappa') r = 0 for a in self.indices: for i in 'xyz': if not energy_derivative < 0: V_rcc[r] = pre * self.im[r] * ( kappa(self.expm_rpc[r], self.ex0E_p, omega, gamma) - kappa(self.exmm_rpc[r], self.ex0E_p, omega, gamma)) if energy_derivative: V_rcc[r] += pre * self.im[r] * ( kappa(self.ex0m_pc, self.expE_rp[r], omega, gamma) - kappa(self.ex0m_pc, self.exmE_rp[r], omega, gamma)) r += 1 self.timer.stop('kappa') # print('V_rcc[2], V_rcc[5]=', V_rcc[2,2,2], V_rcc[5,2,2]) self.timer.stop('amplitudes') # map to modes self.timer.start('pre_r') with np.errstate(divide='ignore'): pre_r = np.where(self.om_r > 0, np.sqrt(units._hbar**2 / 2. / self.om_r), 0) V_rcc = np.dot(V_rcc.T, self.modes.T).T # looks ok print('self.modes.T[-1]',self.modes.T) # looks ok print('V_rcc[-1]=', V_rcc[-1][2,2]) # ok print('Profeta: pre_r=', pre_r) for r, p in enumerate(pre_r): V_rcc[r] *= p self.timer.stop('pre_r') return V_rcc def get_matrix_element(self, omega, gamma): self.read() V_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) if self.approximation.lower() == 'profeta': V_rcc += self.get_matrix_element_Profeta(omega, gamma) elif self.approximation.lower() == 'placzek': V_rcc += self.get_matrix_element_Profeta(omega, gamma, True) elif self.approximation.lower() == 'p-p': V_rcc += self.get_matrix_element_Profeta(omega, gamma, -1) elif self.approximation.lower() == 'albrecht a': V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) elif self.approximation.lower() == 'albrecht b': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma, term='B') elif self.approximation.lower() == 'albrecht c': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma, term='C') elif self.approximation.lower() == 'albrecht bc': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma) elif self.approximation.lower() == 'albrecht': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma) elif self.approximation.lower() == 'albrecht+profeta': V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) V_rcc += self.get_matrix_element_Profeta(omega, gamma) else: raise NotImplementedError( 'Approximation {0} not implemented. '.format( self.approximation) + 'Please use "Profeta", "Albrecht A/B/C/BC", ' + 'or "Albrecht".') return V_rcc def get_intensities(self, omega, gamma=0.1): m2 = ResonantRaman.m2 alpha_rcc = self.get_matrix_element(omega, gamma) if not self.observation: # XXXX remove """Simple sum, maybe too simple""" return m2(alpha_rcc).sum(axis=1).sum(axis=1) # XXX enable when appropraiate # if self.observation['orientation'].lower() != 'random': # raise NotImplementedError('not yet') # random orientation of the molecular frame # Woodward & Long, # Guthmuller, J. J. Chem. Phys. 2016, 144 (6), 64106 m2 = ResonantRaman.m2 alpha2_r = m2(alpha_rcc[:, 0, 0] + alpha_rcc[:, 1, 1] + alpha_rcc[:, 2, 2]) / 9. delta2_r = 3 / 4. * (m2(alpha_rcc[:, 0, 1] - alpha_rcc[:, 1, 0]) + m2(alpha_rcc[:, 0, 2] - alpha_rcc[:, 2, 0]) + m2(alpha_rcc[:, 1, 2] - alpha_rcc[:, 2, 1])) gamma2_r = (3 / 4. * (m2(alpha_rcc[:, 0, 1] + alpha_rcc[:, 1, 0]) + m2(alpha_rcc[:, 0, 2] + alpha_rcc[:, 2, 0]) + m2(alpha_rcc[:, 1, 2] + alpha_rcc[:, 2, 1])) + (m2(alpha_rcc[:, 0, 0] - alpha_rcc[:, 1, 1]) + m2(alpha_rcc[:, 0, 0] - alpha_rcc[:, 2, 2]) + m2(alpha_rcc[:, 1, 1] - alpha_rcc[:, 2, 2])) / 2) if self.observation['geometry'] == '-Z(XX)Z': # Porto's notation return (45 * alpha2_r + 5 * delta2_r + 4 * gamma2_r) / 45. elif self.observation['geometry'] == '-Z(XY)Z': # Porto's notation return gamma2_r / 15. elif self.observation['scattered'] == 'Z': # scattered light in direction of incoming light return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45. elif self.observation['scattered'] == 'parallel': # scattered light perendicular and # polarization in plane return 6 * gamma2_r / 45. elif self.observation['scattered'] == 'perpendicular': # scattered light perendicular and # polarization out of plane return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45. else: raise NotImplementedError def get_cross_sections(self, omega, gamma=0.1): I_r = self.get_intensities(omega, gamma) pre = 1. / 16 / np.pi**2 / units.eps0**2 / units.c**4 # frequency of scattered light omS_r = omega - self.hnu return pre * omega * omS_r**3 * I_r def get_spectrum(self, omega, gamma=0.1, start=200.0, end=4000.0, npts=None, width=4.0, type='Gaussian', method='standard', direction='central', 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'] if not npts: npts = int((end - start) / width * 10 + 1) frequencies = self.get_frequencies(method, direction).real intensities = self.get_cross_sections(omega, gamma) prefactor = 1 if 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 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', method='standard', direction='central'): """Write out spectrum to file. First column is the wavenumber in cm^-1, the second column the absolute infrared intensities, and the third column the absorbance scaled so that data runs from 1 to 0. 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, method, direction) # Write out spectrum in file. First column is absolute intensities. outdata = np.empty([len(energies), 3]) outdata.T[0] = energies outdata.T[1] = spectrum fd = open(out, 'w') fd.write('# Resonant Raman spectrum\n') fd.write('# omega={0:g} eV, gamma={1:g} eV\n'.format(omega, gamma)) 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])) fd.close() def summary(self, omega, gamma=0.1, method='standard', direction='central', log=sys.stdout): """Print summary for given omega [eV]""" hnu = self.get_energies(method, direction) s = 0.01 * units._e / units._c / units._hplanck intensities = self.get_intensities(omega, gamma) if isinstance(log, basestring): log = paropen(log, 'a') parprint('-------------------------------------', file=log) parprint(' excitation at ' + str(omega) + ' eV', file=log) parprint(' gamma ' + str(gamma) + ' eV', file=log) parprint(' approximation:', self.approximation, file=log) parprint(' observation:', self.observation, '\n', file=log) parprint(' Mode Frequency Intensity', file=log) parprint(' # meV cm^-1 [e^4A^4/eV^2]', 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.3g' % (n, 1000 * e, c, s * e, c, intensities[n]), file=log) parprint('-------------------------------------', file=log) parprint('Zero-point energy: %.3f eV' % self.get_zero_point_energy(), file=log) def __del__(self): self.timer.write(self.txt)
class ResonantRaman(Vibrations): """Class for calculating vibrational modes and resonant Raman intensities using finite difference. atoms: Atoms object Excitations: Class to calculate the excitations. The class object is initialized as:: Excitations(atoms.get_calculator()) 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 dipole matrix element in |e| * Angstrom ex.energy: is the transition energy in Hartrees """ def __init__(self, atoms, Excitations, indices=None, gsname='rraman', # name for ground state calculations exname=None, # name for excited state calculations delta=0.01, nfree=2, directions=None, approximation='Profeta', observation={'geometry': '-Z(XX)Z'}, exkwargs={}, # kwargs to be passed to Excitations exext='.ex.gz', # extension for Excitation names txt='-', verbose=False,): assert(nfree == 2) Vibrations.__init__(self, atoms, indices, gsname, delta, nfree) self.name = gsname + '-d%.3f' % delta if exname is None: exname = gsname self.exname = exname + '-d%.3f' % delta self.exext = exext if directions is None: self.directions = np.array([0, 1, 2]) else: self.directions = np.array(directions) self.approximation = approximation self.observation = observation self.exobj = Excitations self.exkwargs = exkwargs self.timer = Timer() self.txt = convert_string_to_fd(txt) self.verbose = verbose @staticmethod def m2(z): return (z * z.conj()).real def log(self, message, pre='# ', end='\n'): if self.verbose: self.txt.write(pre + message + end) self.txt.flush() def calculate(self, filename, fd): """Call ground and excited state calculation""" self.timer.start('Ground state') forces = self.atoms.get_forces() if rank == 0: pickle.dump(forces, fd, protocol=2) fd.close() self.timer.stop('Ground state') self.timer.start('Excitations') basename, _ = os.path.splitext(filename) excitations = self.exobj( self.atoms.get_calculator(), **self.exkwargs) excitations.write(basename + self.exext) self.timer.stop('Excitations') def read_excitations(self): self.timer.start('read excitations') self.timer.start('really read') self.log('reading ' + self.exname + '.eq' + self.exext) ex0_object = self.exobj(self.exname + '.eq' + self.exext, **self.exkwargs) self.timer.stop('really read') self.timer.start('index') matching = frozenset(ex0_object) self.timer.stop('index') def append(lst, exname, matching): self.timer.start('really read') self.log('reading ' + exname, end=' ') exo = self.exobj(exname, **self.exkwargs) lst.append(exo) self.timer.stop('really read') self.timer.start('index') matching = matching.intersection(exo) self.log('len={0}, matching={1}'.format(len(exo), len(matching)), pre='') self.timer.stop('index') return matching exm_object_list = [] exp_object_list = [] for a in self.indices: for i in 'xyz': name = '%s.%d%s' % (self.exname, a, i) matching = append(exm_object_list, name + '-' + self.exext, matching) matching = append(exp_object_list, name + '+' + self.exext, matching) self.ndof = 3 * len(self.indices) self.nex = len(matching) self.timer.stop('read excitations') self.timer.start('select') 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 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.timer.stop('select') self.timer.start('me and energy') eu = units.Hartree self.ex0E_p = np.array([ex.energy * eu for ex in ex0]) self.ex0m_pc = np.array( [ex.get_dipole_me(form='v') for ex in ex0]) exmE_rp = [] expE_rp = [] exF_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]]) exF_rp.append( [(ep.energy - em.energy) for ep, em in zip(exp[r], exm[r])]) exmm_rpc.append( [ex.get_dipole_me(form='v') for ex in exm[r]]) expm_rpc.append( [ex.get_dipole_me(form='v') 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(exF_rp) * eu / 2 / self.delta self.exmm_rpc = np.array(exmm_rpc) self.expm_rpc = np.array(expm_rpc) self.timer.stop('me and energy') def read(self, method='standard', direction='central'): """Read data from a pre-performed calculation.""" if not hasattr(self, 'modes'): self.timer.start('read vibrations') Vibrations.read(self, method, direction) # we now have: # self.H : Hessian matrix # self.im : 1./sqrt(masses) # self.modes : Eigenmodes of the mass weighted H self.om_r = self.hnu.real # energies in eV self.timer.stop('read vibrations') if not hasattr(self, 'ex0E_p'): self.read_excitations() def get_Huang_Rhys_factors(self, forces_r): """Evaluate Huang-Rhys factors derived from forces.""" self.timer.start('Huang-Rhys') assert(len(forces_r.flat) == self.ndof) # solve the matrix equation for the equilibrium displacements # XXX why are the forces mass weighted ??? X_r = np.linalg.solve(self.im[:, None] * self.H * self.im, forces_r.flat * self.im) d_r = np.dot(self.modes, X_r) # Huang-Rhys factors S s = 1.e-20 / units.kg / units.C / units._hbar**2 # SI units self.timer.stop('Huang-Rhys') return s * d_r**2 * self.om_r / 2. def get_matrix_element_AlbrechtA(self, omega, gamma=0.1, ml=range(16)): """Evaluate Albrecht A term. Unit: |e|^2Angstrom^2/eV """ self.read() self.timer.start('AlbrechtA') if not hasattr(self, 'fco'): self.fco = FranckCondonOverlap() # excited state forces F_pr = self.exF_rp.T m_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p, energy in enumerate(self.ex0E_p): S_r = self.get_Huang_Rhys_factors(F_pr[p]) me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) for m in ml: self.timer.start('0mm1') fco_r = self.fco.direct0mm1(m, S_r) self.timer.stop('0mm1') self.timer.start('einsum') m_rcc += np.einsum('a,bc->abc', fco_r / (energy + m * self.om_r - omega - 1j * gamma), me_cc) m_rcc += np.einsum('a,bc->abc', fco_r / (energy + (m - 1) * self.om_r + omega + 1j * gamma), me_cc) self.timer.stop('einsum') self.timer.stop('AlbrechtA') return m_rcc def get_matrix_element_AlbrechtBC(self, omega, gamma=0.1, ml=[1], term='BC'): """Evaluate Albrecht B and/or C term(s).""" self.read() self.timer.start('AlbrechtBC') if not hasattr(self, 'fco'): self.fco = FranckCondonOverlap() # excited state forces F_pr = self.exF_rp.T m_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) for p, energy in enumerate(self.ex0E_p): S_r = self.get_Huang_Rhys_factors(F_pr[p]) for m in ml: self.timer.start('Franck-Condon overlaps') fc1mm1_r = self.fco.direct(1, m, S_r) fc0mm02_r = self.fco.direct(0, m, S_r) fc0mm02_r += np.sqrt(2) * self.fco.direct0mm2(m, S_r) # XXXXX fc1mm1_r[-1] = 1 fc0mm02_r[-1] = 1 print(m, fc1mm1_r[-1], fc0mm02_r[-1]) self.timer.stop('Franck-Condon overlaps') self.timer.start('me dervivatives') dm_rc = [] r = 0 for a in self.indices: for i in 'xyz': dm_rc.append( (self.expm_rpc[r, p] - self.exmm_rpc[r, p]) * self.im[r]) print('pm=', self.expm_rpc[r, p], self.exmm_rpc[r, p]) r += 1 dm_rc = np.array(dm_rc) / (2 * self.delta) self.timer.stop('me dervivatives') self.timer.start('map to modes') # print('dm_rc[2], dm_rc[5]', dm_rc[2], dm_rc[5]) print('dm_rc=', dm_rc) dm_rc = np.dot(dm_rc.T, self.modes.T).T print('dm_rc[-1][2]', dm_rc[-1][2]) self.timer.stop('map to modes') self.timer.start('multiply') # me_cc = np.outer(self.ex0m_pc[p], self.ex0m_pc[p].conj()) for r in range(self.ndof): if 'B' in term: # XXXX denom = (1. / (energy + m * 0 * self.om_r[r] - omega - 1j * gamma)) # ok print('denom=', denom) m_rcc[r] += (np.outer(dm_rc[r], self.ex0m_pc[p].conj()) * fc1mm1_r[r] * denom) if r == 5: print('m_rcc[r]=', m_rcc[r][2, 2]) m_rcc[r] += (np.outer(self.ex0m_pc[p], dm_rc[r].conj()) * fc0mm02_r[r] * denom) if 'C' in term: denom = (1. / (energy + (m - 1) * self.om_r[r] + omega + 1j * gamma)) m_rcc[r] += (np.outer(self.ex0m_pc[p], dm_rc[r].conj()) * fc1mm1_r[r] * denom) m_rcc[r] += (np.outer(dm_rc[r], self.ex0m_pc[p].conj()) * fc0mm02_r[r] * denom) self.timer.stop('multiply') print('m_rcc[-1]=', m_rcc[-1][2, 2]) self.timer.start('pre_r') with np.errstate(divide='ignore'): pre_r = np.where(self.om_r > 0, np.sqrt(units._hbar**2 / 2. / self.om_r), 0) # print('BC: pre_r=', pre_r) for r, p in enumerate(pre_r): m_rcc[r] *= p self.timer.stop('pre_r') self.timer.stop('AlbrechtBC') return m_rcc def get_matrix_element_Profeta(self, omega, gamma=0.1, energy_derivative=False): """Evaluate Albrecht B+C term in Profeta and Mauri approximation""" self.read() self.timer.start('amplitudes') self.timer.start('init') V_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) pre = 1. / (2 * self.delta) self.timer.stop('init') def kappa(me_pc, e_p, omega, gamma, form='v'): """Kappa tensor after Profeta and Mauri PRB 63 (2001) 245415""" me_ccp = np.empty((3, 3, len(e_p)), dtype=complex) for p, me_c in enumerate(me_pc): me_ccp[:, :, p] = np.outer(me_pc[p], me_pc[p].conj()) # print('kappa: me_ccp=', me_ccp[2,2,0]) # ok print('kappa: den=', 1./(e_p - omega - 1j * gamma)) kappa_ccp = (me_ccp / (e_p - omega - 1j * gamma) + me_ccp.conj() / (e_p + omega + 1j * gamma)) return kappa_ccp.sum(2) self.timer.start('kappa') r = 0 for a in self.indices: for i in 'xyz': if not energy_derivative < 0: V_rcc[r] = pre * self.im[r] * ( kappa(self.expm_rpc[r], self.ex0E_p, omega, gamma) - kappa(self.exmm_rpc[r], self.ex0E_p, omega, gamma)) if energy_derivative: V_rcc[r] += pre * self.im[r] * ( kappa(self.ex0m_pc, self.expE_rp[r], omega, gamma) - kappa(self.ex0m_pc, self.exmE_rp[r], omega, gamma)) r += 1 self.timer.stop('kappa') # print('V_rcc[2], V_rcc[5]=', V_rcc[2,2,2], V_rcc[5,2,2]) self.timer.stop('amplitudes') # map to modes self.timer.start('pre_r') with np.errstate(divide='ignore'): pre_r = np.where(self.om_r > 0, np.sqrt(units._hbar**2 / 2. / self.om_r), 0) V_rcc = np.dot(V_rcc.T, self.modes.T).T # looks ok print('self.modes.T[-1]',self.modes.T) # looks ok print('V_rcc[-1]=', V_rcc[-1][2,2]) # ok print('Profeta: pre_r=', pre_r) for r, p in enumerate(pre_r): V_rcc[r] *= p self.timer.stop('pre_r') return V_rcc def get_matrix_element(self, omega, gamma): self.read() V_rcc = np.zeros((self.ndof, 3, 3), dtype=complex) if self.approximation.lower() == 'profeta': V_rcc += self.get_matrix_element_Profeta(omega, gamma) elif self.approximation.lower() == 'placzek': V_rcc += self.get_matrix_element_Profeta(omega, gamma, True) elif self.approximation.lower() == 'p-p': V_rcc += self.get_matrix_element_Profeta(omega, gamma, -1) elif self.approximation.lower() == 'albrecht a': V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) elif self.approximation.lower() == 'albrecht b': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma, term='B') elif self.approximation.lower() == 'albrecht c': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma, term='C') elif self.approximation.lower() == 'albrecht bc': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma) elif self.approximation.lower() == 'albrecht': raise NotImplementedError('not working') V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) V_rcc += self.get_matrix_element_AlbrechtBC(omega, gamma) elif self.approximation.lower() == 'albrecht+profeta': V_rcc += self.get_matrix_element_AlbrechtA(omega, gamma) V_rcc += self.get_matrix_element_Profeta(omega, gamma) else: raise NotImplementedError( 'Approximation {0} not implemented. '.format( self.approximation) + 'Please use "Profeta", "Albrecht A/B/C/BC", ' + 'or "Albrecht".') return V_rcc def get_intensities(self, omega, gamma=0.1): m2 = ResonantRaman.m2 alpha_rcc = self.get_matrix_element(omega, gamma) if not self.observation: # XXXX remove """Simple sum, maybe too simple""" return m2(alpha_rcc).sum(axis=1).sum(axis=1) # XXX enable when appropraiate # if self.observation['orientation'].lower() != 'random': # raise NotImplementedError('not yet') # random orientation of the molecular frame # Woodward & Long, # Guthmuller, J. J. Chem. Phys. 2016, 144 (6), 64106 m2 = ResonantRaman.m2 alpha2_r = m2(alpha_rcc[:, 0, 0] + alpha_rcc[:, 1, 1] + alpha_rcc[:, 2, 2]) / 9. delta2_r = 3 / 4. * ( m2(alpha_rcc[:, 0, 1] - alpha_rcc[:, 1, 0]) + m2(alpha_rcc[:, 0, 2] - alpha_rcc[:, 2, 0]) + m2(alpha_rcc[:, 1, 2] - alpha_rcc[:, 2, 1])) gamma2_r = (3 / 4. * (m2(alpha_rcc[:, 0, 1] + alpha_rcc[:, 1, 0]) + m2(alpha_rcc[:, 0, 2] + alpha_rcc[:, 2, 0]) + m2(alpha_rcc[:, 1, 2] + alpha_rcc[:, 2, 1])) + (m2(alpha_rcc[:, 0, 0] - alpha_rcc[:, 1, 1]) + m2(alpha_rcc[:, 0, 0] - alpha_rcc[:, 2, 2]) + m2(alpha_rcc[:, 1, 1] - alpha_rcc[:, 2, 2])) / 2) if self.observation['geometry'] == '-Z(XX)Z': # Porto's notation return (45 * alpha2_r + 5 * delta2_r + 4 * gamma2_r) / 45. elif self.observation['geometry'] == '-Z(XY)Z': # Porto's notation return gamma2_r / 15. elif self.observation['scattered'] == 'Z': # scattered light in direction of incoming light return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45. elif self.observation['scattered'] == 'parallel': # scattered light perendicular and # polarization in plane return 6 * gamma2_r / 45. elif self.observation['scattered'] == 'perpendicular': # scattered light perendicular and # polarization out of plane return (45 * alpha2_r + 5 * delta2_r + 7 * gamma2_r) / 45. else: raise NotImplementedError def get_cross_sections(self, omega, gamma=0.1): I_r = self.get_intensities(omega, gamma) pre = 1. / 16 / np.pi**2 / units.eps0**2 / units.c**4 # frequency of scattered light omS_r = omega - self.hnu return pre * omega * omS_r**3 * I_r def get_spectrum(self, omega, gamma=0.1, start=200.0, end=4000.0, npts=None, width=4.0, type='Gaussian', method='standard', direction='central', 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'] if not npts: npts = int((end - start) / width * 10 + 1) frequencies = self.get_frequencies(method, direction).real intensities = self.get_cross_sections(omega, gamma) prefactor = 1 if 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 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', method='standard', direction='central'): """Write out spectrum to file. First column is the wavenumber in cm^-1, the second column the absolute infrared intensities, and the third column the absorbance scaled so that data runs from 1 to 0. 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, method, direction) # Write out spectrum in file. First column is absolute intensities. outdata = np.empty([len(energies), 3]) outdata.T[0] = energies outdata.T[1] = spectrum fd = open(out, 'w') fd.write('# Resonant Raman spectrum\n') fd.write('# omega={0:g} eV, gamma={1:g} eV\n'.format(omega, gamma)) 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])) fd.close() def summary(self, omega, gamma=0.1, method='standard', direction='central', log=sys.stdout): """Print summary for given omega [eV]""" hnu = self.get_energies(method, direction) s = 0.01 * units._e / units._c / units._hplanck intensities = self.get_intensities(omega, gamma) if isinstance(log, basestring): log = paropen(log, 'a') parprint('-------------------------------------', file=log) parprint(' excitation at ' + str(omega) + ' eV', file=log) parprint(' gamma ' + str(gamma) + ' eV', file=log) parprint(' approximation:', self.approximation, file=log) parprint(' observation:', self.observation, '\n', file=log) parprint(' Mode Frequency Intensity', file=log) parprint(' # meV cm^-1 [e^4A^4/eV^2]', 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.3g' % (n, 1000 * e, c, s * e, c, intensities[n]), file=log) parprint('-------------------------------------', file=log) parprint('Zero-point energy: %.3f eV' % self.get_zero_point_energy(), file=log) def __del__(self): self.timer.write(self.txt)
# FCOverlap fco = FranckCondonOverlap() # check factorial assert (fco.factorial(8) == factorial(8)) # the second test is useful according to the implementation assert (fco.factorial(5) == factorial(5)) # check T=0 and n=0 equality S = np.array([1, 2.1, 34]) m = 5 assert (((fco.directT0(m, S) - fco.direct(0, m, S)) / fco.directT0(m, S) < 1e-15).all()) # check symmetry S = 2 n = 3 assert (fco.direct(n, m, S) == fco.direct(m, n, S)) # --------------------------- # specials S = 1.5 for m in [2, 7]: equal( fco.direct0mm1(m, S)**2, fco.direct(1, m, S) * fco.direct(m, 0, S), 1.e-17) equal( fco.direct0mm2(m, S)**2, fco.direct(2, m, S) * fco.direct(m, 0, S), 1.e-17)
sys.stderr.write('WARNING: %s\n' % msg) # FCOverlap fco = FranckCondonOverlap() # check factorial assert(fco.factorial(8) == factorial(8)) # the second test is useful according to the implementation assert(fco.factorial(5) == factorial(5)) # check T=0 and n=0 equality S = np.array([1, 2.1, 34]) m = 5 assert(((fco.directT0(m, S) - fco.direct(0, m, S)) / fco.directT0(m, S) < 1e-15).all()) # check symmetry S = 2 n = 3 assert(fco.direct(n, m, S) == fco.direct(m, n, S)) # --------------------------- # specials S = 1.5 for m in [2, 7]: equal(fco.direct0mm1(m, S)**2, fco.direct(1, m, S) * fco.direct(m, 0, S), 1.e-17) equal(fco.direct0mm2(m, S)**2, fco.direct(2, m, S) * fco.direct(m, 0, S), 1.e-17)