Coverage for strongcoca/response/dielectric.py: 100%

124 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-26 18:44 +0000

1import logging 

2from abc import ABC, abstractmethod 

3from typing import Union 

4 

5import numpy as np 

6 

7from ..types import Array, ComplexArray, Path_str 

8from ..units import au_to_eV, eV_to_au 

9from ..utilities import ClassFormatter 

10 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15class DielectricFunction(ABC): 

16 

17 """Objects of this class hold different representations of dielectric functions. 

18 

19 Parameters 

20 ---------- 

21 name 

22 Name of dielectric function. 

23 """ 

24 

25 def __init__(self, 

26 name: str = 'DielectricFunction') -> None: 

27 logger.debug(f'Entering {self.__class__.__name__}.__init__') 

28 self._name = name 

29 

30 def __call__(self, frequencies: Array) -> np.ndarray: 

31 """Returns the dielectric function at the given frequencies 

32 

33 Parameters 

34 ---------- 

35 frequencies 

36 List of frequencies at which the dielectric function is returned; 

37 in units of eV. 

38 """ 

39 freq_w = np.array(frequencies, dtype=float) 

40 freq_w *= eV_to_au 

41 

42 return self._eval_at(freq_w) 

43 

44 @abstractmethod 

45 def _eval_at(self, freq_w: np.ndarray) -> np.ndarray: 

46 """Returns the dielectric function at the given frequencies 

47 

48 Parameters 

49 ---------- 

50 frequencies 

51 List of frequencies at which the dielectric function is returned; 

52 in atomic units. 

53 """ 

54 raise NotImplementedError() 

55 

56 @abstractmethod 

57 def __str__(self) -> str: 

58 raise NotImplementedError() 

59 

60 @property 

61 def name(self) -> str: 

62 """Name of dielectric function.""" 

63 return self._name 

64 

65 

66class AnalyticDielectricFunction(DielectricFunction): 

67 

68 """Objects of this class hold different representations of dielectric functions that 

69 have analytic form. The dielectric function can thus be evaluated at any frequency 

70 without interpolation. 

71 

72 Parameters 

73 ---------- 

74 name 

75 Name of dielectric function. 

76 """ 

77 

78 def __init__(self, 

79 name: str = 'AnalyticDielectricFunction') -> None: 

80 

81 logger.debug(f'Entering {self.__class__.__name__}.__init__') 

82 super().__init__(name=name) 

83 

84 

85class NumericDielectricFunction(DielectricFunction): 

86 

87 """Objects of this class hold different representations of dielectric functions sampled 

88 on a discrete grid of frequencies. The dielectric function can be evaluated at any frequency 

89 within the supplied grid by linear interpolation. 

90 

91 Parameters 

92 ---------- 

93 frequencies 

94 List of frequencies at which the dielectric function :attr:`dielectric_function` 

95 is sampled; units are eV by default, optionally atomic units 

96 (see :attr:`units`). 

97 

98 The list of frequencies and the corresponding dielectric function are sorted if the former 

99 is not monotonically increasing. 

100 dielectric_function 

101 Complex dielectric function sampled at the frequencies :attr:`frequencies`. 

102 units 

103 `eV` to specify :attr:`freq_w` in eV, or `au` to specify in atomic units. 

104 

105 This parameter determines whether conversion should be performed during 

106 initialization and has no effect on instance methods and variables. 

107 name 

108 Name of dielectric function. 

109 

110 Raises 

111 ------ 

112 ValueError 

113 If :attr:`frequencies` or :attr:`dielectric_function` have zero length. 

114 ValueError 

115 If there is a mismatch in the length of :attr:`frequencies` and :attr:`dielectric_function`. 

116 ValueError 

117 If :attr:`units` is not one of the allowed values `eV` or `au`. 

118 

119 Examples 

120 -------- 

121 

122 The following snippet illustrates how a NumericDielectricFunction object can be constructed. 

123 Here, the analytic Drude dielectric function of the free-electron gas is used. 

124 To construct a dielectric function object from a numeric dielectric function the function 

125 :func:`~strongcoca.response.read_dielec_function_file` can be used. 

126 

127 >>> import numpy as np 

128 >>> from strongcoca.response.dielectric import NumericDielectricFunction 

129 >>> 

130 >>> omega_p = 3 

131 >>> gamma = 0.5 

132 >>> freq_w = np.linspace(0.1, 10) 

133 >>> eps_w = 1 - omega_p**2 / (freq_w**2 + 1j*gamma*freq_w) 

134 >>> dielec = NumericDielectricFunction(freq_w, eps_w, name='Drude-function') 

135 >>> print(dielec) 

136 ---------------- NumericDielectricFunction ----------------- 

137 name : Drude-function 

138 Frequency grid : 50 points between 0.10 and 10.00eV 

139 frequencies (eV) : [ 0.1 0.30204 0.50408 ... 

140 9.59592 9.79796 10. ] 

141 dielectric_function : [-33.61538+1.73077e+02j 

142 -25.37528+4.36618e+01j 

143 -16.85366+1.77091e+01j ... 

144 0.90253+5.07897e-03j 

145 0.90649+4.77173e-03j 

146 0.91022+4.48878e-03j] 

147 >>> dense_freq_w = np.linspace(3, 4) 

148 >>> print(dielec(dense_freq_w)) 

149 [0.02425453+0.16310249j 0.0367996 +0.15996402j 0.04934466+0.15682555j 

150 0.06188972+0.15368708j 0.07443478+0.15054861j 0.08697984+0.14741014j 

151 ... 

152 """ 

153 

154 def __init__(self, 

155 frequencies: Union[Array, float], 

156 dielectric_function: Union[ComplexArray, complex], 

157 units: str = 'eV', 

158 name: str = 'NumericDielectricFunction') -> None: 

159 

160 logger.debug(f'Entering {self.__class__.__name__}.__init__') 

161 super().__init__(name=name) 

162 

163 if isinstance(frequencies, float): 

164 frequencies = [frequencies] 

165 

166 if isinstance(dielectric_function, complex) or isinstance(dielectric_function, float): 

167 dielectric_function = [dielectric_function] 

168 

169 freq_w = np.array(frequencies) 

170 eps_w = np.array(dielectric_function, dtype=complex) 

171 

172 if units == 'eV': 

173 freq_w = freq_w * eV_to_au 

174 elif units != 'au': 

175 raise ValueError(f"units has to be 'eV' or 'au', not '{units}'") 

176 

177 if len(freq_w) != len(eps_w): 

178 raise ValueError(f'length of frequencies {len(freq_w)} must match length of ' 

179 f'dielectric function {len(eps_w)}') 

180 

181 if len(freq_w) == 0: 

182 raise ValueError('length of frequencies and dielectric function must not be zero') 

183 

184 # np.diff works if input has at least length 2 

185 if len(freq_w) > 1 and not np.all(np.diff(freq_w) > 0): 

186 sort_w = np.argsort(freq_w) 

187 freq_w = freq_w[sort_w] 

188 eps_w = eps_w[sort_w] 

189 

190 self._freq_w = freq_w 

191 self._eps_w = eps_w 

192 

193 def _eval_at(self, freq_w: np.ndarray) -> np.ndarray: 

194 """Returns the dielectric function at the given frequencies by interpolating the numeric 

195 dielectric function. 

196 

197 Parameters 

198 ---------- 

199 frequencies 

200 List of frequencies at which the dielectric function is returned; 

201 in units of eV. 

202 

203 Raises 

204 ------ 

205 ValueError 

206 If any component of :attr:`frequencies` is outside of the interval of the numeric 

207 dielectric function. 

208 """ 

209 if np.min(freq_w) < self._freq_w[0]: 

210 raise ValueError('Minimum of array frequencies is outside the lower bound of the' 

211 'numeric dielectric function') 

212 if np.max(freq_w) > self._freq_w[-1]: 

213 raise ValueError('Maximum of array frequencies is outside the upper bound of the' 

214 'numeric dielectric function') 

215 

216 eps_w = np.interp(freq_w, self._freq_w, self._eps_w) 

217 

218 return eps_w # type: ignore 

219 

220 def __str__(self) -> str: 

221 fmt = ClassFormatter(self, pad=20) 

222 fmt.append_class_name() 

223 

224 fmt.append_attr('name') 

225 

226 fmt.append_attr('Frequency grid', f'{self.n} points between {self.minfreq:.2f} and ' 

227 f'{self.maxfreq:.2f}eV') 

228 

229 fmt.append_attr('frequencies', unit='eV') 

230 fmt.append_attr('dielectric_function') 

231 

232 return fmt.to_string() 

233 

234 @property 

235 def n(self) -> int: 

236 """number of samples for the dielectric function 

237 :attr:`~strongcoca.response.NumericDielectricFunction.dielectric_function`.""" 

238 return len(self._eps_w) 

239 

240 @property 

241 def minfreq(self) -> float: 

242 """Lowest frequency :attr:`~strongcoca.response.NumericDielectricFunction.frequencies` 

243 for which there is a value of the dielectric function; in units of eV.""" 

244 m = self._freq_w[0] * au_to_eV 

245 assert isinstance(m, float) 

246 return m 

247 

248 @property 

249 def maxfreq(self) -> float: 

250 """Highest frequency :attr:`~strongcoca.response.NumericDielectricFunction.frequencies` 

251 for which there is a value of the dielectric function; in units of eV.""" 

252 m = self._freq_w[-1] * au_to_eV 

253 assert isinstance(m, float) 

254 return m 

255 

256 @property 

257 def frequencies(self) -> np.ndarray: 

258 """Frequencies corresponding to dielectric function 

259 :attr:`~strongcoca.response.NumericDielectricFunction.dielectric_function` in units of eV; 

260 array with shape {n}.""" 

261 return self._freq_w * au_to_eV # type: ignore 

262 

263 @property 

264 def dielectric_function(self) -> np.ndarray: 

265 """Complex unitless dielectric function at frequencies 

266 :attr:`~strongcoca.response.MieGansResponse.frequencies`; array with shape {n}.""" 

267 return self._eps_w 

268 

269 

270class DrudeDielectricFunction(AnalyticDielectricFunction): 

271 

272 r"""Objects of this class represent the Drude dielectric function, which is defined for the 

273 entire positive frequency domain. The function is implemented as 

274 

275 .. math:: 

276 

277 \varepsilon_r(\omega) = 1 - \frac{\omega_p^2}{\omega^2 + i\gamma\omega} 

278 

279 where the plasma frequency :math:`\omega_p` determines where the real part of the dielectric 

280 function is zero and :math:`\gamma` is a broadening. 

281 

282 Parameters 

283 ---------- 

284 plasma_frequency 

285 The plasma frequency :math:`\omega_p`; units are eV by default, optionally atomic units 

286 (see :attr:`units`). 

287 broadening 

288 Broadening of the dielectric function :math:`\gamma` units are eV by default, optionally 

289 atomic units (see :attr:`units`). 

290 name 

291 Name of the dielectric function. 

292 units 

293 `eV` to specify :attr:`plasma_frequency` and :attr:`broadening` in eV, or `au` to specify 

294 in atomic units. 

295 

296 This parameter determines whether conversion should be performed during 

297 initialization and has no effect on instance methods and variables. 

298 """ 

299 

300 def __init__(self, 

301 plasma_frequency: float, 

302 broadening: float, 

303 units: str = 'eV', 

304 name: str = 'DrudeDielectricFunction') -> None: 

305 

306 logger.debug(f'Entering {self.__class__.__name__}.__init__') 

307 super().__init__(name=name) 

308 

309 omega_pl = plasma_frequency 

310 gamma = broadening 

311 if units == 'eV': 

312 omega_pl *= eV_to_au 

313 gamma *= eV_to_au 

314 elif units != 'au': 

315 raise ValueError(f"units has to be 'eV' or 'au', not '{units}'") 

316 

317 self._omega_pl = omega_pl 

318 self._gamma = gamma 

319 

320 def _eval_at(self, freq_w: np.ndarray) -> np.ndarray: 

321 eps_w = 1 - self._omega_pl**2 / (freq_w**2 + 1j*self._gamma*freq_w) 

322 return eps_w # type: ignore 

323 

324 def __str__(self) -> str: 

325 fmt = ClassFormatter(self, pad=15) 

326 fmt.append_class_name() 

327 

328 fmt.append_attr('name') 

329 

330 fmt.append_attr('plasma_frequency', unit='eV') 

331 fmt.append_attr('broadening', unit='eV') 

332 

333 return fmt.to_string() 

334 

335 @property 

336 def plasma_frequency(self) -> float: 

337 """plasma frequency.""" 

338 return self._omega_pl * au_to_eV 

339 

340 @property 

341 def broadening(self) -> float: 

342 """Broadening parameter.""" 

343 return self._gamma * au_to_eV 

344 

345 

346def read_dielec_function_file(dielectric_function_file: Path_str, 

347 frequency_column: int = 0, 

348 dielectric_function_real_column: int = 1, 

349 dielectric_function_imag_column: int = 2, 

350 units: str = 'eV', 

351 name: str = 'NumericDielectricFunction from file', 

352 **np_loadtxt_kwargs, 

353 ) -> NumericDielectricFunction: 

354 """Parses a textfile with columns describing the dielectric function of a material. 

355 

356 Parameters 

357 ---------- 

358 dielectric_function_file 

359 Path to the file containing the dielectic function. 

360 frequency_column 

361 Index of column containing the frequency vector in units of eV. 

362 dielectric_function_real_column 

363 Index of column containing the real part of the unitless dielectric function. 

364 dielectric_function_imag_column 

365 Index of column containing the imaginary part of the unitless dielectric function. 

366 units 

367 Units of the frequency column; 

368 passed to :class:`strongcoca.response.NumericDielectricFunction`. 

369 name 

370 Passed to :class:`~strongcoca.response.NumericDielectricFunction`. 

371 np_loadtxt_kwargs 

372 Parameters passed to :func:`numpy.loadtxt`; useful parameters are `delimiter` or `comments`. 

373 """ 

374 

375 freq_w, df_real_w, df_imag_w = np.loadtxt(dielectric_function_file, 

376 usecols=[frequency_column, 

377 dielectric_function_real_column, 

378 dielectric_function_imag_column], 

379 unpack=True, 

380 **np_loadtxt_kwargs) 

381 df_w = df_real_w + 1j*df_imag_w 

382 

383 df = NumericDielectricFunction(freq_w, df_w, units=units, name=name) 

384 

385 return df 

386 

387 

388def read_dielec_function_from_refractive_index_file( 

389 dielectric_function_file: Path_str, 

390 frequency_column: int = 0, 

391 refractive_index_column: int = 1, 

392 extinction_coefficient_column: int = 2, 

393 units: str = 'eV', 

394 name: str = 'NumericDielectricFunction from file', 

395 **np_loadtxt_kwargs, 

396 ) -> NumericDielectricFunction: 

397 r"""Parses a textfile with columns describing the refractive index and extinction coefficient 

398 of a material. 

399 

400 The refractive index :math:`n(\omega)` and extinction coefficient :math:`k(\omega)` are 

401 related to the complex dielectric function :math:`\varepsilon_r(\omega)` by 

402 

403 .. math:: 

404 \varepsilon_r(\omega) = n^2(\omega) - k^2(\omega) + 2jn(\omega)k(\omega) 

405 

406 Parameters 

407 ---------- 

408 dielectric_function_file 

409 Path to the file containing the dielectic function. 

410 frequency_column 

411 Index of column containing the frequency vector in units of eV. 

412 refractive_index_column 

413 Index of column containing the unitless refractive index. 

414 extinction_coefficient_column 

415 Index of column containing the unitless extinction coefficient. 

416 units 

417 Units of the frequency column; 

418 passed to :class:`~strongcoca.response.NumericDielectricFunction`. 

419 name 

420 Passed to :class:`~strongcoca.response.NumericDielectricFunction`. 

421 np_loadtxt_kwargs 

422 Parameters passed to :func:`numpy.loadtxt`; useful parameters are `delimiter` or `comments`. 

423 """ 

424 

425 freq_w, n_w, k_w = np.loadtxt(dielectric_function_file, 

426 usecols=[frequency_column, 

427 refractive_index_column, 

428 extinction_coefficient_column], 

429 unpack=True, 

430 **np_loadtxt_kwargs) 

431 df_w = n_w**2 - k_w**2 + 2j*n_w*k_w 

432 

433 df = NumericDielectricFunction(freq_w, df_w, units=units, name=name) 

434 

435 return df