# -*- coding: utf-8 -*-
"""
Created on Sat Jan 16 2021
@author: Logan Lang
@author: Pedram Tavadze
@author: Freddy Farah
"""
from typing import List
import itertools
import copy
from scipy.interpolate import CubicSpline
import numpy as np
import networkx as nx
from matplotlib import pylab as plt
import pyvista
from .kpath import KPath
from .brillouin_zone import BrillouinZone
from ..utils import mathematics
from pyprocar.utils.unfolder import Unfolder
from pyprocar.utils import LOGGER
HBAR_EV = 6.582119 *10**(-16) #eV*s
HBAR_J = 1.0545718 *10**(-34) #eV*s
METER_ANGSTROM = 10**(-10) #m /A
EV_TO_J = 1.602*10**(-19)
FREE_ELECTRON_MASS = 9.11*10**-31 # kg
# TODO: Check hormonic average effective mass values
# TODO: Check method to calculate the bands integral
[docs]class ElectronicBandStructure:
"""This object stores electronic band structure informomration.
Parameters
----------
kpoints : np.ndarray
The kpoints array. Will have the shape (n_kpoints, 3)
bands : np.ndarray
The bands array. Will have the shape (n_kpoints, n_bands)
efermi : float
The fermi energy
projected : np.ndarray, optional
The projections array. Will have the shape (n_kpoints, n_bands, n_spins, norbitals,n_atoms), defaults to None
projected_phase : np.ndarray, optional
The full projections array that incudes the complex part. Will have the shape (n_kpoints, n_bands, n_spins, norbitals,n_atoms), defaults to None
kpath : KPath, optional
The kpath for band structure claculation, defaults to None
weights : np.ndarray, optional
The weights of the kpoints. Will have the shape (n_kpoints, 1), defaults to None
labels : List, optional
reciprocal_lattice : np.ndarray, optional
The reciprocal lattice vector matrix. Will have the shape (3, 3), defaults to None
shifted_to_efermi : bool, optional
Boolean to determine if the fermi energy is shifted, defaults to False
"""
[docs] def __init__(
self,
kpoints:np.ndarray,
bands:np.ndarray,
efermi:float,
n_kx:int=None,
n_ky:int=None,
n_kz:int=None,
projected:np.ndarray = None,
projected_phase:np.ndarray=None,
weights:np.ndarray=None,
kpath:KPath=None,
labels:List=None,
reciprocal_lattice:np.ndarray=None,
):
LOGGER.info('Initializing the ElectronicBandStructure object')
self._kpoints = kpoints
self._kpoints_cartesian = self.reduced_to_cartesian(kpoints,reciprocal_lattice)
self._bands = bands - efermi
self._efermi = efermi
self._projected = projected
self._projected_phase = projected_phase
self._reciprocal_lattice= reciprocal_lattice
self._weights = weights
self._kpath = kpath
self.is_mesh = True
if kpath is not None:
self.is_mesh = False
self.has_phase = False
if self.projected_phase is not None:
self.has_phase = True
self.labels = labels
self.ibz_kpoints = None
self.ibz_bands = None
self.ibz_projected = None
self.ibz_projected_phase = None
self.ibz_weights = None
self.initial_band_properties = ['bands','projected','projected_phase']
self.band_derived_properties = ['bands_gradient', 'bands_hessian', 'fermi_velocity', 'harmonic_average_effective_mass', 'fermi_speed']
self.band_dependent_properties = self.initial_band_properties + self.band_derived_properties
self.kpoint_properties = ['kpoints', 'weights']
self.initial_properties=self.kpoint_properties + self.initial_band_properties
self.all_mesh_properties = self.initial_properties + self.band_derived_properties + ['kpoints_cartesian']
# Initialize mesh properties
for prop in self.all_mesh_properties:
setattr(self, "_" + prop+"_mesh", None)
# Initialize array properties
for prop in self.band_derived_properties :
setattr(self, "_" + prop, None)
self._n_kx=n_kx
self._n_ky=n_ky
self._n_kz=n_kz
self._kx_map = None
self._ky_map = None
self._kz_map = None
# if self.is_mesh:
# self._sort_by_kpoints()
LOGGER.info('Subtracting Fermi Energy from Bands')
LOGGER.info(f'Is Mesh: {self.is_mesh}')
LOGGER.info(f'Fermi Energy: {self.efermi}')
LOGGER.info(f'Kpoints shape: {self.kpoints.shape}')
LOGGER.info(f'Bands shape: {self.bands.shape}')
if self.projected is not None:
LOGGER.info(f'Projected shape: {self.projected.shape}')
if self.projected_phase is not None:
LOGGER.info(f'Projected phase shape: {self.projected_phase.shape}')
if self.kpath is not None:
LOGGER.info(f"Kpath: {self.kpath}")
if self.labels is not None:
LOGGER.info(f"Kpath: {self.labels}")
if self.reciprocal_lattice is not None:
LOGGER.info(f"Reciprocal lattice: {self.reciprocal_lattice}")
if self.weights is not None:
LOGGER.info(f"Weights: {self.weights}")
LOGGER.info('Initialized the ElectronicBandStructure object')
@property
def nkpoints(self):
"""The number of k points
Returns
-------
int
The number of k points
"""
return self.kpoints.shape[0]
@property
def n_kx(self):
"""The number of unique kpoints in kx direction in the reduced basis"""
return self._n_kx
@property
def n_ky(self):
"""The number of unique kpoints in kx direction in the reduced basis"""
return self._n_ky
@property
def n_kz(self):
"""The number of unique kpoints in ky direction in the reduced basis"""
return self._n_kz
@property
def nbands(self):
"""The number of bands
Returns
-------
int
The number of bands
"""
return self.bands.shape[1]
@property
def natoms(self):
"""The number of atoms
Returns
-------
int
The number of atoms
"""
return self.projected.shape[2]
@property
def nprincipals(self):
"""The number of principal quantum numbers
Returns
-------
int
The number of principal quantum numbersk points
"""
return self.projected.shape[3]
@property
def norbitals(self):
"""The number of orbitals
Returns
-------
int
The number of orbitals
"""
return self.projected.shape[4]
@property
def nspins(self):
"""The number of spin channels
Returns
-------
int
The number of spin channels
"""
return self.projected.shape[5]
@property
def is_non_collinear(self):
"""Boolean to determine if this is a non-colinear calculation
Returns
-------
bool
Boolean to determine if this is a non-colinear calculation
"""
if self.nspins == 4:
return True
else:
return False
@property
def efermi(self):
return self._efermi
@efermi.setter
def efermi(self, value):
"""This is a setter for the efermi property.
If the efermi property gets changed, the bands_gradient and bands_hessian will be recalculated"""
self._efermi = value
@property
def kpoints(self):
"""Returns the kpoints in fractional basis
"""
return self._kpoints
@kpoints.setter
def kpoints(self, value):
"""This is a setter for the kpoints property.
If the kpoints property gets changed, the cartesian kpoints will be recalculated"""
self._kpoints = value
self._kpoints_cartesian = self.reduced_to_cartesian(self._kpoints,self._reciprocal_lattice)
self._n_kx=len(np.unique(self.kpoints[:,0]))
self._n_ky=len(np.unique(self.kpoints[:,1]))
self._n_kz=len(np.unique(self.kpoints[:,2]))
@property
def kpoints_cartesian(self):
return self._kpoints_cartesian
@kpoints_cartesian.setter
def kpoints_cartesian(self, value):
"""This is a setter for the kpoints_cartesian property.
If the kpoints_cartesian property gets changed, the reciprocal kpoints will be recalculated"""
self._kpoints_cartesian = value
@property
def bands(self):
return self._bands
@bands.setter
def bands(self, value):
"""This is a setter for the bands property.
If the bands property gets changed, the bands_gradient and bands_hessian will be recalculated"""
self._bands = value
# # If bands are changed, reset all the band derived properties
# for prop in self.band_derived_properties:
# setattr(self, "_" + prop, None)
@property
def projected(self):
return self._projected
@projected.setter
def projected(self, value):
"""This is a setter for the projected property.
If the projected property gets changed, the projected_gradient and projected_hessian will be recalculated"""
self._projected = value
@property
def projected_phase(self):
return self._projected_phase
@projected_phase.setter
def projected_phase(self, value):
"""This is a setter for the projected_phase property.
If the projected_phase property gets changed, the projected_gradient and projected_hessian will be recalculated"""
self._projected_phase = value
@property
def weights(self):
return self._weights
@weights.setter
def weights(self, value):
"""This is a setter for the weights property.
If the weights property gets changed, the projected_gradient and projected_hessian will be recalculated"""
self._weights = value
@property
def kpath(self):
return self._kpath
@kpath.setter
def kpath(self, value):
"""This is a setter for the kpath property.
If the kpath property gets changed, the projected_gradient and projected_hessian will be recalculated"""
self._kpath = value
@property
def kx_map(self):
if self._kx_map is None:
unique_x = np.unique(self.kpoints[:,0])
self._kx_map = {value: idx for idx, value in enumerate(unique_x)}
return self._kx_map
@property
def ky_map(self):
if self._ky_map is None:
unique_y = np.unique(self.kpoints[:,1])
self._ky_map = {value: idx for idx, value in enumerate(unique_y)}
return self._ky_map
@property
def kz_map(self):
if self._kz_map is None:
unique_z = np.unique(self.kpoints[:,2])
self._kz_map = {value: idx for idx, value in enumerate(unique_z)}
return self._kz_map
@property
def reciprocal_lattice(self):
return self._reciprocal_lattice
@property
def inv_reciprocal_lattice(self):
"""Returns the inverse of the reciprocal lattice"""
if self.reciprocal_lattice is not None:
return np.linalg.inv(self.reciprocal_lattice)
else:
print(
"Please provide a reciprocal lattice when initiating the Procar class"
)
return None
@property
def bands_gradient(self):
"""
Bands gradient is a numpy array that stores each band gradient a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,n_bands], where the second dimension represents d/dx,d/dy,d/dz
Returns
-------
np.ndarray
Bands fradient is a numpy array that stores each band gradient in a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,n_bands],
where the second dimension represents d/dx,d/dy,d/dz
"""
if self._bands_gradient is None:
self._bands_gradient = self.mesh_to_array(mesh=self.bands_gradient_mesh)
return self._bands_gradient
@property
def bands_hessian(self):
"""
Bands hessian is a numpy array that stores each band hessian in a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,3,n_bands],
where the second and third dimension represent d/dx,d/dy,d/dz
Returns
-------
np.ndarray
Bands hessian is a numpy array that stores each band hessian in a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,3,n_bands],
where the second and third dimension represent d/dx,d/dy,d/dz
"""
if self._bands_hessian is None:
self._bands_hessian = self.mesh_to_array(mesh=self.bands_hessian_mesh)
return self._bands_hessian
@property
def fermi_velocity(self):
"""
fermi_velocity is a numpy array that stores each fermi_velocity a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,n_bands], where the second dimension represents d/dx,d/dy,d/dz
Returns
-------
np.ndarray
fermi_velocity is a numpy array that stores each fermi_velocity in a list that corresponds to the self.kpoints
Shape = [n_kpoints,3,n_bands],
where the second dimension represents d/dx,d/dy,d/dz
"""
if self._fermi_velocity is None:
self._fermi_velocity = self.mesh_to_array(mesh=self.fermi_velocity_mesh)
return self._fermi_velocity
@property
def fermi_speed(self):
"""
fermi speed is a numpy array that stores each fermi speed a list that corresponds to the self.kpoints
Shape = [n_kpoints,n_bands]
Returns
-------
np.ndarray
fermi speed is a numpy array that stores each fermi speed
in a list that corresponds to the self.kpoints
Shape = [n_kpoints,n_bands],
"""
if self._fermi_speed is None:
self._fermi_speed = self.mesh_to_array(mesh=self.fermi_speed_mesh)
return self._fermi_speed
@property
def harmonic_average_effective_mass(self):
"""
harmonic average effective mass is a numpy array that stores
each harmonic average effective mass in a list that corresponds to the self.kpoints
Shape = [n_kpoints,n_bands],
Returns
-------
np.ndarray
harmonic average effective mass is a numpy array that stores
each harmonic average effective mass in a list that corresponds to the self.kpoints
Shape = [n_kpoints,n_bands],
"""
if self._harmonic_average_effective_mass is None:
self._harmonic_average_effective_mass=self.mesh_to_array(mesh=self.harmonic_average_effective_mass_mesh)
return self._harmonic_average_effective_mass
@property
def kpoints_mesh(self):
"""Kpoint mesh representation of the kpoints grid. Shape = [n_kx,n_ky,n_kz,3]
Returns
-------
np.ndarray
Kpoint mesh representation of the kpoints grid. Shape = [n_kx,n_ky,n_kz,3]
"""
if self._kpoints_mesh is None:
self._kpoints_mesh = self.array_to_mesh(self.kpoints,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._kpoints_mesh
@property
def kpoints_cartesian_mesh(self):
"""Kpoint cartesian mesh representation of the kpoints grid. Shape = [n_kx,n_ky,n_kz,3]
Returns
-------
np.ndarray
Kpoint cartesian mesh representation of the kpoints grid. Shape = [n_kx,n_ky,n_kz,3]
"""
if self._kpoints_cartesian_mesh is None:
self._kpoints_cartesian_mesh = self.array_to_mesh(self.kpoints_cartesian,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._kpoints_cartesian_mesh
@property
def bands_mesh(self,force_update=False):
"""
Bands mesh is a numpy array that stores each band in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands]
Returns
-------
np.ndarray
Bands mesh is a numpy array that stores each band in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands]
"""
if self._bands_mesh is None or force_update:
self._bands_mesh = self.array_to_mesh(self.bands,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._bands_mesh
@property
def projected_mesh(self):
"""
projected mesh is a numpy array that stores each projection in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins,n_atoms,n_orbitals]
Returns
-------
np.ndarray
Projection mesh is a numpy array that stores each projection in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins,n_atoms,n_orbitals]
"""
if self._projected_mesh is None:
self._projected_mesh = self.array_to_mesh(self.projected,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._projected_mesh
@property
def projected_phase_mesh(self):
"""
projected phase mesh is a numpy array that stores each projection phases in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins,n_atoms,n_orbitals]
Returns
-------
np.ndarray
projected phase mesh is a numpy array that stores each projection phases in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins,n_atoms,n_orbitals]
"""
if self._projected_phase_mesh is None:
self._projected_phase_mesh = self.array_to_mesh(self.projected_phase,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._projected_phase_mesh
@property
def weights_mesh(self):
"""
weights mesh is a numpy array that stores each weights in a mesh grid.
Shape = [n_kx,n_ky,n_kz,1]
Returns
-------
np.ndarray
weights mesh is a numpy array that stores each weights in a mesh grid.
Shape = [n_kx,n_ky,n_kz,1]
"""
if self._weights_mesh is None:
self._weights_mesh = self.array_to_mesh(self.weights,
nkx=self.n_kx,
nky=self.n_ky,
nkz=self.n_kz)
return self._weights_mesh
@property
def bands_gradient_mesh(self):
"""
Bands gradient mesh is a numpy array that stores each band gradient in a mesh grid.
Shape = [3,n_bands,n_kx,n_ky,n_kz], where the first dimension represents d/dx,d/dy,d/dz
Returns
-------
np.ndarray
Bands fradient mesh is a numpy array that stores each band gradient in a mesh grid.
Shape = [n_kx,n_ky,n_kz,3,n_bands],
where the first dimension represents d/dx,d/dy,d/dz
"""
if self._bands_gradient_mesh is None:
band_gradients=self.calculate_nd_scalar_derivatives(self.bands_mesh, self.reciprocal_lattice)
# print(np.array_equal(scalar_diffs,scalar_diffs_2))
# This is equivalent to the above
# n_i, n_j, n_k, n_bands, n_spins = self.bands_mesh.shape
# band_gradients_2 = np.zeros(( n_i, n_j, n_k ,n_bands, n_spins,3))
# scalar_diffs_2 = np.zeros(( n_i, n_j, n_k ,n_bands, n_spins,3))
# for i_band in range(n_bands):
# for i_spin in range(n_spins):
# band_gradients_2[:,:,:,i_band,i_spin,:]=self.calculate_scalar_gradient_2(self.bands_mesh[:,:,:,i_band,i_spin],
# reciprocal_lattice=self.reciprocal_lattice)
# scalar_diffs_2[:,:,:,i_band,i_spin,:]=self.calculate_scalar_diff_2(self.bands_mesh[:,:,:,i_band,i_spin])
band_gradients *= METER_ANGSTROM
self._bands_gradient_mesh = band_gradients
return self._bands_gradient_mesh
@property
def bands_hessian_mesh(self):
"""
Bands hessian mesh is a numpy array that stores each band hessian in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spin,3,3],
where the last two dimensions represent d/dx,d/dy,d/dz
Returns
-------
np.ndarray
Bands hessian mesh is a numpy array that stores each band hessian in a mesh grid.
Shape = [n_kx,n_ky,n_kzn_bands,n_spin,3,3],
where the first and second dimension represent d/dx,d/dy,d/dz
"""
if self._bands_hessian_mesh is None:
band_hessians=self.calculate_nd_scalar_derivatives(self.bands_gradient_mesh, self.reciprocal_lattice)
# This is equivalent to the previous code
# n_i, n_j, n_k, n_bands, n_spins, n_dim = self.bands_gradient_mesh.shape
# band_hessians = np.zeros(( n_i, n_j, n_k, n_bands, n_spins, 3, 3))
# for i_dim in range(n_dim):
# for i_band in range(n_bands):
# for i_spin in range(n_spins):
# # band_hessians[:,:,:,i_band,i_spin,i_dim,:] = self.calculate_scalar_gradient(scalar_mesh = self.bands_gradient_mesh[:,:,:,i_band,i_spin,i_dim],
# # mesh_grid=self.kpoints_cartesian_mesh)
# band_hessians[:,:,:,i_band,i_spin,i_dim,:] = calculate_scalar_differences_2(scalar_mesh = self.bands_gradient_mesh[:,:,:,i_band,i_spin,i_dim],
# transform_matrix=self.reciprocal_lattice)
band_hessians *= METER_ANGSTROM
self._bands_hessian_mesh = band_hessians
return self._bands_hessian_mesh
@property
def fermi_velocity_mesh(self):
"""
Fermi Velocity mesh is a numpy array that stores each Fermi Velocity in a mesh grid.
Shape = [n_bands,n_kx,n_ky,n_kz,3], where the first dimension represents d/dx,d/dy,d/dz
Returns
-------
np.ndarray
Fermi Velocity mesh is a numpy array that stores each Fermi Velocity in a mesh grid.
Shape = [n_bands,n_kx,n_ky,n_kz,3],
where the first dimension represents d/dx,d/dy,d/dz
"""
if self._fermi_velocity_mesh is None:
self._fermi_velocity_mesh = self.bands_gradient_mesh / HBAR_EV
return self._fermi_velocity_mesh
@property
def fermi_speed_mesh(self):
"""
Fermi speed mesh is a numpy array that stores each Fermi Velocity in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins],
Returns
-------
np.ndarray
Fermi speed mesh is a numpy array that stores each Fermi Velocity in a mesh grid.
Shape = [n_kx,n_ky,n_kz,n_bands,n_spins],
"""
if self._fermi_speed_mesh is None:
self._fermi_speed_mesh = np.linalg.norm(self.fermi_velocity_mesh,axis=-1)
return self._fermi_speed_mesh
@property
def harmonic_average_effective_mass_mesh(self):
"""
harmonic average effective mass mesh is a numpy array that stores each
harmonic average effective mass mesh in a mesh grid.
Shape = [n_bands,n_kx,n_ky,n_kz],
Returns
-------
np.ndarray
harmonic average effective mass mesh is a numpy array that stores
each harmonic average effective mass in a mesh grid.
Shape = [n_bands,n_kx,n_ky,n_kz],
"""
if self._harmonic_average_effective_mass_mesh is None:
# n_i, n_j, n_k, n_bands, n_spins, n_grad_1, n_grad_2, = self.bands_hessian_mesh.shape
# self._harmonic_average_effective_mass_mesh = np.zeros(shape=( n_i, n_j, n_k, n_bands, n_spins))
# print(self.bands_hessian_mesh.shape)
# for iband in range(n_bands):
# for ispin in range(n_spins):
# for k in range(n_k):
# for j in range(n_j):
# for i in range(n_i):
# hessian = self.bands_hessian_mesh[i,j,k,iband,ispin,...] * EV_TO_J / HBAR_J**2
# self._harmonic_average_effective_mass_mesh[i,j,k,iband,ispin] = harmonic_average_effective_mass(hessian)
self._harmonic_average_effective_mass_mesh = self.calculate_harmonic_average_effective_mass(self.bands_hessian_mesh* EV_TO_J/ HBAR_J**2 )
return self._harmonic_average_effective_mass_mesh
@property
def bands_integral(self):
return self.calculate_nd_scalar_integral(self.bands_mesh,self.reciprocal_lattice)
[docs] @staticmethod
def reduced_to_cartesian(kpoints,reciprocal_lattice):
if reciprocal_lattice is not None:
return np.dot(kpoints, reciprocal_lattice)
else:
print(
"Please provide a reciprocal lattice when initiating the Procar class"
)
return
[docs] @staticmethod
def cartesian_to_reduced(cartesian,reciprocal_lattice):
"""Converts cartesian coordinates to fractional coordinates
Parameters
----------
cartesian : np.ndarray
The cartesian coordinates. shape = [N,3]
reciprocal_lattice : np.ndarray
The reciprocal lattice vector matrix. Will have the shape (3, 3), defaults to None
Returns
-------
np.ndarray
The fractional coordinates. shape = [N,3]
"""
if reciprocal_lattice is not None:
return np.dot(cartesian, np.linalg.inv(reciprocal_lattice))
else:
print(
"Please provide a reciprocal lattice when initiating the Procar class"
)
return
[docs] @staticmethod
def array_to_mesh( array, nkx, nky, nkz):
"""
Converts a list to a mesh that corresponds to ebs.kpoints
[n_kx*n_ky*n_kz,...]->[n_kx,n_ky,n_kz,...]. Make sure array is sorted by lexisort
Parameters
----------
array : np.ndarray
The array to convert to a mesh
nkx : int
The number of kx points
nky : int
The number of ky points
nkz : int
The number of kz points
Returns
-------
np.ndarray
mesh
"""
prop_shape = (nkx, nky, nkz,) + array.shape[1:]
scalar_grid=array.reshape(prop_shape,order='C')
return scalar_grid
[docs] @staticmethod
def mesh_to_array(mesh):
"""
Converts a mesh to a list that corresponds to ebs.kpoints
[n_kx,n_ky,n_kz,...]->[n_kx*n_ky*n_kz,...]
Parameters
----------
mesh : np.ndarray
The mesh to convert to a list
Returns
-------
np.ndarray
lsit
"""
nkx, nky, nkz = mesh.shape[:3]
prop_shape = (nkx*nky*nkz,) + mesh.shape[3:]
array=mesh.reshape(prop_shape)
return array
[docs] @staticmethod
def calculate_nd_scalar_derivatives(scalar_array, reciprocal_lattice):
"""Transforms the derivatives to cartesian coordinates
(n,j,k,...)->(n,j,k,...,3)
Parameters
----------
derivatives : np.ndarray
The derivatives to transform
reciprocal_lattice : np.ndarray
The reciprocal lattice
Returns
-------
np.ndarray
The transformed derivatives
"""
letters=['a','b','c','d','e','f','g','h']
scalar_diffs=calculate_scalar_differences(scalar_array)
n_dim=len(scalar_diffs.shape[3:])-1
transform_matrix_einsum_string='ij'
dim_letters=''.join(letters[0:n_dim])
scalar_array_einsum_string='uvw' + dim_letters + 'j'
transformed_scalar_string='uvw' + dim_letters + 'i'
ein_sum_string=transform_matrix_einsum_string + ',' + scalar_array_einsum_string + '->' + transformed_scalar_string
print(ein_sum_string)
scalar_gradients=np.einsum(ein_sum_string, reciprocal_lattice, scalar_diffs)
return scalar_gradients
[docs] @staticmethod
def calculate_nd_scalar_integral(scalar_mesh,reciprocal_lattice):
"""Calculate the scalar integral"""
n1,n2,n3=scalar_mesh.shape[:3]
volume_reduced_vector=np.array([1,1,1])
volume_cartesian_vector=np.dot(reciprocal_lattice,volume_reduced_vector)
volume=np.prod(volume_cartesian_vector)
dv=volume/(n1*n2*n3)
scalar_volume_avg=calculate_scalar_volume_averages(scalar_mesh)
# Compute the integral by summing up the product of scalar values and the volume of each grid cell.
integral = np.sum(scalar_volume_avg * dv,axis=(0,1,2))
return integral
[docs] @staticmethod
def calculate_harmonic_average_effective_mass(hessian):
# letters=['a','b','c','d','e','f','g','h']
# scalar_diffs=calculate_scalar_differences(self.bands_gradient_mesh)
# n_dim=len(scalar_diffs.shape[3:])-1
# transform_matrix_einsum_string='ij'
# dim_letters=''.join(letters[0:n_dim])
# scalar_array_einsum_string='uvw' + dim_letters + 'j'
# transformed_scalar_string='uvw' + dim_letters + 'i'
# ein_sum_string=transform_matrix_einsum_string + ',' + scalar_array_einsum_string + '->' + transformed_scalar_string
# band_hessians=np.einsum(ein_sum_string, self.reciprocal_lattice, scalar_diffs)
# Calculate the trace of each 3x3 matrix along the last two axes
trace_tensor = np.trace(hessian, axis1=-2, axis2=-1)
# Calculate the harmonic average effective mass for each element
e_mass = 3 * trace_tensor**-1 / FREE_ELECTRON_MASS
return e_mass
[docs] @staticmethod
def interpolate_mesh_grid(self, mesh_grid,interpolation_factor=2):
"""This function will interpolate an Nd, 3d mesh grid
[...,nx,ny,nz]
Parameters
----------
mesh_grid : np.ndarray
The mesh grid to interpolate
"""
scalar_dims=mesh_grid.shape[:-3]
new_mesh=None
for i, idx in enumerate(np.ndindex(scalar_dims)):
# Creating slicing list. idx are the scalar dimensions, last 3 are the grid
mesh_idx = list(idx)
tmp = [slice(None)]*3
mesh_idx.extend(tmp)
new_grid = mathematics.fft_interpolate(mesh_grid[mesh_idx], interpolation_factor=interpolation_factor)
if i==0:
new_dim = np.append(scalar_dims,new_grid.shape,axis=0)
new_mesh=np.zeros(shape=(new_dim))
new_mesh[mesh_idx]=new_grid
return new_mesh
[docs] def update_weights(self, weights):
self.weights = weights
return
[docs] def ebs_ipr(self):
"""_summary_
Returns
-------
ret : list float
The IPR projections
"""
orbitals = np.arange(self.norbitals, dtype=int)
# sum over orbitals
proj = np.sum(self.projected[:, :, :, :, orbitals, :], axis=-2)
# keeping only the last principal quantum number
proj = proj[:, :, :, -1, :]
# selecting all atoms:
atoms = np.arange(self.natoms, dtype=int)
# the ipr is \frac{\sum_i |c_i|^4}{(\sum_i |c_i^2|)^2}
# mind, every c_i is c_{i,n,k} with n,k the band and k-point indexes
num = np.absolute(proj)**2
num = np.sum(num[:, :, atoms, :], axis=-2)
den = np.absolute(proj)**1 + 0.0001 # avoiding zero
den = np.sum(den[:, :, atoms, :], axis=-2)**2
IPR = num/den
return IPR
[docs] def ebs_ipr_atom(self):
"""
It returns the atom-resolved , pIPR:
pIPR_j = \\frac{\|c_j\|^4}{(\\sum_i \|c_i^2\|)^2}
Clearly, \\( \\sum_j pIPR_j = IPR \\).
Mind: \( c_i \) is the wavefunction \( c(n,k)_i \), in pyprocar we already
have density projections, \( c_i^2 \).
*THIS QUANTITY IS NOT READY FOR PLOTTING*, please prefer `self.ebs_ipr()`
Returns
-------
ret : list float
The IPR projections
"""
orbitals = np.arange(self.norbitals, dtype=int)
# sum over orbitals
proj = np.sum(self.projected[:, :, :, :, orbitals, :], axis=-2)
# keeping only the last principal quantum number
proj = proj[:, :, :, -1, :]
# selecting all atoms:
atoms = np.arange(self.natoms, dtype=int)
# the partial pIPR is \frac{|c_j|^4}{(\sum_i |c_i^2|)^2}
# mind, every c_i is c_{i,n,k} with n,k the band and k-point indexes
num = np.absolute(proj)**2
den = np.absolute(proj)
den = np.sum(den[:, :, atoms, :], axis=-2)**2
pIPR = num/den[:,:,np.newaxis,:]
# print('pIPR', pIPR.shape)
return pIPR
[docs] def ebs_sum(self,
atoms:List[int]=None,
principal_q_numbers:List[int]=[-1],
orbitals:List[int]=None,
spins:List[int]=None,
sum_noncolinear:bool=True):
"""_summary_
Parameters
----------
atoms : List[int], optional
List of atoms to be summed over, by default None
principal_q_numbers : List[int], optional
List of principal quantum numbers to be summed over, by default [-1]
orbitals : List[int], optional
List of orbitals to be summed over, by default None
spins : List[int], optional
List of spins to be summed over, by default None
sum_noncolinear : bool, optional
Determines if the projection should be summed in a non-colinear calculation, by default True
Returns
-------
ret : list float
The summed projections
"""
principal_q_numbers = np.array(principal_q_numbers)
if atoms is None:
atoms = np.arange(self.natoms, dtype=int)
if spins is None:
spins = np.arange(self.nspins, dtype=int)
if orbitals is None:
orbitals = np.arange(self.norbitals, dtype=int)
# sum over orbitals
ret = np.sum(self.projected[:, :, :, :, orbitals, :], axis=-2)
# sum over principle quantum number
ret = np.sum(ret[:, :, :, principal_q_numbers, :], axis=-2)
# sum over atoms
ret = np.sum(ret[:, :, atoms, :], axis=-2)
# sum over spins only in non collinear and reshaping for consistency (nkpoints, nbands, nspins)
# in non-mag, non-colin nspin=1, in colin nspin=2
if self.is_non_collinear and sum_noncolinear:
ret = np.sum(ret[:, :, spins], axis=-1).reshape(
self.nkpoints, self.nbands, 1
)
return ret
[docs] def unfold(self, transformation_matrix=None, structure=None):
"""The method helps unfold the bands. This is done by using the unfolder to find the new kpoint weights.
The current weights are then updated
Parameters
----------
transformation_matrix : np.ndarray, optional
The transformation matrix to transform the basis. Expected size is (3,3), by default None
structure : pyprocar.core.Structure, optional
The structure of a material, by default None
Returns
-------
None
None
"""
uf = Unfolder(
ebs=self, transformation_matrix=transformation_matrix, structure=structure,
)
self.update_weights(uf.weights)
return None
def _sort_by_kpoints(self):
"""Sorts the bands and projected arrays by kpoints"""
sorted_indices = np.lexsort((self.kpoints[:,2], self.kpoints[:,1], self.kpoints[:,0]))
for prop in self.initial_properties:
original_value = getattr(self, prop)
if original_value is not None:
setattr(self, prop, original_value[sorted_indices,...])
return None
[docs] def ibz2fbz(self, rotations,decimals=4):
"""Applys symmetry operations to the kpoints, bands, and projections
Parameters
----------
rotations : np.ndarray
The point symmetry operations of the lattice
decimals : int
The number of decimals to round the kpoints
to when checking for uniqueness
"""
if not self.is_mesh:
raise ValueError("This function only works for meshes")
n_kpoints = self.kpoints.shape[0]
n_rotations = rotations.shape[0]
# Calculate new shape for kpoints and all properties
total_points = n_kpoints * n_rotations
new_shape = (total_points, 3)
properties=self.initial_properties[2:]
# Initialize new arrays for kpoints and properties
new_kpoints = np.zeros(new_shape)
self.ibz_kpoints = self.kpoints
self.ibz_kpoints_cartesian = self.kpoints_cartesian
new_properties = {}
for prop in properties:
original_value = getattr(self, prop)
if original_value is not None:
setattr(self, f'ibz_{prop}', original_value.copy())
prop_shape = (total_points,) + original_value.shape[1:]
new_properties[prop] = np.zeros(prop_shape)
# Apply rotations and copy properties
for i, rotation in enumerate(rotations):
start_idx = i * n_kpoints
end_idx = start_idx + n_kpoints
# Rotate kpoints
rotated_kpoints = self.kpoints.dot(rotation.T)
new_kpoints[start_idx:end_idx] = rotated_kpoints
# Update properties
for prop in properties:
original_value = getattr(self, "ibz_" + prop)
if original_value is not None:
new_properties[prop][start_idx:end_idx] = original_value
# Apply boundary conditions to kpoints
new_kpoints = -np.fmod(new_kpoints + 6.5, 1) + 0.5
# Floating point error can cause the kpoints to be off by 0.000001 or so
# causing the unique indices to misidentify the kpoints
new_kpoints = new_kpoints.round(decimals=decimals)
_, unique_indices = np.unique(new_kpoints, axis=0, return_index=True)
# Update the object's properties keeping only the unique kpoints
self.kpoints = new_kpoints[unique_indices]
self.bz_kpoints = self.kpoints
self.bz_kpoints_cartesian = self.kpoints_cartesian
for prop in properties:
prop_value = getattr(self, prop)
if prop_value is not None:
setattr(self, prop, new_properties[prop][unique_indices])
setattr(self, "bz_" + prop, new_properties[prop][unique_indices])
self._sort_by_kpoints()
return None
[docs] def ravel_array(self,mesh_grid):
shape = mesh_grid.shape
mesh_grid=mesh_grid.reshape(shape[:-3] + (-1,))
mesh_grid=np.moveaxis(mesh_grid,-1,0)
return mesh_grid
[docs] def fix_collinear_spin(self):
"""
Converts data from two spin channels to a single channel, adjusting the spin down values to negatives. This is typically used for plotting the Density of States (DOS).
Parameters
----------
No parameters are required for this function.
Returns
-------
bool
Returns True if the function changed the data, False otherwise.
"""
print('old bands.shape', self.bands.shape)
if self.bands.shape[2] != 2:
return False
shape = list(self.bands.shape)
shape[1] = shape[1]*2
shape[-1] = 1
self.bands.shape = shape
print('new bands.shape', self.bands.shape)
if self.projected is not None:
print('old projected.shape', self.projected.shape)
self.projected[... , -1] = -self.projected[... , -1]
shape = list(self.projected.shape)
shape[1] = shape[1]*2
shape[-1] = 1
self.projected.shape = shape
print('new projected.shape', self.projected.shape)
return True
[docs] def reduce_kpoints_to_plane(self,k_z_plane,k_z_plane_tol):
"""
Reduces the kpoints to a plane
"""
i_kpoints_near_z_0 = np.where(np.logical_and(self.kpoints_cartesian[:,2] < k_z_plane + k_z_plane_tol,
self.kpoints_cartesian[:,2] > k_z_plane - k_z_plane_tol) )
for prop in self.initial_properties:
original_value = getattr(self, prop)
if original_value is not None:
setattr(self, prop, original_value[i_kpoints_near_z_0,...][0])
return None
[docs] def expand_kpoints_to_supercell(self):
supercell_directions=list(list(itertools.product([1, 0,-1], repeat=2)))
initial_kpoints=copy.copy(self.kpoints )
initial_property_values={}
# Do not use kpoints in intial properties
for prop in self.initial_properties[1:]:
initial_property_values[prop]=copy.copy(getattr(self, prop))
final_kpoints=copy.copy(initial_kpoints)
# final_bands=copy.copy(self.bands)
for supercell_direction in supercell_directions:
if supercell_direction != (0,0):
new_kpoints=copy.copy(initial_kpoints)
new_kpoints[:,0] = new_kpoints[:,0] + supercell_direction[0]
new_kpoints[:,1] = new_kpoints[:,1] + supercell_direction[1]
final_kpoints=np.append(final_kpoints,new_kpoints, axis=0)
# Do not use kpoints in intial properties
for prop in self.initial_properties[1:]:
original_value = getattr(self, prop)
if original_value is not None:
initial_value=initial_property_values[prop]
new_values=np.append(original_value, initial_value, axis=0)
setattr(self, prop, new_values)
self.kpoints=final_kpoints
self._sort_by_kpoints()
[docs] def expand_kpoints_to_supercell_by_axes(self, axes_to_expand=[0, 1, 2]):
# Validate input
if not set(axes_to_expand).issubset({0, 1, 2}):
raise ValueError("axes_to_expand must be a subset of [0, 1, 2]")
# Create supercell directions based on axes to expand
supercell_directions = list(itertools.product([1, 0, -1], repeat=len(axes_to_expand)))
initial_kpoints = copy.deepcopy(self.kpoints)
initial_property_values = {}
# Do not use kpoints in initial properties
for prop in self.initial_properties[1:]:
initial_property_values[prop] = copy.deepcopy(getattr(self, prop))
final_kpoints = copy.deepcopy(initial_kpoints)
for supercell_direction in supercell_directions:
if supercell_direction != tuple([0] * len(axes_to_expand)):
new_kpoints = copy.deepcopy(initial_kpoints)
for i, axis in enumerate(axes_to_expand):
new_kpoints[:, axis] += supercell_direction[i]
final_kpoints = np.append(final_kpoints, new_kpoints, axis=0)
# Do not use kpoints in initial properties
for prop in self.initial_properties[1:]:
original_value = getattr(self, prop)
if original_value is not None:
initial_value = initial_property_values[prop]
new_values = np.append(original_value, initial_value, axis=0)
setattr(self, prop, new_values)
self.kpoints = final_kpoints
self._sort_by_kpoints()
[docs] def reduce_bands_near_fermi(self, bands, tolerance=0.7):
"""
Reduces the bands to those near the fermi energy
"""
energy_level = 0
full_band_index = []
for iband in range(len(self.bands[0,:,0])):
fermi_surface_test = len(np.where(np.logical_and(self.bands[:,iband,0]>=energy_level-tolerance,
self.bands[:,iband,0]<=energy_level+tolerance))[0])
if fermi_surface_test != 0:
full_band_index.append(iband)
if bands:
full_band_index=bands
for prop in self.initial_band_properties:
original_value = getattr(self, prop)
if original_value is not None:
value = original_value[:,full_band_index,...]
setattr(self, prop, value)
return None
[docs] def plot_kpoints(
self,
reduced=False,
show_brillouin_zone=True,
color="r",
point_size=4.0,
render_points_as_spheres=True,
transformation_matrix=None,
):
"""This needs to be moved to core.KPath and updated new implementation of pyvista PolyData
This method will plot the K points in pyvista
Parameters
----------
reduced : bool, optional
Determines wether to plot the kpoints in the reduced or cartesian basis, defaults to False
show_brillouin_zone : bool, optional
Boolean to show the Brillouin zone, defaults to True
color : str, optional
Color of the points, defaults to "r"
point_size : float, optional
Size of points, defaults to 4.0
render_points_as_spheres : bool, optional
Boolean for how points are rendered, defaults to True
transformation_matrix : np.ndarray, optional, optional
Reciprocal Lattice Matrix, defaults to None
Returns
-------
None
None
"""
p = pyvista.Plotter()
if show_brillouin_zone:
if reduced:
brillouin_zone = BrillouinZone(
np.diag([1, 1, 1]), transformation_matrix,
)
brillouin_zone_non = BrillouinZone(np.diag([1, 1, 1]),)
else:
brillouin_zone = BrillouinZone(
self.reciprocal_lattice, transformation_matrix
)
brillouin_zone_non = BrillouinZone(self.reciprocal_lattice,)
p.add_mesh(
brillouin_zone.pyvista_obj,
style="wireframe",
line_width=3.5,
color="black",
)
p.add_mesh(
brillouin_zone_non.pyvista_obj,
style="wireframe",
line_width=3.5,
color="white",
)
if reduced:
kpoints = self.kpoints_reduced
else:
kpoints = self.kpoints_cartesian
p.add_mesh(
kpoints,
color=color,
point_size=point_size,
render_points_as_spheres=render_points_as_spheres,
)
if transformation_matrix is not None:
p.add_mesh(
np.dot(kpoints, transformation_matrix),
color="blue",
point_size=point_size,
render_points_as_spheres=render_points_as_spheres,
)
p.add_axes(
xlabel="Kx", ylabel="Ky", zlabel="Kz", line_width=6, labels_off=False
)
p.show()
def __str__(self):
ret = 'Enectronic Band Structure \n'
ret += '------------------------ \n'
ret += 'Total number of kpoints = {}\n'.format(self.nkpoints)
ret += 'Total number of bands = {}\n'.format(self.nbands)
ret += 'Total number of atoms = {}\n'.format(self.natoms)
ret += 'Total number of orbitals = {}\n'.format(self.norbitals)
return ret
def calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis):
"""Calculates the scalar differences over the
k mesh grid using central differences
Parameters
----------
scalar_mesh : np.ndarray
The scalar mesh. shape = [n_kx,n_ky,n_kz]
Returns
-------
np.ndarray
scalar_gradient_mesh shape = [n_kx,n_ky,n_kz]
"""
n=scalar_mesh.shape[axis]
# Calculate indices with periodic boundary conditions
plus_one_indices = np.arange(n) + 1
minus_one_indices = np.arange(n) - 1
plus_one_indices[-1] = 0
minus_one_indices[0] = n - 1
if axis==0:
return (scalar_mesh[plus_one_indices,...] - scalar_mesh[minus_one_indices,...])/2
elif axis==1:
return (scalar_mesh[:,plus_one_indices,:,...] - scalar_mesh[:,minus_one_indices,:,...])/2
elif axis==2:
return (scalar_mesh[:,:,plus_one_indices,...] - scalar_mesh[:,:,minus_one_indices,...])/2
def calculate_forward_averages_on_meshgrid_axis(scalar_mesh,axis):
"""Calculates the scalar differences over the
k mesh grid using central differences
Parameters
----------
scalar_mesh : np.ndarray
The scalar mesh. shape = [n_kx,n_ky,n_kz]
Returns
-------
np.ndarray
scalar_gradient_mesh shape = [n_kx,n_ky,n_kz]
"""
n=scalar_mesh.shape[axis]
# Calculate indices with periodic boundary conditions
plus_one_indices = np.arange(n) + 1
zero_one_indices = np.arange(n)
plus_one_indices[-1] = 0
if axis==0:
return (scalar_mesh[zero_one_indices,...] + scalar_mesh[plus_one_indices,...]) /2
elif axis==1:
return (scalar_mesh[:,zero_one_indices,:,...] + scalar_mesh[:,plus_one_indices,:,...]) /2
elif axis==2:
return (scalar_mesh[:,:,zero_one_indices,...] + scalar_mesh[:,:,plus_one_indices,...]) /2
def calculate_scalar_volume_averages(scalar_mesh):
"""Calculates the scalar averages over the k mesh grid in cartesian coordinates
"""
scalar_sums_i=calculate_forward_averages_on_meshgrid_axis(scalar_mesh,axis=0)
scalar_sums_j=calculate_forward_averages_on_meshgrid_axis(scalar_mesh,axis=1)
scalar_sums_k=calculate_forward_averages_on_meshgrid_axis(scalar_mesh,axis=2)
scalar_sums=(scalar_sums_i + scalar_sums_j + scalar_sums_k)/3
return scalar_sums
def calculate_scalar_differences(scalar_mesh):
"""Calculates the scalar gradient over the k mesh grid in cartesian coordinates
Uses gradient trnasformation matrix to calculate the gradient
scalar_differens are calculated by central differences
Parameters
----------
scalar_mesh : np.ndarray
The scalar mesh. shape = [n_kx,n_ky,n_kz,...,3]
"""
scalar_diffs_i=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=0)
scalar_diffs_j=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=1)
scalar_diffs_k=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=2)
scalar_diffs=np.array([scalar_diffs_i,scalar_diffs_j,scalar_diffs_k])
scalar_diffs=np.moveaxis(scalar_diffs,0,-1)
return scalar_diffs
def calculate_scalar_differences_2(scalar_mesh,transform_matrix):
"""Calculates the scalar gradient over the k mesh grid in cartesian coordinates
Uses gradient trnasformation matrix to calculate the gradient
scalar_differens are calculated by central differences
Parameters
----------
scalar_mesh : np.ndarray
The scalar mesh. shape = [n_kx,n_ky,n_kz,...,3]
"""
scalar_diffs_i=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=0)
scalar_diffs_j=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=1)
scalar_diffs_k=calculate_central_differences_on_meshgrid_axis(scalar_mesh,axis=2)
scalar_diffs=np.array([scalar_diffs_i,scalar_diffs_j,scalar_diffs_k])
scalar_diffs=np.moveaxis(scalar_diffs,0,-1)
scalar_diffs_2=np.einsum('ij,uvwj->uvwi', transform_matrix, scalar_diffs)
return scalar_diffs_2
# def reorder(self, plot=True, cutoff=0.2):
# nspins = self.nspins
# if self.is_non_collinear:
# nspins = 1
# # projected = np.sum(self.projected, axis=-1).reshape(self.nkpoints,
# # self.nbands,
# # self.natoms,
# # self.nprincipals,
# # self.norbitals,
# # nspins)
# projected = self.projected
# projected_phase = self.projected_phase
# else:
# projected = self.projected
# projected_phase = self.projected_phase
# new_bands = np.zeros_like(self.bands)
# new_projected = np.zeros_like(self.projected)
# new_projected_phase = np.zeros_like(self.projected_phase)
# for ispin in range(nspins):
# # DG = nx.Graph()
# # X = np.arange(self.nkpoints)
# # DG.add_nodes_from(
# # [
# # ((i, j), {"pos": (X[i], self.bands[i, j, ispin])})
# # for i, j in itertools.product(
# # range(self.nkpoints), range(self.nbands)
# # )
# # ]
# # )
# # pos = nx.get_node_attributes(DG, "pos")
# # S = np.zeros(shape=(self.nkpoints, self.nbands, self.nbands))
# for ikpoint in range(1, self.nkpoints):
# order = []
# for iband in range(self.nbands):
# prj = []
# idx = []
# for jband in range(self.nbands):# range(max(0, iband-2), min(iband+2, self.nbands-1)):
# psi_i = projected_phase[ikpoint, iband, :, :, :, ispin].flatten() # |psi(k,iband)>=\sum u(atom, n, ml)
# psi_j = projected_phase[ikpoint-1, jband, :, :, :, ispin].flatten() # |psi(k+1,jband)>
# psi_i /= np.absolute(psi_i).sum()
# psi_j /= np.absolute(psi_j).sum()
# # diff = np.absolute(psi_i-psi_j).sum()
# diff = np.absolute(np.vdot(psi_i, psi_j))
# prj.append(diff)
# if iband == jband and prj[-1] < cutoff:
# prj[-1] = cutoff
# idx.append(jband)
# jband = idx[np.argmax(prj)]
# self.bands[ikpoint, [iband, jband], ispin] = self.bands[ikpoint, [jband, iband], ispin]
# # for ikpoint in range(1, self.nkpoints):
# # order = []
# # for iband in range(self.nbands):
# # prj = []
# # idx = []
# # slope =
# # for jband in range(max(0, iband-2), min(iband+2, self.nbands-1)):
# # dK = np.linalg.norm(self.kpoints[ikpoint+1] -self.kpoints[ikpoint])
# # dE = self.bands[ikpoint + 1, jband, ispin]
# # - self.bands[ikpoint, iband, ispin]
# # )
# # dEdK = (dE / dK)
# # prj.append(dEdk)
# # if iband == jband and prj[-1] < cutoff:
# # prj[-1] = cutoff
# # idx.append(jband)
# # jband = idx[np.argmax(prj)]
# # self.bands[ikpoint, [iband, jband], ispin] = self.bands[ikpoint, [jband, iband], ispin]
# # prj = np.repeat(projected[ikpoint, iband, :, :, :, ispin].reshape(1,
# # self.natoms, self.nprincipals, self.norbitals), self.nbands, axis=0)
# # dK = np.linalg.norm(self.kpoints[ikpoint+1] -self.kpoints[ikpoint])
# # if dK == 0:
# # continue
# # prj = np.repeat(
# # np.sqrt(projected[ikpoint, iband, :, :, :, ispin].astype(np.complex_)).reshape(
# # 1, -1
# # ),
# # self.nbands,
# # axis=0,
# # )
# # prj /= np.linalg.norm(prj[0])
# # prj_1 = np.sqrt(
# # projected[ikpoint + 1, :, :, :, :, ispin].astype(np.complex_).reshape(
# # self.nbands, -1
# # )
# # )
# # prj_1 = prj_1.T / np.linalg.norm(prj_1, axis=1)
# # prj_1 = prj_1.T
# # # return prj, prj_1
# # # prod = prj*prj_1
# # # prod = prod.sum(axis=-1)
# # # prod = prod.sum(axis=-1)
# # # prod = prod.sum(axis=-1)
# # # return prj, prj_1
# # # # prod = np.exp(-1*prod* 1/(self.bands[ikpoint+1, :, ispin]-self.bands[ikpoint, iband, ispin]))
# # # prod = np.exp(-1/abs(prod))
# # # prod = np.exp(-1*abs(self.bands[ikpoint+1, :, ispin]-self.bands[ikpoint, iband, ispin]+e))
# # dots = np.array([np.vdot(x, y) for x, y in zip(prj, prj_1)])
# # diff = np.linalg.norm(prj - prj_1, axis=1)
# # dE = np.abs(
# # self.bands[ikpoint + 1, :, ispin]
# # - self.bands[ikpoint, iband, ispin]
# # )
# # dEdK = (dE / dK)
# # jband = np.argsort(diff)
# # counter = 0
# # # while jband[counter] in order:
# # # counter+=1
# # order.append(jband[counter])
# # # if iband !=0 and jband== 0:
# # # print(ikpoint, iband)
# # # print(iband, self.bands[ikpoint, iband, ispin], jband, self.bands[ikpoint, jband, ispin])
# # # diffs.append(diff)
# # # DG.add_weighted_edges_from([((ikpoint, iband),(ikpoint+1, x[0]),x[1]) for x in zip(range(self.nbands), prod)])
# # DG.add_edge((ikpoint, iband), (ikpoint + 1, jband[counter]))
# # if plot:
# # plt.figure(figsize=(16, 9))
# # nodes = nx.draw_networkx_nodes(
# # DG, pos, node_size=5, node_color=["blue", "red"][ispin])
# # edges = nx.draw_networkx_edges(
# # DG,
# # pos,
# # edge_color='red'
# # )
# # plt.show()
# # # if len(order) == 0:
# # # new_bands[ikpoint+1,:,ispin] = self.bands[ikpoint+1,:,ispin]
# # # new_projected[ikpoint+1, :, :, :, :, :] = self.projected[ikpoint+1, :, :, :, :, :]
# # # else :
# # # new_bands[ikpoint+1,:,ispin] = self.bands[ikpoint+1, order,ispin]
# # # new_projected[ikpoint+1, :, :, :, :, :] = self.projected[ikpoint+1, order, :, :, :, :]
# # # self.bands = new_bands
# # # self.projected = new_projected
# return