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

134 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-04-15 18:15 +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 def _repr_html_(self) -> str: 

61 """HTML representation for Jupyter notebooks.""" 

62 rows = [f'<tr><td style="text-align: left;">name</td><td>{self._name}</td></tr>'] 

63 return ( 

64 f'<h4>{self.__class__.__name__}</h4>' 

65 '<table>' 

66 '<thead><tr>' 

67 '<th style="text-align: left;">field</th>' 

68 '<th style="text-align: left;">value</th>' 

69 '</tr></thead>' 

70 '<tbody>' + ''.join(rows) + '</tbody>' 

71 '</table>' 

72 ) 

73 

74 @property 

75 def name(self) -> str: 

76 """Name of dielectric function.""" 

77 return self._name 

78 

79 

80class AnalyticDielectricFunction(DielectricFunction): 

81 

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

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

84 without interpolation. 

85 

86 Parameters 

87 ---------- 

88 name 

89 Name of dielectric function. 

90 """ 

91 

92 def __init__(self, 

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

94 

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

96 super().__init__(name=name) 

97 

98 

99class NumericDielectricFunction(DielectricFunction): 

100 

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

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

103 within the supplied grid by linear interpolation. 

104 

105 Parameters 

106 ---------- 

107 frequencies 

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

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

110 (see :attr:`units`). 

111 

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

113 is not monotonically increasing. 

114 dielectric_function 

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

116 units 

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

118 

119 This parameter determines whether conversion should be performed during 

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

121 name 

122 Name of dielectric function. 

123 

124 Raises 

125 ------ 

126 ValueError 

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

128 ValueError 

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

130 ValueError 

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

132 

133 Examples 

134 -------- 

135 

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

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

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

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

140 

141 >>> import numpy as np 

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

143 >>> 

144 >>> omega_p = 3 

145 >>> gamma = 0.5 

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

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

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

149 >>> print(dielec) 

150 ---------------- NumericDielectricFunction ----------------- 

151 name : Drude-function 

152 Frequency grid : 50 points between 0.10 and 10.00eV 

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

154 9.59592 9.79796 10. ] 

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

156 -25.37528+4.36618e+01j 

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

158 0.90253+5.07897e-03j 

159 0.90649+4.77173e-03j 

160 0.91022+4.48878e-03j] 

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

162 >>> print(dielec(dense_freq_w)) 

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

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

165 ... 

166 """ 

167 

168 def __init__(self, 

169 frequencies: Union[Array, float], 

170 dielectric_function: Union[ComplexArray, complex], 

171 units: str = 'eV', 

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

173 

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

175 super().__init__(name=name) 

176 

177 if isinstance(frequencies, float): 

178 frequencies = [frequencies] 

179 

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

181 dielectric_function = [dielectric_function] 

182 

183 freq_w = np.array(frequencies) 

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

185 

186 if units == 'eV': 

187 freq_w = freq_w * eV_to_au 

188 elif units != 'au': 

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

190 

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

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

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

194 

195 if len(freq_w) == 0: 

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

197 

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

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

200 sort_w = np.argsort(freq_w) 

201 freq_w = freq_w[sort_w] 

202 eps_w = eps_w[sort_w] 

203 

204 self._freq_w = freq_w 

205 self._eps_w = eps_w 

206 

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

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

209 dielectric function. 

210 

211 Parameters 

212 ---------- 

213 frequencies 

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

215 in units of eV. 

216 

217 Raises 

218 ------ 

219 ValueError 

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

221 dielectric function. 

222 """ 

223 atol = np.finfo(float).eps * max(abs(self._freq_w[0]), abs(self._freq_w[-1])) 

224 if np.min(freq_w) < self._freq_w[0] - atol: 

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

226 'numeric dielectric function') 

227 if np.max(freq_w) > self._freq_w[-1] + atol: 

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

229 'numeric dielectric function') 

230 

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

232 

233 return eps_w # type: ignore 

234 

235 def _repr_html_(self) -> str: 

236 """HTML representation for Jupyter notebooks.""" 

237 rows = [ 

238 f'<tr><td style="text-align: left;">name</td><td>{self._name}</td></tr>', 

239 f'<tr><td style="text-align: left;">frequency grid</td>' 

240 f'<td>{self.n} points between {self.minfreq:.2f} and {self.maxfreq:.2f} eV</td></tr>', 

241 ] 

242 return ( 

243 f'<h4>{self.__class__.__name__}</h4>' 

244 '<table>' 

245 '<thead><tr>' 

246 '<th style="text-align: left;">field</th>' 

247 '<th style="text-align: left;">value</th>' 

248 '</tr></thead>' 

249 '<tbody>' + ''.join(rows) + '</tbody>' 

250 '</table>' 

251 ) 

252 

253 def __str__(self) -> str: 

254 fmt = ClassFormatter(self, pad=20) 

255 fmt.append_class_name() 

256 

257 fmt.append_attr('name') 

258 

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

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

261 

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

263 fmt.append_attr('dielectric_function') 

264 

265 return fmt.to_string() 

266 

267 @property 

268 def n(self) -> int: 

269 """number of samples for the dielectric function 

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

271 return len(self._eps_w) 

272 

273 @property 

274 def minfreq(self) -> float: 

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

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

277 m = self._freq_w[0] * au_to_eV 

278 assert isinstance(m, float) 

279 return m 

280 

281 @property 

282 def maxfreq(self) -> float: 

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

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

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

286 assert isinstance(m, float) 

287 return m 

288 

289 @property 

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

291 """Frequencies corresponding to dielectric function 

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

293 array with shape {n}.""" 

294 return self._freq_w * au_to_eV # type: ignore 

295 

296 @property 

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

298 """Complex unitless dielectric function at frequencies 

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

300 return self._eps_w 

301 

302 

303class DrudeDielectricFunction(AnalyticDielectricFunction): 

304 

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

306 entire positive frequency domain. The function is implemented as 

307 

308 .. math:: 

309 

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

311 

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

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

314 

315 Parameters 

316 ---------- 

317 plasma_frequency 

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

319 (see :attr:`units`). 

320 broadening 

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

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

323 name 

324 Name of the dielectric function. 

325 units 

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

327 in atomic units. 

328 

329 This parameter determines whether conversion should be performed during 

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

331 """ 

332 

333 def __init__(self, 

334 plasma_frequency: float, 

335 broadening: float, 

336 units: str = 'eV', 

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

338 

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

340 super().__init__(name=name) 

341 

342 omega_pl = plasma_frequency 

343 gamma = broadening 

344 if units == 'eV': 

345 omega_pl *= eV_to_au 

346 gamma *= eV_to_au 

347 elif units != 'au': 

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

349 

350 self._omega_pl = omega_pl 

351 self._gamma = gamma 

352 

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

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

355 return eps_w # type: ignore 

356 

357 def _repr_html_(self) -> str: 

358 """HTML representation for Jupyter notebooks.""" 

359 rows = [ 

360 f'<tr><td style="text-align: left;">name</td><td>{self._name}</td></tr>', 

361 f'<tr><td style="text-align: left;">plasma_frequency (eV)</td>' 

362 f'<td>{self.plasma_frequency:.4f}</td></tr>', 

363 f'<tr><td style="text-align: left;">broadening (eV)</td>' 

364 f'<td>{self.broadening:.4f}</td></tr>', 

365 ] 

366 return ( 

367 f'<h4>{self.__class__.__name__}</h4>' 

368 '<table>' 

369 '<thead><tr>' 

370 '<th style="text-align: left;">field</th>' 

371 '<th style="text-align: left;">value</th>' 

372 '</tr></thead>' 

373 '<tbody>' + ''.join(rows) + '</tbody>' 

374 '</table>' 

375 ) 

376 

377 def __str__(self) -> str: 

378 fmt = ClassFormatter(self, pad=15) 

379 fmt.append_class_name() 

380 

381 fmt.append_attr('name') 

382 

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

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

385 

386 return fmt.to_string() 

387 

388 @property 

389 def plasma_frequency(self) -> float: 

390 """plasma frequency.""" 

391 return self._omega_pl * au_to_eV 

392 

393 @property 

394 def broadening(self) -> float: 

395 """Broadening parameter.""" 

396 return self._gamma * au_to_eV 

397 

398 

399def read_dielec_function_file(dielectric_function_file: Path_str, 

400 frequency_column: int = 0, 

401 dielectric_function_real_column: int = 1, 

402 dielectric_function_imag_column: int = 2, 

403 units: str = 'eV', 

404 name: str = 'NumericDielectricFunction from file', 

405 **np_loadtxt_kwargs, 

406 ) -> NumericDielectricFunction: 

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

408 

409 Parameters 

410 ---------- 

411 dielectric_function_file 

412 Path to the file containing the dielectic function. 

413 frequency_column 

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

415 dielectric_function_real_column 

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

417 dielectric_function_imag_column 

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

419 units 

420 Units of the frequency column; 

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

422 name 

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

424 np_loadtxt_kwargs 

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

426 """ 

427 

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

429 usecols=[frequency_column, 

430 dielectric_function_real_column, 

431 dielectric_function_imag_column], 

432 unpack=True, 

433 **np_loadtxt_kwargs) 

434 df_w = df_real_w + 1j*df_imag_w 

435 

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

437 

438 return df 

439 

440 

441def read_dielec_function_from_refractive_index_file( 

442 dielectric_function_file: Path_str, 

443 frequency_column: int = 0, 

444 refractive_index_column: int = 1, 

445 extinction_coefficient_column: int = 2, 

446 units: str = 'eV', 

447 name: str = 'NumericDielectricFunction from file', 

448 **np_loadtxt_kwargs, 

449 ) -> NumericDielectricFunction: 

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

451 of a material. 

452 

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

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

455 

456 .. math:: 

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

458 

459 Parameters 

460 ---------- 

461 dielectric_function_file 

462 Path to the file containing the dielectic function. 

463 frequency_column 

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

465 refractive_index_column 

466 Index of column containing the unitless refractive index. 

467 extinction_coefficient_column 

468 Index of column containing the unitless extinction coefficient. 

469 units 

470 Units of the frequency column; 

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

472 name 

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

474 np_loadtxt_kwargs 

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

476 """ 

477 

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

479 usecols=[frequency_column, 

480 refractive_index_column, 

481 extinction_coefficient_column], 

482 unpack=True, 

483 **np_loadtxt_kwargs) 

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

485 

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

487 

488 return df