Source code for effmass.analysis

#! /usr/bin/env python3
"""
A module for analysing the data contained in a :class:`Segment` object.

Contains the :class:`Segment` class and methods for calculating various definitions of the effective mass.
"""

import warnings
import numpy as np
from scipy import integrate
from effmass import extrema
from effmass import angstrom_to_bohr
from effmass import ev_to_hartree
from effmass import boltzmann
from effmass import q

warnings.simplefilter(action='ignore', category=FutureWarning)


def _check_poly_order(polyfit_order):
    """Raises an AssertionError if the order of the polynomial is less than 2.

    Args:
        polyfit_order (int): the order of the polynomial fit.

    Returns:
        None.
    """

    if polyfit_order < 2:
        raise ValueError("the order of the polynomial must be 2 or higher")


def _solve_quadratic(a, b, c):
    r"""
    Solves quadratic equation of the form :math:`ax^2+bx+c=0` for multiple values of c.
    If the determinant is more than 0 (two solutions), it always returns the larger root.

    Args:
        a (float): the coefficient of :math:`x^2`.
        b (float): the coefficient of :math:`x^1`.
        c (list(float)): a list of coefficients of :math:`x^0`.

    Returns:
        list(float): the root of the equation for each value of c.

    """
    det = [b**2 - 4 * a * item for item in c]
    x = []
    for d in det:
        if d < 0:
            x.append(np.nan)
        if d == 0:
            x.append(-b / (2 * a))
        if d > 0:
            x.append((-b + np.sqrt(d)) / (2 * a))
    return x


[docs]class Segment: """Class for segments of the bandstructure. A Segment contains data for a particular region of reciprocal space and particular band. Attributes: band (int): The band number of the segment (counting starts from 0). kpoint_indices (list(int)): The indices of :attr:`effmass.inputs.Data.kpoints` from which this Segment is formed. kpoints (array(float)): 2-dimensional array. Each row contains the fractional coordinates of a kpoint [kx,ky,kz]. A slice of :attr:`effmass.inputs.Data.kpoints`. cartesian_kpoints (array(float)): 2-dimensional array. Each row contains the cartesian coordinates (angstrom :math:`^{-1}`) of a kpoint. dk_angs (array(float)): 1-dimensional array which contains the distance (angstrom :math:`^{-1}`) between each kpoint and the extrema. dk_bohr (array(float)): 1-dimensional array which contains the distance (bohr :math:`^{-1}`) between each kpoint and the extrema. energies (array(float)): 1-dimensional array which contains the energy (eV) of each eigenstate. A slice of :attr:`effmass.inputs.Data.energies`. dE_eV (array(float)): 1-dimensional array which contains the difference in energy (hartree) between each kpoint and the extrema. dE_hartree (array(float)): 1-dimensional array which contains the difference in energy (eV) between each kpoint and the extrema. occupancy (array(float)): 2-dimensional array. Each row contains occupation number of the eigenstates for a particular band. A slice of :attr:`effmass.inputs.Data.occupancy`. direction (array(float)): 1-dimensional array with length 3. The direction between kpoints in the segment. band_type (str): The band type, determined by occupancy of the eigenstate. Argument choices are "conduction_band", "valence_band" or "unknown". If set to "unknown", some class methods will raise a warning and return None. fermi_energy (float): the fermi energy in eV. """
[docs] def __init__(self, Data, band, kpoint_indices): """Initialise an instance of the Segment class. Args: Data (Data): Data instance initialised from the bandstructure which contains the segment band (int): the band number of the segment kpoint_indices (list(int)): the kpoint indices of the segment Returns: None. """ self.band = band self.kpoint_indices = kpoint_indices self.kpoints = np.array([Data.kpoints[k] for k in kpoint_indices ]) # in units 2*pi/angstrom self.cartesian_kpoints = np.array([ np.dot(k, Data.reciprocal_lattice) for k in self.kpoints ]) # in units 1/Angstrom. Reciprocal lattice includes factor 2*pi. self.dk_angs = np.linalg.norm( self.cartesian_kpoints - self.cartesian_kpoints[0], axis=1) self.dk_bohr = np.divide( self.dk_angs, angstrom_to_bohr ) # divide as we are in reciprocal space --> units in inverse length self.energies = np.array( [Data.energies[band, k] for k in kpoint_indices]) # in units eV self.dE_eV = self.energies - self.energies[0] self.dE_hartree = np.multiply(self.energies - self.energies[0], ev_to_hartree) if Data.occupancy is not None: self.occupancy = np.array( [Data.occupancy[band, k] for k in kpoint_indices]) else: self.occupancy = None self.direction = extrema.calculate_direction(self.kpoints[1], self.kpoints[2]) self.fermi_energy = Data.fermi_energy self._VBM = Data.VBM self._CBM = Data.CBM self.band_type = self._band_type()
def __str__(self): """ Segment string method. Returns: A string containing the energy of the Segment extrema (referenced to the VBM) and the start- and end- points of the Segment in reciprocal space. """ energy_str = "{0:.2f}".format(self.energies[0] - self._VBM) start_str = str(np.round(self.kpoints[0], 3)) end_str = str(np.round(self.kpoints[-1], 3)) return energy_str + " eV; " + start_str + "-->" + end_str def _check_kanefit_points(self, polyfit_order=6): """Raises an AssertionError if there are not enough data points to fit a Kane dispersion. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. Returns: None. """ idx = self.explosion_index(polyfit_order=polyfit_order) assert idx > 3, "Unable to calculate alpha parameter as inflexion too close to extrema"
[docs] def explosion_index(self, polyfit_order=6): r""" This will find the index at which there is a change in sign of the second derivative. In the region of this point the first derivative will pass through zero and so the transport mass (:math:`\frac{1}{\delta E \delta k}`) will explode. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. Notes: This marks the point at which the Kane dispersion is definitely not valid, although it may be that the Kane dispersion is a poor approximation prior to this. """ dedk, d2edk2 = self.poly_derivatives( polyfit_order=polyfit_order, dk=self.dk_bohr, polyfit_weighting=False) sign = np.sign(d2edk2) signchange = ((np.roll(sign, 1) - sign) != 0).astype(int) signchange[0] = 0 if 1 in signchange: cutoff = np.where(signchange == 1)[0][0] else: cutoff = len(self.dE_eV) - 1 return cutoff
def _fermi_function(self, eigenvalue, fermi_level=None, temp=300): r""" Calculates the probability that an eigenstate is occupied using Fermi-Dirac statistics: ..math:: p=\frac{1}{e^{\frac{Delta E}{kT}+1} Args: eigenvalue (float): the energy (eV) of the eigenstate. fermi_level (float): the fermi level (eV). Returns: float: the probability that the eigenstate is occupied """ if temp <= 0: raise ValueError("temperature must be more than 0K") fermi_level = self.fermi_energy if fermi_level is None else fermi_level if self.band_type == "conduction_band": probability = (1 / ((np.exp( (eigenvalue - fermi_level) / (((boltzmann * temp) / q))) + 1))) elif self.band_type == "valence_band": probability = 1 - (1 / ((np.exp( (eigenvalue - fermi_level) / (((boltzmann * temp) / q))) + 1))) else: raise ValueError( "Unable to calculate fermi function as the band type is unknown. Please set the Segment.band_type attribute manually (options are \"valence_band\" or \"conduction_band\").") return probability
[docs] def weighting(self, fermi_level=None, temp=300): """Calculates a weighting for each kpoint using the Fermi-Dirac statistics. Args: quasi_fermi_level (float, optional): The fermi level to be used in Fermi-Dirac statistics. Defaults to :attr:`effmass.inputs.Segment.fermi_energy`. temp (float, optional): The temperature (K) to be used in Fermi-Dirac statistics. Defaults to 300. Returns: array(float): A 1-dimensional array which contains the weighting for each k-point. Notes: The weighting is relative only: constants will cancel out when using to weight least square fits or means. """ fermi_level = self.fermi_energy if fermi_level is None else fermi_level # weighting is given by the fermi function weighting = (np.array([ self._fermi_function(E, fermi_level=fermi_level, temp=temp) for E in self.energies ])) assert weighting.all() != 0, "Unable to assign a weighting to the dispersion as the Fermi-Dirac distribution at all kpoints is smaller than Python's double precision floats. Calculate band edge values using Segment.five_point_leastsq_effmass() or Segment.finite_difference_effmass()." return weighting
def _band_type(self): """Identifies the band type, determined by the occupancy of the first k-point in the segment. An occupancy of 1.0 or 2.0 returns "valence_band" (as :class:`~effmass.analysis.Segment` is in the valence band). An occupancy of 0.0 returns "conduction_band" (as :class:`~effmass.analysis.Segment` is in the conduction band). In the case of partial occupancy, "unknown" is returned. Args: None Returns: str: the band type of the segment, either "valence_band", "conduction_band" or "unknown". """ if self.energies[0] <= self._VBM: band_type = "valence_band" elif self.energies[0] >= self._CBM: band_type = "conduction_band" else: print( "The band type unknown. Please set the Segment.band_type attribute manually." ) band_type = "unknown" return band_type # The collection of methods below calculate the optical effective mass by integrating numerically the analytical # expression for the second derivative of the Kane dispersion multiplied # by a Fermi-Dirac weighting.
[docs] def mass_integration(self, fermi_level=None, temp=300, alpha=None, mass_bandedge=None, upper_limit=None): """Integrates the product of the fermi-dirac distribution, density of states and second derivative of kane dispersion along the one- dimensional slice of k-space defined by :class:`~effmass.analysis.Segment` (up to :meth:`~effmass.analysis.Segment.explosion_index`). Args: fermi_level (float, optional): Fermi level (eV) to be used in Fermi-dirac statistics. Defaults to :attr:`~effmass.analysis.Segment.fermi_energy`. temp (float, optional): The temperature (K) to be used in Fermi-Dirac statistics. Defaults to 300. alpha (float, optional): The alpha parameter of the Kane dispersion (hartree$^{-1}$) mass_bandedge: The mass at bandedge parameter of the Kane dispersion (units electron mass). upper_limit (float, optional): The integration upper limit (bohr$^{-1}$). Defaults to where the Kane quasi-linear dispersion is no longer valide, defined by :meth:`~effmass.analysis.Segment.explosion_index`. Returns: float: The optical effective mass (units of electron mass), defined as the inverse of the second derivative of a kane dispersion, weighted according to occupancy of available eigenstates (the product of density of states and the fermi-dirac distribution). Note: The sign of the alpha parameter and mass_bandedge are important. If these are negative (as would be expected for the valence band), then they must be passed as negative values to the function. """ alpha = self.alpha() if alpha is None else alpha mass_bandedge = self.kane_mass_band_edge( ) if mass_bandedge is None else mass_bandedge upper_limit = self.dk_bohr[ self.explosion_index()] if upper_limit is None else upper_limit fermi_level = self.fermi_energy if fermi_level is None else fermi_level result = integrate.quad( self._mass_integrand, 0, upper_limit, args=(fermi_level, temp, alpha, mass_bandedge)) return result
def _mass_integrand(self, k, fermi_level, temp, alpha, mass_bandedge): """Helper function for :meth:`~effmass.analysis.Segment.mass_integration`. A function for the weighted mass integrand. """ return self.fd(k, fermi_level, temp, alpha, mass_bandedge) * ( self._second_derivative_kane_dispersion(k, alpha, mass_bandedge))
[docs] def fd(self, k, fermi_level, temp, alpha, mass_bandedge): r""" Calculates the probability that an eigenstate of momentum k is occupied, using Fermi-Dirac statistics and assuming a Kane dispersion. Args: k: the momentum of the eigenstate (bohr:math:`^{-1}`). fermi_level (float, optional): Fermi level (eV) to be used in Fermi-dirac statistics. Defaults to :attr:`~effmass.analysis.Segment.fermi_energy`. temp (float, optional): The temperature (K) to be used in Fermi-Dirac statistics. Defaults to 300. alpha (float, optional): The alpha parameter of the Kane dispersion (hartree$^{-1}$) mass_bandedge: The mass at bandedge parameter of the Kane dispersion (units electron mass). Returns: float: The probability that the eigenstate is occupied. Note: The sign of the alpha parameter and mass_bandedge are important. If these are negative (as would be expected for the valence band), then they must be passed as negative values to the function. """ assert temp > 0, "temperature must be more than 0K" if self.band_type == "conduction_band": return (1 / ((np.exp((self.energies[0] + self._kane_dispersion( k, alpha, mass_bandedge) - fermi_level) / ( (boltzmann * temp) / q))) + 1)) elif self.band_type == "valence_band": return 1 - (1 / ((np.exp((self.energies[0] + self._kane_dispersion( k, alpha, mass_bandedge) - fermi_level) / ( (boltzmann * temp) / q))) + 1)) elif self.band_type == "unknown": raise ValueError("Unable to calculate fermi function as there is partial occupancy of the bands and the band type is unknown. Please set the Segment.band_type attribute manually (options are \"valence_band\" or \"conduction_band\").") else: raise ValueError( "Please set the Segment.band_type attribute (options are \"valence_band\" or \"conduction_band\")")
def _kane_dispersion(self, k, alpha, mass_bandedge): """Helper function for :meth:`~effmass.analysis.Segment.fd`. Analytic form of the kane dispersion. """ # returns in eV return (_solve_quadratic( alpha, 1, [(-k * k) / (2 * mass_bandedge)])[0]) / ev_to_hartree def _second_derivative_kane_dispersion(self, k, alpha, mass_bandedge): """Helper function for :meth:`~effmass.analysis.Segment.mass_integration`. Analytic form of the second derivative of the kane dispersion. """ # returns in eV return 1 / (mass_bandedge * ((1 + ( (2 * k * k * alpha) / (mass_bandedge)))**(3 / 2)))
[docs] def weight_integration(self, fermi_level=None, temp=300, alpha=None, mass_bandedge=None, upper_limit=None): """Integrates the product of the fermi-dirac distribution along the one-dimensional slice of k-space defined by :class:`~effmass.analysis.Segment` (up to :meth:`~effmass.analysis.Segment.explosion_index`). Args: fermi_level (float, optional ): Fermi level (eV) to be used in Fermi-dirac statistics. Defaults to :attr:`~effmass.analysis.Segment.fermi_energy`. temp (float, optional): The temperature (K) to be used in Fermi-Dirac statistics. Defaults to 300. upper_limit (float, optional): The integration upper limit (bohr$^{-1}$). Defaults to where the Kane quasi-linear dispersion is no longer valide, defined by :meth:`~effmass.analysis.Segment.explosion_index`. Returns: float: A normalisation factor for :meth:`~effmass.analysis.Segment.mass_integration`. Note: The sign of the alpha parameter and mass_bandedge are important. If these are negative (as would be expected for the valence band), then they must be passed as negative values to the function. """ alpha = self.alpha() if alpha is None else alpha mass_bandedge = self.kane_mass_band_edge( ) if mass_bandedge is None else mass_bandedge fermi_level = self.fermi_energy if fermi_level is None else fermi_level upper_limit = self.dk_bohr[ self.explosion_index()] if upper_limit is None else upper_limit result = integrate.quad( self._weight_integrand, 0, upper_limit, args=(fermi_level, temp, alpha, mass_bandedge)) return result
def _weight_integrand(self, k, fermi_level, temp, alpha, mass_bandedge): """Helper function for :meth:`~effmass.analysis.Segment.weight_integration`. A function for the weight integrand. """ return self.fd(k, fermi_level, temp, alpha, mass_bandedge)
[docs] def optical_effmass_kane_dispersion(self, fermi_level=None, temp=300, alpha=None, mass_bandedge=None, upper_limit=None): r""" Calculates the optical effective mass, with the dispersion approximated by a Kane quasi-linear function. This optical effective mass is defined as: ..math:: \frac{1}{m_o} = \frac{\int f(E_k(k),T) \frac{\delta^2 E_k(k)}{\delta k^2} dk}{\int f(E_k(k),T) dk} where the integral is along the one-dimensional slice of k-space defined by :class:`~effmass.analysis.Segment` (up to :meth:`~effmass.analysis.Segment.explosion_index`) and :math:`f(E_k,T)` is the Fermi-Dirac distribution. E_k(k) is the Kane dispersion: ..math:: \frac{\hbar ^2}{2m_{tb}} = E(1+\alpha E) where the transport mass at bandedge (:math:`m_{tb}`) is calculated using :meth:`effmass.analysis.Segment.kane_mass_band_edge` and the alpha parameter is calculated using :meth:`effmass.analysis.Segment.alpha`. Args: fermi_level (float, optional ): Fermi level (eV) to be used in Fermi-dirac statistics. Defaults to :attr:`~effmass.analysis.Segment.fermi_energy`. temp (float, optional): The temperature (K) to be used in Fermi-Dirac statistics. Defaults to 300. alpha (float, optional): The alpha parameter of the Kane dispersion (hartree$^{-1}$) mass_bandedge: The mass at bandedge parameter of the Kane dispersion (units electron mass). upper_limit (float, optional): The integration upper limit (bohr$^{-1}$). Defaults to where the Kane quasi-linear dispersion is no longer valide, defined by :meth:`~effmass.analysis.Segment.explosion_index`. Returns: float: The optical effective mass (units of electron mass) of the :class:`~effmass.analysis.Segment`. Note: The sign of the alpha parameter and mass_bandedge are important. If these are negative (as would be expected for the valence band), then they must be passed as negative values to the function. """ fermi_level = self.fermi_energy if fermi_level is None else fermi_level if ((fermi_level) > (self.energies[self.explosion_index()])): if (self.band_type == "conduction_band"): print( "Fermi level {} is beyond the energy range where the Kane dispersion is valid.". format(fermi_level)) if (fermi_level) < (self.energies[self.explosion_index()]): if self.band_type == "valence_band": print( "Fermi level {} is beyond the energy range where the Kane dispersion is valid.". format(fermi_level)) top = self.mass_integration( fermi_level=fermi_level, temp=temp, alpha=alpha, mass_bandedge=mass_bandedge, upper_limit=upper_limit) bottom = self.weight_integration( fermi_level=fermi_level, alpha=alpha, mass_bandedge=mass_bandedge, temp=temp, upper_limit=upper_limit) assert top[0] != 0, "Unable to calculate the optical effective mass as the Fermi-Dirac distribution at all kpoints is smaller than Python's double precision floats." return bottom[0] / top[0]
#### def _polynomial_function(self, polyfit_order=6, polyfit_weighting=True): """Helper function which constructs a polynomial function using a least squares fit to dispersion data.""" _check_poly_order(polyfit_order) # we know that there is symmetry between k and -k negative_dk = [-value for value in self.dk_bohr[::-1]] sym_dk = np.concatenate((negative_dk, self.dk_bohr[1:])) sym_dE = np.concatenate((self.dE_hartree[::-1], self.dE_hartree[1:])) if polyfit_weighting: # weight to enable a better fit for the values where it is # important weighting = self.weighting() else: weighting = np.ones(len(self.dE_hartree)) # as it needs same dimensions as x and y. W = np.append(weighting[::-1], weighting[1:]) W = np.sqrt( np.diag(W)) # to allow dot product between weight and matrix/y. # for explanation of the coefficient matrix and least squares fit see https://stackoverflow.com/questions/32260204/numpy-polyfit-passing-through-0 # for how to incorporate weighting see https://stackoverflow.com/questions/27128688/how-to-use-least-squares-with-weight-matrix-in-python # and https://stackoverflow.com/questions/19624997/understanding-scipys-least-square-function-with-irls # by eliminating the units matrix of the array we are forcing a zero # offset; the fit must pass through 0,0 as is physical coeff_matrix = np.vstack([ sym_dk**i for i in np.arange(polyfit_order + 1)[polyfit_order:0:-1] ]).T w_coeff_matrix = np.dot(W, coeff_matrix) w_sym_dE = np.dot(sym_dE, W) coeff = np.append(np.linalg.lstsq(w_coeff_matrix, w_sym_dE)[0], [0]) # remember to set zeroth power to 0! function = np.poly1d(coeff) # function = np.poly1d(np.polyfit(sym_dk,sym_dE,polyfit_order,w=W)) # ----> simple polyfit call for sanity's sake return function
[docs] def poly_derivatives(self, polyfit_order=6, polyfit_weighting=True, dk=None): """Constructs a polynomial function using a least squares fit to Segment dispersion data, then evaluates first and second order derivatives. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. polyfit_weighting (bool, optional): If True, polyfit will be weighted according to occupation of eigenstates. If False, no weighting will be used. dk (array, optional): distance (bohr :math:^{-1}) from extrema in reciprocal space at which to evaluate first and second order derivatives. Defaults to 100 points evenly distributed across the whole segment. Returns: tuple: A tuple containing a 1d array of first derivatives and 1d array of second derivatives, evaluated at dk: ([dedk],[d2edk2]) """ function = self._polynomial_function( polyfit_order=polyfit_order, polyfit_weighting=polyfit_weighting) if dk is None: dk = np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100) dedk = function.deriv(m=1)(dk) d2edk2 = function.deriv(m=2)(dk) return dedk, d2edk2
[docs] def poly_fit(self, polyfit_order=6, polyfit_weighting=True): """Constructs a polynomial function using a least squares fit to :class:`~effmass.analysis.Segment` dispersion data, then evaluates at 100 points evenly distributed across :attr:`~effmass.analysis.Segment.dk_bohr`. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. polyfit_weighting (bool, optional): If True, polyfit will be weighted according to occupation of eigenstates. If False, no weighting will be used. Returns: array: 1d array containing energies (hartree). """ function = self._polynomial_function( polyfit_order=polyfit_order, polyfit_weighting=polyfit_weighting) values = np.polyval( function, np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100)) return values
[docs] def optical_poly_effmass(self, polyfit_order=6): r""" Calculates the optical effective mass with a polynomial approximation to the dispersion This optical effective mass is defined as: ..math:: \frac{1}{m_o} = \frac{\sum_i f(E_i) g(k_i) \frac{\delta^2 E}{\delta k^2}|_i}{\sum f(E_i) g(k_i)} where the sum is over eigenstates i contained withing the :class:`~effmass.analysis.Segment`. :math:`f_(E_i)` is the probability of occupation (Fermi-Dirac statistics) and :math:`g(k_i)` is the density of states at that k-point. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. Returns: float: The optical effective mass (units of electron mass) of the :class:`~effmass.analysis.Segment`. """ inertial_mass = self.inertial_effmass( polyfit_order=polyfit_order, dk=self.dk_bohr) weighting = self.weighting() optical_mass = np.power( np.average(np.power(inertial_mass, -1), weights=weighting), -1) return optical_mass
[docs] def inertial_effmass(self, polyfit_order=6, dk=None, polyfit_weighting=False): r""" Calculates the inertial (curvature) effective mass (:math:`\frac{1}{\frac{\delta^2E}{\delta k^2}}`), evaluated at :attr:`~effmass.analysis.Segment.dk_bohr`. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. dk (array, optional): distance (bohr :math:^{-1}) from extrema in reciprocal space at which to evaluate second order derivatives. Defaults to 100 points evenly distributed across the whole segment. polyfit_weighting (bool, optional): If True, polyfit will be weighted according to occupation of eigenstates. If False, no weighting will be used. Returns: array(float). 1d array containing the conductivity effective mass (in units of electron rest mass) evaluated at the points specified in dk. """ dedk, d2edk2 = self.poly_derivatives( polyfit_order=polyfit_order, dk=dk, polyfit_weighting=polyfit_weighting) conductivity_mass = [(1 / x) for x in d2edk2] return conductivity_mass
[docs] def transport_effmass(self, polyfit_order=6, dk=None, polyfit_weighting=False): r""" Calculates the transport mass (:math:`\frac{k}{\delta E \delta k}`), evaluated at :attr:`~effmass.analysis.Segment.dk_bohr` . Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. dk (array, optional): distance (bohr :math:^{-1}) from extrema in reciprocal space at which to evaluate first order derivatives. Defaults to 100 points evenly distributed across the whole segment. polyfit_weighting (bool, optional): If True, polyfit will be weighted according to occupation of eigenstates. If False, no weighting will be used. Returns: array(float). 1d array containing the transport effective mass (in units of electron rest mass) evaluated at the points specified in dk. """ dedk, d2edk2 = self.poly_derivatives( polyfit_order=polyfit_order, dk=dk, polyfit_weighting=polyfit_weighting) # dk=0 for first term gives discontinuity if dk is None: dk = np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100) transport_mass = [(x / y) for x, y in zip(dk[1:], dedk[1:])] transport_mass = np.append(np.array([np.nan]), transport_mass) return transport_mass
[docs] def alpha(self, polyfit_order=6, truncate=True): r""" The transport mass (:math:`\frac{k}{\delta E \delta k}`) is calculated as a function of :attr:`~effmass.analysis.Segment.dk_bohr` and fitted to a straight line. The gradient of this line determines the alpha parameter which is used in the kane dispersion. See :meth:`effmass.analysis.Segment.transport_effmass`. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. truncate (bool, optional): If True, data only up to :meth:`effmass.analysis.Segment.explosion_index` is used. If False, alpha is calculated using data for the whole segment. Defaults to True. Returns: float: The alpha parameter (hartree :math:`^{-1}`). """ if truncate: self._check_kanefit_points(polyfit_order=polyfit_order) transport_mass = self.transport_effmass( polyfit_order=polyfit_order, dk=self.dk_bohr, polyfit_weighting=False) if truncate: idx = self.explosion_index(polyfit_order=polyfit_order) gradient, intercept = np.polyfit(self.dE_hartree[1:idx + 1], transport_mass[1:idx + 1], 1) else: gradient, intercept = np.polyfit(self.dE_hartree[1:], transport_mass[1:], 1) alpha = np.divide(gradient, 2 * intercept) return alpha
[docs] def kane_mass_band_edge(self, polyfit_order=6, truncate=True): r""" The transport mass (:math:`\frac{1}{\delta E \delta k}`) is calculated as a function of :attr:`~effmass.analysis.Segment.dk_bohr` and fitted to a straight line. The intercept of this line with the y-axis gives a transport mass at bandedge which is used as a parameter in the kane dispersion. See :meth:`effmass.analysis.Segment.transport_effmass`. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. truncate (bool, optional): If True, data only up to :meth:`effmass.analysis.Segment.explosion_index` is used. If False, alpha is calculated using data for the whole segment. Defaults to True. Returns: float: transport mass at bandedge (in units of electron mass). """ if truncate: self._check_kanefit_points(polyfit_order=polyfit_order) transport_mass = self.transport_effmass( polyfit_order=polyfit_order, dk=self.dk_bohr, polyfit_weighting=False) if truncate: idx = self.explosion_index(polyfit_order=polyfit_order) gradient, intercept = np.polyfit(self.dE_hartree[1:idx + 1], transport_mass[1:idx + 1], 1) else: gradient, intercept = np.polyfit(self.dE_hartree[1:], transport_mass[1:], 1) return intercept # intercept is the band edge transport mass
[docs] def kane_fit(self, polyfit_order=6): r""" Calculate the Kane quasi-linear dispersion parameters, then evaluates at 100 points evenly distributed across :attr:`~effmass.analysis.Segment.dk_bohr`. The Kane quasi-linear dispersion is described by ..math:: \frac{\hbar ^2 k^2}{2m_{tb}} = E(1+\alpha E) where the transport mass at bandedge (:math:`m_{tb}`) and the alpha parameter are calculated by fitting a linear function to the transport mass :math:`m_t` as a function of energy E. ..math:: m_t = m_{tb}(1+2\alpha E) The transport mass :math:`m_t` is calculated by approximating the dispersion with a polynomial function and taking the first derivative, see :meth:`~effmass.analysis.Segment.transport_effmass`. Args: polyfit_order (int, optional): order of polynomial used to approximate the dispersion. Defaults to 6. Returns: array: 1d array containing energies (hartree). """ self._check_kanefit_points(polyfit_order=polyfit_order) alpha = self.alpha(polyfit_order=polyfit_order) transport_mass_bandedge = self.kane_mass_band_edge( polyfit_order=polyfit_order) bandedge_energy = [ ((k**2)) / (2 * transport_mass_bandedge) for k in np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100) ] roots = _solve_quadratic(alpha, 1, [(-1) * ele for ele in bandedge_energy]) return roots
[docs] def finite_difference_effmass(self): """The curvature at the band edge is calculated using a second order forward finite difference method. This is then inverted to give an effective mass. Args: None Returns: float: Bandedge effective mass from finite difference (in units of electron mass). """ x = (self.dE_hartree[2] - (2 * self.dE_hartree[1])) / (self.dk_bohr[1]**2) mass = 1 / x return mass
[docs] def finite_difference_fit(self): r""" Calculates the curvature at the band edge using a finite difference method and then evaluates the corresponding quadratic dispersion along :attr:`~effmass.analysis.Segment.dk_bohr`. See :meth:`effmass.analysis.Segment.finite_difference_effmass`. Args: None Returns: list(float): list containing energies (hartree). The energies are calculated at 100 points evenly distributed across :attr:`~effmass.analysis.Segment.dk_bohr` using the quadratic approximation. """ m_bandedge = self.finite_difference_effmass() values = [((k**2) / (2 * m_bandedge)) for k in np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100)] return values
[docs] def weighted_leastsq_effmass(self): """Fits a parabolic dispersion using the weighted least-squares method to all points in the segment, plus those from symmetry, E(k)=E(-k). Args: None Returns: float: Curvature effective mass (in units of electron mass) Notes: weighting is given by the Fermi-Dirac distribution. """ # https://stackoverflow.com/questions/19624997/understanding-scipys-least-square-function-with-irls # https://stackoverflow.com/questions/27128688/how-to-use-least-squares-with-weight-matrix-in-python negative_dk = [-value for value in self.dk_bohr[::-1]] sym_dk = np.concatenate((negative_dk, self.dk_bohr[1:])) coeff_matrix = np.vstack([sym_dk**2]).T weighting = self.weighting() # as it needs same dimensions as x and y. W = np.append(weighting[::-1], weighting[1:]) W = np.sqrt( np.diag(W)) # to allow dot product between weight and matrix/y. sym_dE = np.concatenate((self.dE_hartree[::-1], self.dE_hartree[1:])) w_sym_dE = np.dot(sym_dE, W) w_coeff_matrix = np.dot(W, coeff_matrix) coeff = np.linalg.lstsq(w_coeff_matrix, w_sym_dE)[0] mass = 1 / (2 * coeff[0]) return mass
[docs] def weighted_leastsq_fit(self): """Calculates the curvature effective mass usinhag a weighted least- squares fit and then evaluates the corresponding parabolic dispersion along :attr:`~effmass.analysis.Segment.dk_bohr`. Args: None Returns: list(float): list containing energies (hartree). The energies are calculated at 100 points evenly distributed across :attr:`~effmass.analysis.Segment.dk_bohr`. """ m_bandedge = self.weighted_leastsq_effmass() values = [((k**2) / (2 * m_bandedge)) for k in np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100)] return values
[docs] def five_point_leastsq_effmass(self): """Fits a parabolic dispersion using the least-squares method to 5 points (3 DFT-calculated points + 2 from symmetry). Args: None Returns: float: Curvature effective mass (in units of electron mass). Notes: no weighting is used. """ sym_dk = np.array([ -self.dk_bohr[2], -self.dk_bohr[1], self.dk_bohr[0], self.dk_bohr[1], self.dk_bohr[2] ]) sym_dE = np.array([ self.dE_hartree[2], self.dE_hartree[1], self.dE_hartree[0], self.dE_hartree[1], self.dE_hartree[2] ]) coeff_matrix = np.vstack([sym_dk**2]).T coeff = np.linalg.lstsq(coeff_matrix, sym_dE)[0] mass = 1 / (2 * coeff[0]) return mass
[docs] def five_point_leastsq_fit(self): """Calculates the curvature effective mass using a parabolic least- squares fit and then evaluates the corresponding parabolic dispersion along :attr:`~effmass.analysis.Segment.dk_bohr`. Args: None Returns: list(float): list containing energies (hartree). The energies are calculated at 100 points evenly distributed across :attr:`~effmass.analysis.Segment.dk_bohr`. """ m_bandedge = self.five_point_leastsq_effmass() values = [((k**2) / (2 * m_bandedge)) for k in np.linspace(self.dk_bohr[0], self.dk_bohr[-1], 100)] return values
[docs]class SegmentVasp(Segment): """ Class for segments of a Vasp bandstructure. Inherits from :class:`~effmass.analysis.Segment` class, and extends to include DOS Segment data. Additional attributes: dos (array): 2-dimensional array. Each row contains density of states data (units "number of states / unit cell") at a given energy: [energy(float),dos(float)]. A slice of :attr:`effmass.inputs.DataVasp.dos`. integrated_dos (array): 2-dimensional array. Each row contains integrated density of states data at a given energy: [energy(float),integrated_dos(float)]. A slice of :attr:`effmass.inputs.DataVasp.integrated_dos`. """
[docs] def __init__(self, DataVasp, band, kpoint_indices): super().__init__(DataVasp, band, kpoint_indices) self.dos = self._dos(DataVasp) self.integrated_dos = self._integrated_dos(DataVasp)
def _dos(self, DataVasp): """Returns slice of :attr:`effmass.DataVasp.dos` corresponding to the energy range of the segment. Args: DataVasp (DataVasp): :class:`~effmass.inputs.DataVasp` instance which was used to generate the :class:`~effmass.analysis.SegmentVasp`. Returns: list(floats): slice of :attr:`effmass.DataVasp.dos` corresponding to the energy range of the segment. """ if DataVasp.dos == []: dos = [] else: dos = [] for i in range(len(self.energies)): for j in range(len(DataVasp.dos)): if self.energies[i] < DataVasp.dos[j][0]: dos.append(DataVasp.dos[j][1]) break return dos def _integrated_dos(self, DataVasp): """Returns slice of :attr:`effmass.DataVasp.integrated_dos` corresponding to the energy range of the segment. Args: DataVasp (DataVasp): :class:`~effmass.inputs.DataVasp` instance which was used to generate the :class:`~effmass.analysis.Segment`. Returns: array: slice of :attr:`effmass.DataVasp.integrated_dos` corresponding to the energy range of the segment. """ if DataVasp.integrated_dos == []: integrated_dos = [] else: integrated_dos = [] for i in range(len(self.energies)): for j in range(len(DataVasp.integrated_dos)): if self.energies[i] < DataVasp.integrated_dos[j][0]: integrated_dos.append(DataVasp.integrated_dos[j][1]) break return integrated_dos