Source code for effmass.extrema

#! /usr/bin/env python3

"""
A module for finding the band structure extrema and instantiating 
a :class:`Segment` object for each extrema point.

The extrema are found within an energy range given by the :class:`Settings` class.
Each `Segment` object contains data for kpoints within an energy range given by the :class:`Settings` class. 
"""

import math
import warnings
import numpy as np
import numpy.ma as ma
from effmass import analysis
from effmass import inputs


def _check_partial_occupancy(occupancy):
    """Raises warning if there is partial occupancy of bands.

    Args:
        occupancy (array): Array with shape (number_of_bands,number_of_kpoints). Each row contains occupation number of the eigenstates for a particular band. Values range from 0-1 (spin-polarised) or 0-2 (non-spin-polarised). See :attr:`effmass.inputs.Data.occupancy`.

    Returns:
        None.
    """
    if not np.all(np.in1d(occupancy,([0, 1, 2]))):
        warnings.warn(
            "You have partial occupation numbers in Data.occupancy. You should check that the attributes Data.VBM, Data.CBM and Data.fermi_energy are correct, and if not, set them manually."
        )


def _calc_CBM(occupancy, energy):
    """Finds the minimum unoccupied energy eigenstate (conduction band minimum)
    .

    Args:
        occupancy (array): occupancy of eigenstates with shape (number_of_kpoints, number_of_bands)
        energy (array): energy (eV) of eigenstates with shape (number_of_kpoints, number_of_bands)

    Returns:
        float: the minimum unoccupied energy (eV)
    """
    _check_partial_occupancy(occupancy)
    energy_unoccupied = ma.masked_where(occupancy > 0.5, energy)
    return np.amin(energy_unoccupied)


def _calc_VBM(occupancy, energy):
    """Finds the minimum unoccupied energy eigenstate (valence band maximum).

    Args:
        occupancy (array): occupancy of eigenstates with shape (number_of_kpoints, number_of_bands)
        energy (array): energy (eV) of eigenstates with shape (number_of_kpoints, number_of_bands)

    Returns:
        float: the minimum unoccupied energy (eV)
    """
    _check_partial_occupancy(occupancy)
    energy_occupied = ma.masked_where(occupancy < 0.5, energy)
    return np.amax(energy_occupied)


[docs]def calculate_direction(a, b): """Calculates the direction vector between two points. Args: a (list): the position vector of point a. b (list): the position vector of point b. Returns: array: The (unnormalised) direction vector between points a and b. The smallest magnitude of an element is 1 (eg: [1,1,2]). """ difference = np.subtract(a, b) if np.count_nonzero(difference) < 1: print("The two k-points are equal") return np.array([0, 0, 0]) # we need to find the smallest non-zero value within a-b a = np.array(a) b = np.array(b) direction_masked = ma.masked_equal( a - b, 0) # return array with invalid entries where values are equal direction_filled = ma.filled( direction_masked, 10 **6) # fill invalid elements of array with a large number s direction_absolute = np.absolute( direction_filled) # return absolute values of each element smallest = np.amin(direction_absolute) direction = ( b - a) / smallest # use the minimum absolute value as a divisor a-b if -1 in direction: direction = np.multiply(direction, -1) return direction
[docs]def change_direction(kpoints, kpoint_indices): """Finds the index of the kpoint (if any) where there is a change of direction in reciprocal space. Args: kpoints (array): array of kpoints with shape (number_of_kpoints, 3). Each row contains the fractional coordinates of a kpoint [kx,ky,kz]. See :attr:`effmass.inputs.Data.kpoints`. kpoint_indices (list (int)): the kpoint indices over which to check for change in direction Returns: int: the index of the kpoint where there is a change of direction. If there is no change of direction, returns None. """ new_direction = calculate_direction(kpoints[kpoint_indices[0]], kpoints[kpoint_indices[1]]) change_index = None for i, value in enumerate(kpoint_indices[:-1]): old_direction = new_direction new_direction = calculate_direction(kpoints[kpoint_indices[i]], kpoints[kpoint_indices[i + 1]]) if np.linalg.norm(new_direction - old_direction) > 0.005: change_index = i + 1 break return change_index
[docs]def calc_CBM_VBM_from_Fermi(Data, CBMVBM_search_depth=4.0): """ This function is used to find the CBM and VBM when there is no occupancy data. It relies upon the Fermi level being in the middle of the band gap. The CBMVBM_search_depth is refereced from the fermi energy. Args: DataASE (DataASE): instance of the :class:`DataASE` class. Returns: (float, float): A tuple containing the conduction band minimum and valence band maximum in eV. """ Data.CBM = Data.fermi_energy Data.VBM = Data.fermi_energy Settings = inputs.Settings(extrema_search_depth=CBMVBM_search_depth) CB_indices = find_CB_indices(Data, Settings) VB_indices = find_VB_indices(Data, Settings) CBM = min([Data.energies[i][j] for i,j in CB_indices]) VBM = max([Data.energies[i][j] for i,j in VB_indices]) return CBM, VBM
[docs]def get_minimum_indices(Data,extrema_search_depth): """Finds the kpoint indices and band indices of all minimum turning points in CB within `extrema_search_depth`. Args: Data (Data): instance of the :class:`Data` class. extrema_search_depth (float): energy in kT from bandedge over which to search for minima. Returns: array: A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each minimum in the CB band. """ energy_CB = ma.masked_where(Data.energies < Data.CBM, Data.energies) # returns array of energies within energy_range electrons_in_range = ma.masked_where( np.absolute(energy_CB - Data.CBM) > extrema_search_depth, energy_CB) CB_minima = ma.masked_where( _mark_minima(electrons_in_range) == 0, electrons_in_range) CB_min_indices = np.argwhere(CB_minima.mask == 0) return CB_min_indices
[docs]def get_maximum_indices(Data,extrema_search_depth): """Finds the kpoint indices and band indices of all maximum turning points in VB within `extrema_search_depth`. Args: Data (Data): instance of the :class:`Data` class. extrema_search_depth (float): energy in kT from bandedge over which to search for maxima. Returns: array: A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each maximum in the VB band. """ energy_VB = ma.masked_where(Data.energies > Data.VBM, Data.energies) # returns array of energies within energy_range holes_in_range = ma.masked_where( np.absolute(energy_VB - Data.VBM) > extrema_search_depth, energy_VB) VB_maxima = ma.masked_where( _mark_maxima(holes_in_range) == 0, holes_in_range) VB_max_indices = np.argwhere(VB_maxima.mask == 0) return VB_max_indices
[docs]def get_frontier_CB_indices(Data,CB_min_indices, degeneracy_condition): """Returns the indices of the lowest energy minima across the Brillouin Zone Args: Data (Data): instance of the :class:`Data` class. CB_min_indices (array(int)): A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each minimum in the CB band. Returns: frontier_indices (array(int)): A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each minimum in the frontier conduction band(s). """ frontier_indices = np.where(CB_min_indices[:, 0] == CB_min_indices[:, 0].min())[0] frontier_indices = CB_min_indices[frontier_indices] for band, kpoint in frontier_indices: i = 1 while math.isclose(Data.energies[band+i, kpoint],Data.energies[band, kpoint], abs_tol=degeneracy_condition): frontier_indices = np.append(frontier_indices, np.array([[band+i, kpoint]]), axis=0) i += 1 return frontier_indices
[docs]def get_frontier_VB_indices(Data,VB_max_indices, degeneracy_condition): """Returns the indices of the highest energy maxima across the Brillouin Zone Args: Data (Data): instance of the :class:`Data` class. VB_max_indices (array(int)): A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each maximum in the VB band. Returns: frontier_indices (array(int)): A 2-dimensional array. Each row contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each maximum in the frontier valence band(s). """ frontier_indices = np.where(VB_max_indices[:, 0] == VB_max_indices[:, 0].max())[0] frontier_indices = VB_max_indices[frontier_indices] for band, kpoint in frontier_indices: i = 1 while math.isclose(Data.energies[band-i, kpoint],Data.energies[band, kpoint], abs_tol=degeneracy_condition): frontier_indices = np.append(frontier_indices, np.array([[band-i, kpoint]]), axis=0) i += 1 return frontier_indices
[docs]def find_CB_indices(Data, Settings): """Finds the kpoint index and band index of the minimum energy turning points within :attr:`effmass.inputs.Settings.energy_range` of the conduction band minimum (:attr:`effmass.inputs.Data.CBM`). Return indices for the lowest energy CB only if `frontier_bands_only` is True. Args: Data (Data): instance of the :class:`Data` class. Settings (Settings): instance of the :class:`Settings` class. Returns: array: A 2-dimensional array. Contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each minima. """ if Settings.conduction_band is True: CB_min_indices = get_minimum_indices(Data, Settings.extrema_search_depth) if Settings.frontier_bands_only is True: # At a given k-point there may be multiple bands with highest energy CB_min_indices = get_frontier_CB_indices(Data, CB_min_indices, Settings.degeneracy_condition) else: CB_min_indices = None return CB_min_indices
[docs]def find_VB_indices(Data, Settings): """Finds the kpoint index and band index of the maximum energy turning points within :attr:`effmass.inputs.Settings.energy_range` of the valence band maximum (:attr:`effmass.inputs.Data.VBM`). Return indices for the highest energy VB only if `frontier_bands_only` is True. Args: Data (Data): instance of the :class:`Data` class. Settings (Settings): instance of the :class:`Settings` class. Returns: array: A 2-dimensional array. Contains [:attr:`efmmas.inputs.Data.bands` index, :attr:`effmass.inputs.Data.kpoints` index] for each maxima. """ VB_max_indices = get_maximum_indices(Data, Settings.extrema_search_depth) if Settings.frontier_bands_only is True: # At a given k-point there may be multiple bands with highest energy VB_max_indices = get_frontier_VB_indices(Data, VB_max_indices, Settings.degeneracy_condition) return VB_max_indices
# Returns an array of band numbers and k-points for extrema, selected according to user settings. # The first index differentiates between the valence band and conduction band. def _mark_maxima(holes_array): """Helper function which takes an array of hole energies and returns an array for masking those which are not maxima.""" # create array to append to not_maxima = [] width = holes_array.shape[1] height = holes_array.shape[0] # handle edge cases for band in range(0, height): if (holes_array[band, 1] >= holes_array[band, 0]): not_maxima.append([band, 0]) for band in range(0, height): if (holes_array[band, -2] >= holes_array[band, -1]): not_maxima.append([band, -1]) # find the bands where numbers either side aren't bigger (the maxima) # will also include inflexion but needed to get HSP's for band in range(0, height): for k in range(1, width - 1): if (holes_array[band, k - 1] >= holes_array[band, k] or holes_array[band, k + 1] >= holes_array[band, k]): not_maxima.append([band, k]) # Need to assign false after inspection # or it screws with the algorithm for row in not_maxima: holes_array[row[0], row[1]] = False # returns matrix with all non-maxima marked false return holes_array def _mark_minima(electrons_array): """Helper function which takes an array of electron energies and returns an array for masking those which are not minima.""" # create array to append to not_minima = [] width = electrons_array.shape[1] height = electrons_array.shape[0] # handle edge cases for band in range(0, height): if (electrons_array[band, 1] <= electrons_array[band, 0]): not_minima.append([band, 0]) for band in range(0, height): if (electrons_array[band, -2] <= electrons_array[band, -1]): not_minima.append([band, -1]) # find the bands where numbers either side aren't bigger (the maxima) for band in range(0, height): for k in range(1, width - 1): if (electrons_array[band, k - 1] <= electrons_array[band, k] or electrons_array[band, k + 1] <= electrons_array[band, k]): not_minima.append([band, k]) # Need to assign false after inspection # or it screws with the algorithm for row in not_minima: electrons_array[row[0], row[1]] = False # returns matrix with all non-maxima marked false return electrons_array def _within(band_index, kpoint_index, trial_kpoint_index, Settings, Data): """Helper function which checks whether trial_kpoint_index is less than :attr:`effmass.inputs.Settings.energy_range`. away from kpoint_index for a given band """ if (np.absolute(Data.energies[band_index, trial_kpoint_index] - Data.energies[band_index, kpoint_index]) ) < Settings.energy_range: return True else: return False def _kpoints_after(band_index, kpoint_index, Settings, Data): """Helper function that returns eigenstates which are 1) on the same band 2) before the given kpoint in the bandstructure route and 3) within :attr:`effmass.inputs.Settings.energy_range`.""" trial_kpoint_index = kpoint_index + 1 kpoint_after = [kpoint_index] while (trial_kpoint_index < Data.number_of_kpoints and _within(band_index, kpoint_index, trial_kpoint_index, Settings, Data) is True): kpoint_after.append(trial_kpoint_index) trial_kpoint_index = trial_kpoint_index + 1 return kpoint_after def _kpoints_before(band_index, kpoint_index, Settings, Data): """Helper function that returns eigenstates which are 1) on the same band 2) before the given eigenstate in the route through reciprocal space 3) within :attr:`effmass.inputs.Settings.energy_range`.""" trial_kpoint_index = kpoint_index - 1 kpoint_before = [kpoint_index] while trial_kpoint_index >= 0 and _within(band_index, kpoint_index, trial_kpoint_index, Settings, Data) is True: kpoint_before.append(trial_kpoint_index) trial_kpoint_index = trial_kpoint_index - 1 return kpoint_before
[docs]def get_kpoints_before(band_index, kpoint_index, Settings, Data, truncate_dir_change=True): """For a given eigenstate, finds eigenstates which 1) belong to the same band 2) come before the given eigenstate in the route through reciprocal space 3) are within :attr:`effmass.inputs.Settings.energy_range`. Args: band_index (int): index of :attr:`effmass.inputs.Data.bands`. kpoint_index (int): index of :attr:`effmass.inputs.Data.kpoints`. Settings (Settings): instance of the :class:`Settings` class. Data (Data): instance of the :class:`Data` class. truncate_dir_change (bool): If True, truncates eigenstates when there is a change in direction. If False, there is no truncation. Defaults to True. Returns: list(int): indices of :attr:`effmass.inputs.Data.kpoints`. """ kpoints_before = _kpoints_before(band_index, kpoint_index, Settings, Data) if len(kpoints_before) > 2: change_index = change_direction(Data.kpoints, kpoints_before) if change_index: if truncate_dir_change is True: kpoints_before = kpoints_before[:change_index] if len(kpoints_before) < 3: kpoints_before = None else: kpoints_before = None return kpoints_before
[docs]def get_kpoints_after(band_index, kpoint_index, Settings, Data, truncate_dir_change=True): """For a given eigenstate, finds eigenstates which 1) belong to the same band 2) come after the given eigenstate in the route through reciprocal space 3) are within :attr:`effmass.inputs.Settings.energy_range`. Args: band_index (int): index of :attr:`effmass.inputs.Data.bands`. kpoint_index (int): index of :attr:`effmass.inputs.Data.kpoints`. Settings (Settings): instance of the :class:`Settings` class. Data (Data): instance of the :class:`Data` class. truncate_dir_change (bool): If True, truncates eigenstates when there is a change in direction. If False, there is no truncation. Defaults to True. Returns: list(int): indices of :attr:`effmass.inputs.Data.kpoints`. """ kpoints_after = _kpoints_after(band_index, kpoint_index, Settings, Data) # now make sure that there is not a change in direction for this set of kpoints and that long enough if len(kpoints_after) > 2: change_index = change_direction(Data.kpoints, kpoints_after) if change_index: if truncate_dir_change is True: kpoints_after = kpoints_after[:change_index] if len(kpoints_after) < 3: kpoints_after = None else: kpoints_after = None return kpoints_after
def _dot_product(vector1, vector2): return np.dot(vector1, vector2) / (np.linalg.norm(vector1)*np.linalg.norm(vector2))
[docs]def filter_segments_by_direction(segment_list, direction): """Filter a list of Segments so that only those in a particular direction remain. Args: segment_list (list(Segment)): A list of :class:`Segment` objects. direction (array(float)): The direction array, length 3. Returns: segment_list (list(Segment)): A list of :class:`Segment` objects. """ return [segment for segment in segment_list if math.isclose(_dot_product(segment.direction, direction), 1)]
# need to test for equality of direction not magnitude of vector
[docs]def generate_segments(Settings, Data, bk=None, truncate_dir_change=True): """Generates a list of Segment objects. Args: Settings (Settings): instance of the :class:`Settings` class. Data (Data): instance of the :class:`Data` class. truncate_dir_change (bool): If True, truncates eigenstates when there is a change in direction. If False, there is no truncation. Defaults to True. bk (list(int)): To manually set an extrema point, in format [:attr:`effmass.inputs.Data.energies` row index, :attr:`effmass.inputs.Data.kpoints` row index]. Defaults to None. Returns: list(Segment): A list of :class:`Segment` objects. """ if bk: extrema_array = bk else: if Settings.valence_band is True and Settings.conduction_band is True: extrema_array = np.concatenate((find_VB_indices(Data, Settings),find_CB_indices(Data, Settings))) elif Settings.valence_band is True: extrema_array = find_VB_indices(Data, Settings) elif Settings.conduction_band is True: extrema_array = find_CB_indices(Data, Settings) kpoints_list = [] band_list = [] for band, kpoint in extrema_array: # flattened CB and VB arrays to a single array kpoints_before = get_kpoints_before( band, kpoint, Settings, Data, truncate_dir_change=truncate_dir_change) kpoints_after = get_kpoints_after( band, kpoint, Settings, Data, truncate_dir_change=truncate_dir_change) if kpoints_before: kpoints_list.append(kpoints_before) band_list.append(band) if kpoints_after: kpoints_list.append(kpoints_after) band_list.append(band) segment_list = [ analysis.Segment(Data, band, kpoints) for band, kpoints in zip(band_list, kpoints_list) ] if Settings.direction: segment_list = filter_segments_by_direction(segment_list,np.array(Settings.direction)) return segment_list