Coverage for gpaw/elph/raman_calculator.py: 92%
86 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
1"""
2Calculates Raman matrices.
4i -> j -> m -> n
5i, n are valence; j, m are conduction, also i=n in the end
6see https://doi.org/10.1038/s41467-020-16529-6
7"""
8import numpy as np
10from ase.units import invcm, Hartree
11from ase.utils.filecache import MultiFileJSONCache
12from gpaw.calculator import GPAW
13from gpaw.typing import ArrayND
15# TODO: only take kd, don't need whole calculator
18class ResonantRamanCalculator:
19 def __init__(
20 self,
21 calc: GPAW,
22 wph_w: ArrayND,
23 momname: str = "mom_skvnm.npy",
24 elphname: str = "gsqklnn.npy",
25 raman_name: str = "Rlab",
26 ):
27 """Resonant Raman Matrix calculator
29 Parameters
30 ----------
31 calc: GPAW
32 GPAW calculator object.
33 w_ph: str, np.ndarray
34 Zone centre phonon frequencies in eV
35 momname: str
36 Name of momentum file
37 elphname: str
38 Name of electron-phonon file
39 raman_name: str
40 Name of Rlab file cache. Default 'Rlab'
41 """
42 # those won't make sense here
43 assert calc.wfs.gd.comm.size == 1
44 assert calc.wfs.bd.comm.size == 1
45 self.kd = calc.wfs.kd
46 self.calc = calc
48 # Phonon frequencies
49 if isinstance(wph_w, str):
50 wph_w = np.load(wph_w)
51 assert max(wph_w) < 1.0 # else not eV units
52 self.w_ph = wph_w
54 # Load files
55 mom_skvnm = np.load(momname, mmap_mode="c")
56 g_sqklnn = np.load(elphname, mmap_mode="c") # [s,q=0,k,l,n,m]
57 self.mom_skvnm = mom_skvnm
58 self.g_sqklnn = g_sqklnn
60 # Define a few more variables
61 nspins = g_sqklnn.shape[0]
62 nk = g_sqklnn.shape[2]
63 assert wph_w.shape[0] == g_sqklnn.shape[3]
64 assert mom_skvnm.shape[0] == nspins
65 assert mom_skvnm.shape[1] == nk
66 assert mom_skvnm.shape[-1] == g_sqklnn.shape[-1]
68 # JSON cache
69 self.raman_cache = MultiFileJSONCache(raman_name)
70 with self.raman_cache.lock("phonon_frequencies") as handle:
71 if handle is not None:
72 handle.save(wph_w)
73 # Resonant part of Raman tensor does not have a frequency grid
74 with self.raman_cache.lock("frequency_grid") as handle:
75 if handle is not None:
76 handle.save(None)
78 @classmethod
79 def resonant_term(
80 cls,
81 f_vc: ArrayND,
82 E_vc: ArrayND,
83 mom_dnn: ArrayND,
84 elph_lnn: ArrayND,
85 nc: int,
86 nv: int,
87 w_in: float,
88 wph_w: ArrayND,
89 ) -> ArrayND:
90 """Resonant term of the Raman tensor"""
91 nmodes = elph_lnn.shape[0]
92 term_l = np.zeros((nmodes), dtype=complex)
93 t_ij = f_vc * mom_dnn[0, :nv, nc:] / (w_in - E_vc)
94 for l in range(nmodes):
95 t_xx = elph_lnn[l]
96 t_mn = f_vc.T * mom_dnn[1, nc:, :nv] / (w_in - wph_w[l] - E_vc.T)
97 term_l[l] += np.einsum("sj,jm,ms", t_ij, t_xx[nc:, nc:], t_mn)
98 term_l[l] -= np.einsum("is,ni,sn", t_ij, t_xx[:nv, :nv], t_mn)
99 return term_l
101 def calculate(self, w_in, d_i, d_o, gamma_l=0.1, limit_sum=False):
102 """Calculate resonant Raman matrix
104 Parameters
105 ----------
106 w_in: float
107 Laser frequency in eV
108 d_i: int
109 Incoming polarization
110 d_o: int
111 Outgoing polarization
112 gamma_l: float
113 Line broadening in eV, (0.1eV=806rcm)
114 limit_sum: bool
115 Limit sum to occupied valence/unoccupied conduction states
116 Use for debugging and testing. Don't use in production
117 """
119 raman_l = np.zeros((self.w_ph.shape[0]), dtype=complex)
121 print(f"Calculating Raman spectrum: Laser frequency = {w_in} eV")
123 # Loop over kpoints - this is parallelised
124 for kpt in self.calc.wfs.kpt_u:
125 # print(f"Rank {self.kd.comm.rank}: s={kpt.s}, k={kpt.k}")
127 f_n = kpt.f_n / kpt.weight
128 assert np.isclose(max(f_n), 1.0, atol=0.1)
130 vs = np.arange(0, len(f_n))
131 cs = np.arange(0, len(f_n))
132 nc = 0
133 nv = len(f_n)
134 if limit_sum: # good to test that we got arrays right
135 vs = np.where(f_n >= 0.1)[0]
136 cs = np.where(f_n < 0.9)[0]
137 nv = max(vs) + 1 # VBM+1 index
138 nc = min(cs) # CBM index
140 # Precalculate f * (1-f) term
141 f_vc = np.outer(f_n[vs], 1.0 - f_n[cs])
143 # Precalculate E-E term
144 E_vc = np.zeros((len(vs), len(cs)), dtype=complex) + 1j * gamma_l
145 for n in range(len(vs)):
146 E_vc[n] += (kpt.eps_n[cs] - kpt.eps_n[n]) * Hartree
148 # Obtain appropriate part of mom and g arrays
149 pols = [d_i, d_o]
150 mom_dnn = np.ascontiguousarray(self.mom_skvnm[kpt.s, kpt.k, pols])
151 assert mom_dnn.shape[0] == 2
152 g_lnn = np.ascontiguousarray(self.g_sqklnn[kpt.s, 0, kpt.k])
154 # Raman contribution of this k-point
155 raman_l += self.resonant_term(
156 f_vc, E_vc, mom_dnn, g_lnn, nc, nv, w_in, self.w_ph
157 ) * kpt.weight
158 # Collect parallel contributions
159 self.kd.comm.sum(raman_l)
161 if self.kd.comm.rank == 0:
162 print(f"Raman intensities per mode in {'xyz'[d_i]}{'xyz'[d_o]}")
163 print("--------------------------")
164 ff = " Phonon {} with energy = {:4.2f} rcm: {:.4f}"
165 for l in range(self.w_ph.shape[0]):
166 print(ff.format(l, self.w_ph[l] / invcm, raman_l[l]))
168 return raman_l
170 def calculate_raman_tensor(self, w_in, gamma_l=0.1):
171 """Calculate whole Raman tensor
173 Parameters
174 ----------
175 w_in: float
176 Laser frequency in eV
177 gamma_l: float
178 Line broadening in eV, (0.1eV=806rcm)
179 """
180 # If exist already, don't recompute
181 for i in range(3):
182 for j in range(3):
183 with self.raman_cache.lock(f"{'xyz'[i]}{'xyz'[j]}") as handle:
184 if handle is None:
185 continue
186 R_l = self.calculate(w_in, i, j, gamma_l)
187 self.kd.comm.barrier()
188 handle.save(R_l)
190 def nm_to_eV(self, laser_wave_length):
191 return 1.239841e3 / laser_wave_length