import numpy as np
import tbmodels as tbm
import pythtb as ptb
import wannierberri as wberri
from . import _tbmodels
from . import _pythtb
from . import _wberri
import scipy.linalg as la
[docs]
class Model():
r"""
Generic :python:`Model` class from which :python:`Supercell` and :python:`FiniteModel` classes are constructed. Contains the functions which are needed to load spin, positions, Hamiltonian matrices, and other useful information.
Parameters
----------
tbmodel :
Model instance.
spinful :
Whether the model is spinful or not. Default is :python:`False`.
states_uc :
Number of states per unit cell (if the model is spinful, this should be twice the dimension of the basis). Default is :python:`None`, which means it is computed from :python:`tbmodel`.
Lx :
Number of unit cells repeated along the :math:`\mathbf{a}_1` direction. Default is ``1``.
Ly :
Number of unit cells repeated along the :math:`\mathbf{a}_2` direction. Default is ``1``.
file_spn :
Whether the file ``.spn`` is provided if the model is obtained from Wannier functions generated by Wannier90 code starting from a :math:`\Gamma`-only *ab initio* calculation. Default is :python:`False`.
n_occ :
Number of occupied states. This information is required *only* if the model is obtained from Wannier functions generated by Wannier90 code starting from a :math:`\Gamma`-only *ab initio* calculation. Otherwise, half-filling is automatically assumed.
.. warning::
This class should **not** be called explicitly by the user since it is already called from both :python:`Supercell` and :python:`FiniteModel` upon creation.
"""
def __init__(self, tbmodel = None, spinful : bool = False, states_uc : int = None, Lx : int = 1, Ly : int = 1,
file_spn : bool = False, n_occ : int = None):
# Store local variables
self.model = tbmodel
self.Lx = Lx
self.Ly = Ly
self.spinful = spinful
# Wannier tight-binding model variables
if isinstance(self.model, wberri.System_w90):
self.wannier = True
self.file_spn = file_spn
if n_occ == None:
raise NotImplementedError("Provide number of occupied states as n_occ.")
else:
self.wannier = False
self.file_spn = False
# Generate the positions of the atoms in the lattice
self.cart_positions = self._get_positions()
self.n_orb = self.cart_positions[:,0].size
self.dim_r = self._get_dim()
if self.wannier:
self.hamiltonian, self.data = self._get_hamiltonian()
self.n_occ = n_occ
else:
self.hamiltonian, self.n_occ = self._get_hamiltonian()
# Generate the spin matrices if the model is spinful
if self.spinful:
if self.wannier and self.file_spn:
self.eig, self.un_0 = self._diagonalize()
self.sz = self._read_spn()
else:
self.sz = self._get_spin()
else:
self.sz = None
# Generate the position matrices
self.r = []
for d in range (self.dim_r):
self.r.append( np.diag(self.cart_positions[:,d]) )
# Number of states per unit cell
if states_uc == None:
self.states_uc = self._calc_states_uc(self.model, self.spinful)
else:
self.states_uc = states_uc
# Disordered system
self.disordered = False
self._mask = self._initialize_mask()
#################################################
# Load functions
#################################################
def _get_positions(self):
r"""
Returns the cartesian coordinates of the states in the sample.
"""
if isinstance(self.model, tbm.Model):
return _tbmodels.get_positions(self.model)
elif isinstance(self.model, ptb.tb_model):
return _pythtb.get_positions(self.model, self.spinful)
elif isinstance(self.model, wberri.System_w90):
return _wberri.get_positions(self.model)
else:
raise NotImplementedError("Invalid model instance.")
def _get_hamiltonian(self):
r"""
Returns the hamiltonian matrix at the :math:`\Gamma`-point.
"""
if isinstance(self.model, tbm.Model):
gamma_point = np.zeros(self.dim_r)
return _tbmodels.get_hamiltonian(self.model, gamma_point)
elif isinstance(self.model, ptb.tb_model):
gamma_point = np.zeros(self.dim_r)
return _pythtb.get_hamiltonian(self.model, self.spinful, gamma_point, self.dim_r)
elif isinstance(self.model, wberri.System_w90):
return _wberri.get_hamiltonian(self.model)
else:
raise NotImplementedError("Invalid model instance.")
def _diagonalize(self):
"""
Returns the eigenvalues and the eigenstates of the Hamiltonian matrix at the :math:`Gamma`-point along rows.
"""
eig, u_n0 = la.eigh(self.hamiltonian)
u_n0 = u_n0.T
return eig, u_n0
def _get_spin(self):
r"""
Returns the spin matrix elements in the basis of tight-binding orbitals which are diagonal in the spin operator :math:`S_z`.
"""
if isinstance(self.model, tbm.Model) or isinstance(self.model, ptb.tb_model) or isinstance(self.model, wberri.System_w90):
return np.diag([1, -1] * (self.n_orb//2))
else:
raise NotImplementedError("Invalid model instance.")
def _read_spn(self):
"""
Returns the spin matrix elements in the basis of Wannier functions
"""
if isinstance(self.model, wberri.System_w90):
return _wberri.read_spn(self.model,self.data,self.un_0)
else:
raise NotImplementedError("Invalid model instance.")
def _get_dim(self):
r"""
Returns the real-space dimensionality of the model.
Parameters
----------
model :
Model instance.
"""
if isinstance(self.model, tbm.Model):
return self.model.dim
elif isinstance(self.model, ptb.tb_model):
return self.model._dim_r
elif isinstance(self.model, wberri.System_w90):
return(self.cart_positions[0,:].size)
else:
raise NotImplementedError("Invalid model instance.")
def _calc_states_uc(self, model, spinful):
r"""
Returns number of states per unit cell.
Parameters
----------
model :
Model instance.
spinful :
Whether the model is spinful or not.
"""
if isinstance(model, tbm.Model):
return _tbmodels.calc_states_uc(model)
elif isinstance(model, ptb.tb_model):
return _pythtb.calc_states_uc(model, spinful)
elif isinstance(model, wberri.System_w90):
return _wberri.calc_states_uc(model)
else:
raise NotImplementedError("Invalid model instance.")
def _initialize_mask(self):
r"""
Returns a list of :python:`True` values with length the dimension of the lattice.
"""
if isinstance(self.model, tbm.Model):
return _tbmodels.initialize_mask(self.model)
elif isinstance(self.model, ptb.tb_model):
return _pythtb.initialize_mask(self.model, self.spinful)
elif isinstance(self.model, wberri.System_w90):
return _wberri.calc_states_uc(self.model)
else:
raise NotImplementedError("Invalid model instance.")
#################################################
# Lattice functions
#################################################
[docs]
def add_onsite_disorder(self, w : float = 0, seed : int = None):
r"""
Add on-site Anderson disorder to the model. Anderson disorder consists in a diagonal term in the Hamiltonian :math:`\mathcal H_{Anderson}=\sum_i w^{}_i c_i^{\dagger}c^{}_i` where :math:`w^{}_i` are random variables uniformly distributed in the interval :math:`\bigl[-\frac{W}{2},\frac{W}{2} \bigr]`.
Parameters
----------
w :
Disorder amplitude, defining the extrema of the interval :math:`\bigl[-\frac{W}{2},\frac{W}{2} \bigr]`. Default is ``0``.
seed :
Seed to be used in the random number generation. Default is :python:`None`, which result in the default Numpy seed.
"""
if w != 0: self.disordered = True
# Set the seed for the random number generator
if seed is not None:
np.random.seed(seed)
if self.spinful:
spinstates = 2
else:
spinstates = 1
# Create an array of random numbers uniformly distributed in (-w/2, w/2)
d = 0.5 * w * (2 * np.random.rand(self.n_orb // spinstates) - 1.0)
dis = np.repeat(d,spinstates)
# This function returns None, modify self.hamiltonian
np.fill_diagonal(self.hamiltonian,self.hamiltonian.diagonal() + dis)
[docs]
def add_vacancies(self, vacancies_list):
r"""
Add vacancies in the system by removing a site in the lattice.
Parameters
----------
vacancies_list:
A list of [cell_x, cell_y, basis] pointing to the cell and atom to be removed. Multiple values at once are allowed. For instance, consider a :math:`L_x\times L_y` lattice with a basis of :math:`N_b` atoms, and :math:`M` sites to be removed. Then, the list :python:`[[np.random.randint(L_x), np.random.randint(L_y), np.random.randint(N_b)] for _ in range(M)]` can be given as argument.
.. note::
In order to enforce half-filling, if the model is not spinful, an even number of vacancies is required.
"""
ham = self.hamiltonian
pos = self.r
cart_pos = self.cart_positions.T
sz = self.sz
nocc = self.n_occ
norbs = self.n_orb
# Convert vacancies_list to internal indexing
if np.array(vacancies_list).shape == (3,):
vacancies = self._xy_to_index(vacancies_list[0], vacancies_list[1], vacancies_list[2])
else:
vacancies = []
for hl in vacancies_list:
vacancies.append(self._xy_to_index(hl[0], hl[1], hl[2]))
# Remove the selected sites
if np.array(vacancies).shape == ():
self._mask[vacancies] = False
if not self.spinful:
raise RuntimeError("Adding an odd number of vacancies causes the model to no longer be half-filled.")
else:
self._mask[vacancies + 1] = False
for _ in range(2):
ham = np.delete(ham, vacancies, 0)
ham = np.delete(ham, vacancies, 1)
for dim in range(len(self.r)):
pos[dim] = np.delete(pos[dim], vacancies, 0)
pos[dim] = np.delete(pos[dim], vacancies, 1)
cart_pos = np.delete(cart_pos, vacancies, 1)
sz = np.delete(sz, vacancies, 0)
sz = np.delete(sz, vacancies, 1)
nocc -= 1
norbs -= 2
else:
# Avoid relabelling
vacancies = np.sort(vacancies)[::-1]
# Check if the number of vacancies is odd if the model is not spinful
if (not self.spinful) and (len(vacancies) % 2 == 1):
raise RuntimeError("Adding an odd number of vacancies causes the model to no longer be half-filled.")
for h in vacancies:
self._mask[h] = False
if not self.spinful:
ham = np.delete(ham, h, 0)
ham = np.delete(ham, h, 1)
for dim in range(len(self.r)):
pos[dim] = np.delete(pos[dim], h, 0)
pos[dim] = np.delete(pos[dim], h, 1)
cart_pos = np.delete(cart_pos, h, 1)
nocc -= 0.5
norbs -= 1
else:
self._mask[h] = False
self._mask[h + 1] = False
for _ in range(2):
ham = np.delete(ham, h, 0)
ham = np.delete(ham, h, 1)
for dim in range(len(self.r)):
pos[dim] = np.delete(pos[dim], h, 0)
pos[dim] = np.delete(pos[dim], h, 1)
cart_pos = np.delete(cart_pos, h, 1)
sz = np.delete(sz, h, 0)
sz = np.delete(sz, h, 1)
nocc -= 1
norbs -= 2
self.hamiltonian = ham
self.sz = sz
self.n_occ = int(nocc)
self.disordered = True
self.cart_positions = cart_pos.T
[docs]
def make_heterostructure(self, model2, direction : int = 0, start : int = 0, stop : int = 0):
r"""
Create a heterostructure starting from :python:`model2`. The system will be split in the direction :python:`direction` starting from the unit cell whose index is :python:`start` and stopping at the cell indexed as :python:`end`.
Parameters
----------
model2 :
The model whose Hamiltonian has to be used when constructing the heterostructure.
direction :
Direction in which the splitting is realized, allowed values are ``0`` for the :math:`\mathbf{a}_1` direction or ``1`` for the :math:`\mathbf{a}_2` direction. Default is ``0``.
start :
Starting point for the splitting in the :python:`direction` direction. Default is ``0``.
end :
End point of the splitting in the :python:`direction` direction. Default is ``0``.
.. warning::
The current instance of the model will be modified.
"""
# Ensure start <= stop
if start > stop:
tmp = start
start = stop
stop = tmp
# Check input data are ok
if not (start >= 0 and start < (self.Lx if direction == 0 else self.Ly)):
raise RuntimeError("Start point value not allowed.")
if not (stop > 0 and stop < (self.Lx if direction == 0 else self.Ly)):
raise RuntimeError("End point value not allowed.")
if direction not in [0, 1]:
raise RuntimeError("Direction not allowed: insert 0 for 'x' and 1 for 'y'.")
if not issubclass(type(model2), Model):
raise RuntimeError("The two models must be instances of Model, Supercell or FiniteModel.")
if not (self.Lx == model2.Lx and self.Ly == model2.Ly and np.allclose(self.r, model2.r)):
raise RuntimeError("You can only build heterostructures of the same model.")
# Generate the Hamiltonian of the second model
hamilt_model2 = np.copy(model2.hamiltonian)
# Check if only the on-site terms are changed (if so, avoid checking all the matrix elements)
onsite_only = False
if np.allclose( self.hamiltonian - np.diag(np.diag(self.hamiltonian)), hamilt_model2 - np.diag(np.diag(hamilt_model2)) ):
onsite_only = True
# Remove on-site terms from the model
onsite1 = np.diag(self.hamiltonian).copy()
onsite2 = np.diag(hamilt_model2).copy()
self.hamiltonian -= np.diag(onsite1)
if direction == 0:
# Splitting along the x direction
ind = np.array([[(start + i) * self.Ly * self.states_uc + j * self.states_uc for j in range(self.Ly)] for i in range(stop - start + 1)]).flatten()
else:
# Splitting along the y direction
ind = np.array([[i * self.Ly * self.states_uc + start * self.states_uc + j * self.states_uc for j in range(stop - start + 1)] for i in range(self.Lx)]).flatten()
for i in ind:
for j in range(self.states_uc):
onsite1[i + j] = onsite2[i + j]
# Add the new on-site terms
self.hamiltonian += np.diag(onsite1)
# If other matrix element are changed
if not onsite_only:
# Indices of every atom in the selected cells, not only of the initial atom of the cell
indices = np.array([[i + j for j in range(self.states_uc)] for i in ind]).flatten()
# Cycle over the rows of the hopping matrix
for k in range(self.hamiltonian.shape[0]):
# Cycle over the columns of the hopping matrix
for l in range(self.hamiltonian.shape[1]):
if k == l: continue
# Hopping amplitudes
amplitude1 = self.hamiltonian[k][l]
amplitude2 = hamilt_model2[k][l]
if k in indices:
if np.absolute(amplitude2) < 1e-10: continue
self.hamiltonian[k][l] = amplitude2
else:
if np.absolute(amplitude1) < 1e-10: continue
self.hamiltonian[k][l] = amplitude1
#################################################
# Utility functions
#################################################
def _xy_to_index(self, cellx : int, celly : int, basis : int):
r"""
Convert :python:`[cell_x, cell_y, basis]` to the internal indexing of the lattice sites.
Parameters
----------
cellx :
Index of the unit cell along the :math:`\mathbf{a}_1` direction.
celly :
Index of the unit cell along the :math:`\mathbf{a}_2` direction.
basis :
Index of the atom in the basis of the lattice. This index is defined when creating the model.
Returns
-------
index :
The index of an atom in the lattice according to the internal indexing.
.. note::
The conventional internal indexing is the one also used in PythTB and TBmodels when dealing with supercells: the first index is the bottom-left corner of the lattice and the last one is the top-right. The indices increase first along the :math:`\mathbf{a}_2` direction, then along :math:`\mathbf{a}_1`.
"""
return self.Ly * self.states_uc * cellx + self.states_uc * celly + basis * (2 if self.spinful else 1)
def _xy_to_line(self, fixed_coordinate : str, xy : int):
r"""
Returns the indices of the sites of the lattice along the fixed direction :python:`fixed_coordinate` starting from :python:`xy`.
Parameters
----------
fixed_coordinate :
Coordinate that has to be kept fixed. If one is interested in the indices along the :math:`\mathbf{a}_1` direction, this has to be set :python:`'y'` else :python:`'x'`.
xy :
Index of the unit cell along the direction :python:`fixed_coordiante` from which the indices are returned.
Returns
-------
index_list :
The list of indices of the atoms in the lattice keeping fixed :python:`fixed_direction` from the cell :python:`xy`.
"""
if fixed_coordinate == 'x':
return np.array([self.states_uc * xy * self.Ly + i for i in range(self.states_uc * self.Ly) if self._mask[self.states_uc * xy * self.Ly + i]]).flatten().tolist()
elif fixed_coordinate == 'y':
indices = []
for i in range(self.Lx):
for j in range(self.states_uc):
if self._mask[self.states_uc * xy + self.states_uc * self.Ly * i + j]:
indices.append(self.states_uc * xy + self.states_uc * self.Ly * i + j)
return np.array(indices)
else:
raise RuntimeError("Direction not allowed, only 'x' or 'y'.")
#################################################
# Macroscopic average functions
#################################################
def _lattice_contraction(self, cutoff : float):
r"""
Defines which atomic sites must be contracted on one site, for each site of the lattice.
Parameters
----------
cutoff :
Real-space cutoff for the contraction window.
Returns
-------
contraction :
List of the indices of the atoms that are within cutoff from each other, per atom.
"""
contraction = []
# Diagonal position matrix elements
rx = self.cart_positions[:, 0]; ry = self.cart_positions[:, 1]
# Return True if current is close to trial within cutoff
def within_range(current, trial):
return True if (rx[current] - rx[trial]) ** 2 + (ry[current] - ry[trial]) ** 2 - cutoff ** 2 < 1e-6 else False
# Store the lattice sites closer than cutoff for each lattice site
for current in range(len(rx)):
contraction.append([trial for trial in range(len(rx)) if within_range(current, trial)])
return contraction
def _PBC_lattice_contraction(self, cutoff):
r"""
Defines which atomic sites must be contracted on one site, for each site of the lattice within PBCs (minimum convention image).
Parameters
----------
cutoff :
Real-space cutoff for the contraction window.
Returns
-------
contraction :
List of the indices of the atoms that are within cutoff from each other, per atom.
"""
contraction = []
# Compute the primitive lattice vectors
if isinstance(self.model, tbm.Model):
lvecs = np.copy(self.model.uc)
elif isinstance(self.model, ptb.tb_model):
lvecs = np.copy(self.model._lat)
else:
raise NotImplementedError("Invalid model instance.")
lvecs[0] /= self.Lx
lvecs[1] /= self.Ly
# Compute reduced coordinates
reds = []
linv = np.linalg.inv(lvecs)
for r in range(len(self.cart_positions)):
reds.append(np.dot(np.array([self.cart_positions[r, 0], self.cart_positions[r, 1]]), linv))
reds = np.array(reds)
rx = reds[:, 0]; ry = reds[:, 1]
# Return True if current is close to trial within cutoff (trial comprise minimum image convention)
def within_range(current, trial):
trialorbitals = np.array([
[rx[trial], ry[trial]], [rx[trial], ry[trial] + self.Ly], [rx[trial], ry[trial] - self.Ly], [rx[trial] + self.Lx, ry[trial]],
[rx[trial] + self.Lx, ry[trial] + self.Ly], [rx[trial] + self.Lx, ry[trial] - self.Ly], [rx[trial] - self.Lx, ry[trial]],
[rx[trial] - self.Lx, ry[trial] + self.Ly], [rx[trial] - self.Lx, ry[trial] - self.Ly]
])
for to in trialorbitals:
# Go back to real space coordinates to compute the distance
dr = np.dot(np.array([rx[current] - to[0], ry[current] - to[1]]), lvecs)
if np.dot(dr, dr) - cutoff ** 2 < 1e-6:
return True
return False
# Store the lattice sites closer than cutoff for each lattice site
for current in range(len(rx)):
contraction.append([trial for trial in range(len(rx)) if within_range(current, trial)])
return contraction
def _average_over_radius(self, vals, cutoff : float, contraction = None):
r"""
Evaluate the average of :python:`vals` according to a certain :python:`cutoff` in real space.
Parameters
----------
vals :
Values that have to be averaged.
cutoff :
Real space cutoff for the real space average.
contraction :
List of atoms that have to be considered in the macroscopic average, per atom. Default is :python:`None`, which means it is computed from the positions of the atoms in the lattice.
Returns
-------
return_vals :
List of averaged values per lattice site within a cutoff.
"""
return_vals = []
# If no contraction list is passed, it is computed from the lattice position (default OBC contraction)
if contraction is None:
contraction = self._lattice_contraction(cutoff)
# Macroscopic average within a certain radius = cutoff
for current in range(len(self.cart_positions[:, 0])):
tmp = [vals[ind] for ind in contraction[current]]
if not len(tmp) == 0:
return_vals.append(np.sum(tmp) / (len(tmp) / self.states_uc))
else:
raise RuntimeError("Unexpected error occourred in counting the neighbors of a lattice site, there are none.")
return np.array(return_vals)