import logging
from abc import ABC, abstractmethod
from typing import Union
import numpy as np
from ..types import Array, ComplexArray, Path_str
from ..units import au_to_eV, eV_to_au
from ..utilities import ClassFormatter
logger = logging.getLogger(__name__)
[docs]
class DielectricFunction(ABC):
"""Objects of this class hold different representations of dielectric functions.
Parameters
----------
name
Name of dielectric function.
"""
def __init__(self,
name: str = 'DielectricFunction') -> None:
logger.debug(f'Entering {self.__class__.__name__}.__init__')
self._name = name
[docs]
def __call__(self, frequencies: Array) -> np.ndarray:
"""Returns the dielectric function at the given frequencies
Parameters
----------
frequencies
List of frequencies at which the dielectric function is returned;
in units of eV.
"""
freq_w = np.array(frequencies, dtype=float)
freq_w *= eV_to_au
return self._eval_at(freq_w)
@abstractmethod
def _eval_at(self, freq_w: np.ndarray) -> np.ndarray:
"""Returns the dielectric function at the given frequencies
Parameters
----------
frequencies
List of frequencies at which the dielectric function is returned;
in atomic units.
"""
raise NotImplementedError()
@abstractmethod
def __str__(self) -> str:
raise NotImplementedError()
@property
def name(self) -> str:
"""Name of dielectric function."""
return self._name
class AnalyticDielectricFunction(DielectricFunction):
"""Objects of this class hold different representations of dielectric functions that
have analytic form. The dielectric function can thus be evaluated at any frequency
without interpolation.
Parameters
----------
name
Name of dielectric function.
"""
def __init__(self,
name: str = 'AnalyticDielectricFunction') -> None:
logger.debug(f'Entering {self.__class__.__name__}.__init__')
super().__init__(name=name)
[docs]
class NumericDielectricFunction(DielectricFunction):
"""Objects of this class hold different representations of dielectric functions sampled
on a discrete grid of frequencies. The dielectric function can be evaluated at any frequency
within the supplied grid by linear interpolation.
Parameters
----------
frequencies
List of frequencies at which the dielectric function :attr:`dielectric_function`
is sampled; units are eV by default, optionally atomic units
(see :attr:`units`).
The list of frequencies and the corresponding dielectric function are sorted if the former
is not monotonically increasing.
dielectric_function
Complex dielectric function sampled at the frequencies :attr:`frequencies`.
units
`eV` to specify :attr:`freq_w` in eV, or `au` to specify in atomic units.
This parameter determines whether conversion should be performed during
initialization and has no effect on instance methods and variables.
name
Name of dielectric function.
Raises
------
ValueError
If :attr:`frequencies` or :attr:`dielectric_function` have zero length.
ValueError
If there is a mismatch in the length of :attr:`frequencies` and :attr:`dielectric_function`.
ValueError
If :attr:`units` is not one of the allowed values `eV` or `au`.
Examples
--------
The following snippet illustrates how a NumericDielectricFunction object can be constructed.
Here, the analytic Drude dielectric function of the free-electron gas is used.
To construct a dielectric function object from a numeric dielectric function the function
:func:`~strongcoca.response.read_dielec_function_file` can be used.
>>> import numpy as np
>>> from strongcoca.response.dielectric import NumericDielectricFunction
>>>
>>> omega_p = 3
>>> gamma = 0.5
>>> freq_w = np.linspace(0.1, 10)
>>> eps_w = 1 - omega_p**2 / (freq_w**2 + 1j*gamma*freq_w)
>>> dielec = NumericDielectricFunction(freq_w, eps_w, name='Drude-function')
>>> print(dielec)
---------------- NumericDielectricFunction -----------------
name : Drude-function
Frequency grid : 50 points between 0.10 and 10.00eV
frequencies (eV) : [ 0.1 0.30204 0.50408 ...
9.59592 9.79796 10. ]
dielectric_function : [-33.61538+1.73077e+02j
-25.37528+4.36618e+01j
-16.85366+1.77091e+01j ...
0.90253+5.07897e-03j
0.90649+4.77173e-03j
0.91022+4.48878e-03j]
>>> dense_freq_w = np.linspace(3, 4)
>>> print(dielec(dense_freq_w))
[0.02425453+0.16310249j 0.0367996 +0.15996402j 0.04934466+0.15682555j
0.06188972+0.15368708j 0.07443478+0.15054861j 0.08697984+0.14741014j
...
"""
def __init__(self,
frequencies: Union[Array, float],
dielectric_function: Union[ComplexArray, complex],
units: str = 'eV',
name: str = 'NumericDielectricFunction') -> None:
logger.debug(f'Entering {self.__class__.__name__}.__init__')
super().__init__(name=name)
if isinstance(frequencies, float):
frequencies = [frequencies]
if isinstance(dielectric_function, complex) or isinstance(dielectric_function, float):
dielectric_function = [dielectric_function]
freq_w = np.array(frequencies)
eps_w = np.array(dielectric_function, dtype=complex)
if units == 'eV':
freq_w = freq_w * eV_to_au
elif units != 'au':
raise ValueError(f"units has to be 'eV' or 'au', not '{units}'")
if len(freq_w) != len(eps_w):
raise ValueError(f'length of frequencies {len(freq_w)} must match length of '
f'dielectric function {len(eps_w)}')
if len(freq_w) == 0:
raise ValueError('length of frequencies and dielectric function must not be zero')
# np.diff works if input has at least length 2
if len(freq_w) > 1 and not np.all(np.diff(freq_w) > 0):
sort_w = np.argsort(freq_w)
freq_w = freq_w[sort_w]
eps_w = eps_w[sort_w]
self._freq_w = freq_w
self._eps_w = eps_w
def _eval_at(self, freq_w: np.ndarray) -> np.ndarray:
"""Returns the dielectric function at the given frequencies by interpolating the numeric
dielectric function.
Parameters
----------
frequencies
List of frequencies at which the dielectric function is returned;
in units of eV.
Raises
------
ValueError
If any component of :attr:`frequencies` is outside of the interval of the numeric
dielectric function.
"""
if np.min(freq_w) < self._freq_w[0]:
raise ValueError('Minimum of array frequencies is outside the lower bound of the'
'numeric dielectric function')
if np.max(freq_w) > self._freq_w[-1]:
raise ValueError('Maximum of array frequencies is outside the upper bound of the'
'numeric dielectric function')
eps_w = np.interp(freq_w, self._freq_w, self._eps_w)
return eps_w # type: ignore
def __str__(self) -> str:
fmt = ClassFormatter(self, pad=20)
fmt.append_class_name()
fmt.append_attr('name')
fmt.append_attr('Frequency grid', f'{self.n} points between {self.minfreq:.2f} and '
f'{self.maxfreq:.2f}eV')
fmt.append_attr('frequencies', unit='eV')
fmt.append_attr('dielectric_function')
return fmt.to_string()
@property
def n(self) -> int:
"""number of samples for the dielectric function
:attr:`~strongcoca.response.NumericDielectricFunction.dielectric_function`."""
return len(self._eps_w)
@property
def minfreq(self) -> float:
"""Lowest frequency :attr:`~strongcoca.response.NumericDielectricFunction.frequencies`
for which there is a value of the dielectric function; in units of eV."""
m = self._freq_w[0] * au_to_eV
assert isinstance(m, float)
return m
@property
def maxfreq(self) -> float:
"""Highest frequency :attr:`~strongcoca.response.NumericDielectricFunction.frequencies`
for which there is a value of the dielectric function; in units of eV."""
m = self._freq_w[-1] * au_to_eV
assert isinstance(m, float)
return m
@property
def frequencies(self) -> np.ndarray:
"""Frequencies corresponding to dielectric function
:attr:`~strongcoca.response.NumericDielectricFunction.dielectric_function` in units of eV;
array with shape {n}."""
return self._freq_w * au_to_eV # type: ignore
@property
def dielectric_function(self) -> np.ndarray:
"""Complex unitless dielectric function at frequencies
:attr:`~strongcoca.response.MieGansResponse.frequencies`; array with shape {n}."""
return self._eps_w
[docs]
class DrudeDielectricFunction(AnalyticDielectricFunction):
r"""Objects of this class represent the Drude dielectric function, which is defined for the
entire positive frequency domain. The function is implemented as
.. math::
\varepsilon_r(\omega) = 1 - \frac{\omega_p^2}{\omega^2 + i\gamma\omega}
where the plasma frequency :math:`\omega_p` determines where the real part of the dielectric
function is zero and :math:`\gamma` is a broadening.
Parameters
----------
plasma_frequency
The plasma frequency :math:`\omega_p`; units are eV by default, optionally atomic units
(see :attr:`units`).
broadening
Broadening of the dielectric function :math:`\gamma` units are eV by default, optionally
atomic units (see :attr:`units`).
name
Name of the dielectric function.
units
`eV` to specify :attr:`plasma_frequency` and :attr:`broadening` in eV, or `au` to specify
in atomic units.
This parameter determines whether conversion should be performed during
initialization and has no effect on instance methods and variables.
"""
def __init__(self,
plasma_frequency: float,
broadening: float,
units: str = 'eV',
name: str = 'DrudeDielectricFunction') -> None:
logger.debug(f'Entering {self.__class__.__name__}.__init__')
super().__init__(name=name)
omega_pl = plasma_frequency
gamma = broadening
if units == 'eV':
omega_pl *= eV_to_au
gamma *= eV_to_au
elif units != 'au':
raise ValueError(f"units has to be 'eV' or 'au', not '{units}'")
self._omega_pl = omega_pl
self._gamma = gamma
def _eval_at(self, freq_w: np.ndarray) -> np.ndarray:
eps_w = 1 - self._omega_pl**2 / (freq_w**2 + 1j*self._gamma*freq_w)
return eps_w # type: ignore
def __str__(self) -> str:
fmt = ClassFormatter(self, pad=15)
fmt.append_class_name()
fmt.append_attr('name')
fmt.append_attr('plasma_frequency', unit='eV')
fmt.append_attr('broadening', unit='eV')
return fmt.to_string()
@property
def plasma_frequency(self) -> float:
"""plasma frequency."""
return self._omega_pl * au_to_eV
@property
def broadening(self) -> float:
"""Broadening parameter."""
return self._gamma * au_to_eV
[docs]
def read_dielec_function_file(dielectric_function_file: Path_str,
frequency_column: int = 0,
dielectric_function_real_column: int = 1,
dielectric_function_imag_column: int = 2,
units: str = 'eV',
name: str = 'NumericDielectricFunction from file',
**np_loadtxt_kwargs,
) -> NumericDielectricFunction:
"""Parses a textfile with columns describing the dielectric function of a material.
Parameters
----------
dielectric_function_file
Path to the file containing the dielectic function.
frequency_column
Index of column containing the frequency vector in units of eV.
dielectric_function_real_column
Index of column containing the real part of the unitless dielectric function.
dielectric_function_imag_column
Index of column containing the imaginary part of the unitless dielectric function.
units
Units of the frequency column;
passed to :class:`strongcoca.response.NumericDielectricFunction`.
name
Passed to :class:`~strongcoca.response.NumericDielectricFunction`.
np_loadtxt_kwargs
Parameters passed to :func:`numpy.loadtxt`; useful parameters are `delimiter` or `comments`.
"""
freq_w, df_real_w, df_imag_w = np.loadtxt(dielectric_function_file,
usecols=[frequency_column,
dielectric_function_real_column,
dielectric_function_imag_column],
unpack=True,
**np_loadtxt_kwargs)
df_w = df_real_w + 1j*df_imag_w
df = NumericDielectricFunction(freq_w, df_w, units=units, name=name)
return df
[docs]
def read_dielec_function_from_refractive_index_file(
dielectric_function_file: Path_str,
frequency_column: int = 0,
refractive_index_column: int = 1,
extinction_coefficient_column: int = 2,
units: str = 'eV',
name: str = 'NumericDielectricFunction from file',
**np_loadtxt_kwargs,
) -> NumericDielectricFunction:
r"""Parses a textfile with columns describing the refractive index and extinction coefficient
of a material.
The refractive index :math:`n(\omega)` and extinction coefficient :math:`k(\omega)` are
related to the complex dielectric function :math:`\varepsilon_r(\omega)` by
.. math::
\varepsilon_r(\omega) = n^2(\omega) - k^2(\omega) + 2jn(\omega)k(\omega)
Parameters
----------
dielectric_function_file
Path to the file containing the dielectic function.
frequency_column
Index of column containing the frequency vector in units of eV.
refractive_index_column
Index of column containing the unitless refractive index.
extinction_coefficient_column
Index of column containing the unitless extinction coefficient.
units
Units of the frequency column;
passed to :class:`~strongcoca.response.NumericDielectricFunction`.
name
Passed to :class:`~strongcoca.response.NumericDielectricFunction`.
np_loadtxt_kwargs
Parameters passed to :func:`numpy.loadtxt`; useful parameters are `delimiter` or `comments`.
"""
freq_w, n_w, k_w = np.loadtxt(dielectric_function_file,
usecols=[frequency_column,
refractive_index_column,
extinction_coefficient_column],
unpack=True,
**np_loadtxt_kwargs)
df_w = n_w**2 - k_w**2 + 2j*n_w*k_w
df = NumericDielectricFunction(freq_w, df_w, units=units, name=name)
return df