"""
Implementation of System class.
The System class extendes the Crystal class, and it
has the basic functionality for the other models to derive from it.
"""
from __future__ import annotations
from typing import Tuple, Union, List
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg
import matplotlib.pyplot as plt
import sys
from itertools import product
from tightbinder.crystal import Crystal
[docs]
class System(Crystal):
"""
System class provides all basic functionality to create any type of model. This class
serves as a base class for the actual models to inherit from. By itself it does not solve
any system as it lacks specific implementation of the Hamiltonian, which has to be provided
in the actual model implementation.
:param system_name: Name of the system. Defaults to None.
:param bravais_lattice: List with bravais lattice basis. Defaults to None.
:param motif: List of atoms with their positions and their species. Defaults to None.
:param crystal: Crystal object. Defaults to None.
"""
def __init__(self, system_name: str = None, bravais_lattice: Union[list, np.ndarray] = None,
motif: Union[list, np.ndarray] = None, crystal: Crystal = None):
if crystal is None:
super().__init__(bravais_lattice, motif)
else:
assert type(crystal) == Crystal
if bravais_lattice is not None or motif is not None:
print("Warning: Provided values for Bravais lattice or motif are disregarded" +
"in presence of crystal object")
super().__init__(crystal.bravais_lattice, crystal.motif)
self.system_name = system_name
self._norbitals = None
self._basisdim = None
self._filling = None
self._boundary = None
self.bonds = []
self._unit_cell_list = None
self.hamiltonian = None
self.first_neighbour_distance = None
self._matrix_type = "dense"
# TODO add flag for initialize_hamiltonian
# ####################################################################################
# #################################### Properties ####################################
# ####################################################################################
@property
def norbitals(self):
return self._norbitals
@norbitals.setter
def norbitals(self, norbitals):
assert type(norbitals) == list
self._norbitals = norbitals
@property
def filling(self):
return self._filling
@filling.setter
def filling(self, filling):
if filling < 0:
print("Error: filling attribute must be between 0 and norbitals")
else:
self._filling = filling
@property
def boundary(self):
return self._boundary
@boundary.setter
def boundary(self, boundary):
if boundary not in ["PBC", "OBC"]:
print('Error: Incorrect boundary option')
sys.exit(1)
self._boundary = boundary
@property
def basisdim(self):
return self._basisdim
@basisdim.setter
def basisdim(self, basisdim):
if basisdim < self.natoms:
raise ValueError(f"basisdim has to be at least equal to the number of atoms (motif, {self.natoms})")
self._basisdim = basisdim
@property
def matrix_type(self):
return self._matrix_type
@matrix_type.setter
def matrix_type(self, type):
if type not in ["dense", "sparse"]:
raise ValueError("Invalid matrix type. Must be either dense or sparse.")
self._matrix_type = type
@property
def unit_cell_list(self):
if self._unit_cell_list is None:
return []
for cell in self._unit_cell_list:
return self._unit_cell_list
# ####################################################################################
# ################################# Bonds/Neighbours #################################
# ####################################################################################
[docs]
def add_bond(self, initial: int, final: int, cell: Union[list, tuple, np.ndarray] = (0., 0., 0.), neigh: str = '1') -> None:
"""
Method to add a hopping between two atoms of the motif.
NB: The hopping has a specified direction, from initial to final. The HSince the final
Hamiltonian is computed taking into account hermiticity, it is not necessary to specify the hopping
in the other direction. (TODO: Change this, no longer working like this).
:param initial: Index of initial atom of the bond.
:param final: Index of final atom of the bond.
:param cell: Array with cell containing the final atom. Defaults to [0., 0., 0.].
:param neigh: String to denote if the bond corresponds to first neighbour, second, etc.
Defaults to '1'.
"""
hopping_info = [initial, final, cell, neigh]
self.bonds.append(hopping_info)
[docs]
def add_bonds(self, initial: List[int], final: List[int], cells: Union[list, np.ndarray] = None, neigh: List[str] = None) -> None:
"""
Same method as add_hopping but we input a list of hoppings at once.
:param initial: List of indices of initial atom of the bond.
:param final: List of indices of final atom of the bond.
:param cell: List of cell vectors containing the final atom. Defaults to None, which
corresponds to the origin cell.
:param neigh: List of strings to denote if the bonds corresponds to first neighbour, second, etc respectively.
Defaults to None, which corresponds to all being '1'.
"""
if len(initial) != len(final):
raise ValueError("Initial and final lists do not have same size")
if cells is None:
cells = np.zeros([len(initial), 3])
if neigh is None:
neigh = ['1']*len(initial)
else:
self.boundary = "PBC"
cells = np.array(cells)
for bond in zip(initial, final, cells, neigh):
self.add_bond(bond[0], bond[1], bond[2], bond[3])
[docs]
def compute_neighbour_distances(self, nni: int) -> List[float]:
"""
Method to compute neighbour distance up to neighbours nni.
:param nni: Maximum neighbour distance to consider. nni=5 mean that it
will find first, second up to fifth neighbours distances.
:return: List of neighbour distances.
"""
if self.boundary == "OBC":
near_cells = np.array([[0.0, 0.0, 0.0]])
else:
near_cells = generate_near_cells(self.bravais_lattice, nni)
neigh_distance = np.array([])
atoms = np.kron(np.array(self.motif)[:, :3], np.ones((near_cells.shape[0], 1))) + np.kron(np.ones((len(self.motif), 1)), near_cells)
for reference_atom in self.motif:
distance = np.linalg.norm(atoms - reference_atom[:3], axis=1)
distance = np.unique(distance)[1:] # Remove 0 from distances
neigh_distance = np.concatenate((neigh_distance, distance))
neigh_distance = neigh_distance.round(2)
neigh_distance = np.unique(neigh_distance)[:nni]
return neigh_distance
[docs]
def find_first_neighbours(self) -> list:
"""
Method to find the first neighbours of the atoms of the motif.
:return: List of the corresponding bonds.
"""
if self.boundary == "OBC":
near_cells = np.array([[0.0, 0.0, 0.0]])
else:
near_cells = generate_near_cells(self.bravais_lattice)
first_neigh_distance = self.compute_first_neighbour_distance(near_cells)
eps = 1E-2
atoms = np.copy(self.motif)
bonds = []
for n, reference_atom in enumerate(self.motif):
for cell in near_cells:
distance = np.linalg.norm(atoms[:, :3] + cell - reference_atom[:3], axis=1)
neigh_atoms_indices_max = np.where(distance <= first_neigh_distance + eps)[0]
neigh_atoms_indices_min = np.where(first_neigh_distance - eps < distance)[0]
neigh_atoms_indices = np.intersect1d(neigh_atoms_indices_max, neigh_atoms_indices_min)
for i in neigh_atoms_indices:
bonds.append([n, i, cell])
return bonds
[docs]
def find_neighbours(self, mode: str = "minimal", nn: int = 1, r: float = None) -> None:
"""
From a list of atoms (motif), it sets the list of bonds within the solid.
I.e.: Atom list -> List of neighbours/atom.
By default it will look for the minimal distance between atoms to determine first neighbours.
For amorphous systems the option radius is available to determine neighbours within a given radius R.
Boundary conditions can also be set, either PBC (default) or OBC.
:param mode: Search mode, can be either 'minimal' or 'radius'. Defaults to 'minimal'.
:param nn: Next neighbour, used to specify up to which neighbour there are hoppings if mode='minimal'
:param r: Value for radius sphere to detect neighbours if mode='radius'
"""
if self.bonds is not None:
self.bonds = []
eps = 1E-2
if mode is "radius":
if r is None:
raise Exception("Error: Search mode is radius but no r given, exiting...")
# TODO: Check if this is necessary
# min_cell_size = np.min(np.linalg.norm(self.bravais_lattice, axis=1))
# nn = int(r // min_cell_size + 1)
elif mode is "minimal" and r is not None:
print("Search mode is minimal but a radius was given (will not be used)")
# Prepare unit cells to loop over depending on boundary conditions
if self.boundary == "OBC":
near_cells = np.array([[0.0, 0.0, 0.0]])
else:
near_cells = generate_near_cells(self.bravais_lattice, nn)
# Determine neighbour distances up to nn
neigh_distances = self.compute_neighbour_distances(nn)
self.first_neighbour_distance = neigh_distances[0]
# print(f"Neighbour distances: {neigh_distances}")
if mode == "radius" and r < self.first_neighbour_distance:
print("Warning: Radius smaller than first neighbour distance")
# Look for neighbours
# First in mode='minimal'
atoms = np.copy(self.motif)
index = 0
if mode == 'minimal':
for n, reference_atom in enumerate(self.motif):
for cell in near_cells:
distance = np.linalg.norm(atoms[:, :3] + cell - reference_atom[:3], axis=1)
for nn, nn_distance in enumerate(neigh_distances):
neigh_atoms_indices_max = np.where(distance <= nn_distance + eps)[0]
neigh_atoms_indices_min = np.where(nn_distance - eps < distance)[0]
neigh_atoms_indices = np.intersect1d(neigh_atoms_indices_max, neigh_atoms_indices_min)
for i in neigh_atoms_indices:
self.add_bond(n, i, cell, str(nn + 1))
# Then in mode='radius'
else:
for n, reference_atom in enumerate(self.motif):
for cell in near_cells:
distance = np.linalg.norm(atoms[:, :3] + cell - reference_atom[:3], axis=1)
neigh_atoms_indices_max = np.where(distance <= r + eps)[0]
neigh_atoms_indices_min = np.where(eps < distance)[0]
neigh_atoms_indices = np.intersect1d(neigh_atoms_indices_max, neigh_atoms_indices_min)
for i in neigh_atoms_indices:
self.add_bond(n, i, cell)
print("Done")
[docs]
def find_disconnected_atoms(self) -> List[int]:
"""
Method to find which atoms do not have neighbours, i.e. they are disconnected from
the lattice.
:return: List of indices of disconnected atoms.
"""
all_bonds = self.__reconstruct_all_bonds()
initial_atoms = [initial_atom for initial_atom, _, _ in all_bonds]
disconnected_atoms = [atom for atom in range(self.natoms) if atom not in initial_atoms]
return disconnected_atoms
[docs]
def remove_disconnected_atoms(self):
"""
Method to remove disconnected atoms from the motif. Based on the method remove_atoms.
"""
disconnected_atoms = self.find_disconnected_atoms()
self.remove_atoms(disconnected_atoms)
def __reconstruct_all_bonds(self) -> list:
"""
Method to obtain all neighbours for all atoms since the find_neighbours method
only computes one-way hoppings (i.e. i->j and not j->i).
NB: Not necessary in principle since we now compute all explicitly.
:return: List with all bonds.
"""
all_bonds = self.bonds[:]
for bond in self.bonds:
initial, final, cell, nn = bond
all_bonds.append([final, initial, cell, nn])
return all_bonds
[docs]
@staticmethod
def atom_coordination_number(index: int, bonds: list) -> int:
"""
Method to find the coordination number for a specific atom in the crystal.
:param index: Index of atom whose coordination number we want to compute.
:param bonds: List of indices within the solid.
:return: Number of neighbours.
"""
neighbours = np.where(bonds[:, 0] == index)[0]
return len(neighbours)
[docs]
def coordination_number(self) -> Tuple[float, List[int]]:
"""
Method to find the coordination number of the solid. Returns coordination number of solid,
and a list with the different individual coordinations numbers found within the solid.
:return: Coordination number, and list of all individual coordination numbers found.
"""
# bonds = self.__reconstruct_all_bonds()
bonds = np.array(self.bonds, dtype=object)
coordination = 0
coordination_list = []
for index in range(self.natoms):
coordination_number = self.atom_coordination_number(index, bonds)
coordination += coordination_number
if coordination_number not in coordination_list:
coordination_list.append(coordination_number)
coordination_list = np.sort(coordination_list)
coordination /= self.natoms
return coordination, coordination_list
[docs]
def find_lowest_coordination_atoms(self, include_first_neighbours: bool = False) -> List[int]:
"""
Routine to determine the atoms of the solid with the lowest coordination atoms.
TODO: Define parameter to specify storing atoms with coordinations lower than
one specified.
:param include_first_neighbours: Includes in the list of atoms, also the first
neighbours of those found originally. Defaults to False.
:return: List of indices of atoms.
"""
# bonds = self.__reconstruct_all_bonds()
bonds = np.array(self.bonds, dtype=object)
if bonds.size == 0:
raise RuntimeError("Model must be initialized first")
coordination, coordination_list = self.coordination_number()
atoms = []
for index in range(self.natoms):
atom_coordination = self.atom_coordination_number(index, bonds)
if atom_coordination < coordination_list[-1]:
atoms.append(index)
if include_first_neighbours:
for atom in np.copy(atoms):
neighbours = np.where(bonds[:, 0] == atom)[0]
for index in neighbours:
neighbour = bonds[index][1]
if neighbour not in atoms:
atoms.append(neighbour)
return atoms
[docs]
def compute_first_neighbour_distance(self, near_cells: Union[list, np.ndarray] = None) -> float:
"""
Routine to compute the first neighbour distance.
DEPRECATED since there is compute_neighbour_distances which works up to n neighbour distances.
:param near_cells: List of unit cells to loop over. Defaults to None.
:return: First neighbour distance.
"""
if near_cells is None:
near_cells = generate_near_cells(self.bravais_lattice)
neigh_distance = 1E100
fixed_atom = self.motif[0][:3]
for atom, cell in product(self.motif, near_cells):
distance = np.linalg.norm(atom[:3] + cell - fixed_atom)
if 1E-4 < distance < neigh_distance:
neigh_distance = distance
self.first_neighbour_distance = neigh_distance
return neigh_distance
def _determine_connected_unit_cells(self) -> None:
"""
Method to calculate which unit cells connect with the origin from the neighbour list.
"""
unit_cell_list = [[0.0, 0.0, 0.0]]
if self.boundary == "PBC":
for bond in self.bonds:
unit_cell = list(bond[2])
if unit_cell not in unit_cell_list:
unit_cell_list.append(unit_cell)
self._unit_cell_list = unit_cell_list
[docs]
def identify_boundary(self, alpha: float = 0.4, ndim: int = 2, verbose: bool = False, corner_atoms: bool = False) -> np.ndarray:
"""
Method to obtain the indices of the outermost atoms of the motif, taking into account
boundary conditions. NB: Currently works only in 2D
:param alpha: This parameters tunes the shape of the boundary. For alpha=0,
the boundary is a convex hull; as alpha increases the boundary becomes more
concave. Default value is 0.4
:param ndim: Dimension of the expected boundary. Defaults to 2.
:param verbose: Boolean parameter to print indices of boundary atoms. Defaults to False.
:param cornes_atoms: Boolean to toggle detection of atoms that might be shared by the
periodic boundary and the open boundary.
:return: Indices of atoms in the boundary.
:raises AssertionError: Crystal dimension different from two raises error. Also
raises if bonds are not computed.
"""
# Requires having computed the bonds previously
if not self.bonds:
raise AssertionError("Bonds must be computed first")
bonds = self.bonds
edges = self.identify_motif_edges(alpha, ndim)
if not edges:
import copy
# If there are no edges, is because the system is effectively unidimensional,
# meaning that the Delaunay triangulation fails. Then consider to supercell to
# be able to triangulate.
bigger_system = copy.deepcopy(self).supercell(n1=2)
bigger_system.initialize_hamiltonian()
bonds = bigger_system.bonds
edges = bigger_system.identify_motif_edges(alpha, ndim)
boundary_atoms = [edge[i] for edge in edges for i in range(2)]
boundary_atoms = np.unique(boundary_atoms)
# Remove boundary atoms from the periodic boundary
# First compute atoms in the periodic boundary
periodic_boundary_atoms = []
for bond in bonds:
cell = bond[2]
if np.linalg.norm(cell) != 0:
periodic_boundary_atoms.append(bond[0])
periodic_boundary_atoms.append(bond[1])
periodic_boundary_atoms = np.unique(periodic_boundary_atoms)
boundary_atoms = np.setdiff1d(boundary_atoms, periodic_boundary_atoms)
# Detect atoms in the corner between periodic and finite boundary
if corner_atoms:
corner_atoms = []
for atom in boundary_atoms:
for bond in bonds:
if bond[0] == atom and bond[1] in periodic_boundary_atoms:
corner_atoms.append(bond[1])
corner_atoms = np.unique(corner_atoms)
boundary_atoms = np.concatenate([boundary_atoms, corner_atoms])
atoms = []
for atom in boundary_atoms:
if atom in range(len(self.motif)):
atoms.append(atom)
atoms = np.unique(atoms)
if verbose:
print(f"Atoms in boundary: {atoms}")
if len(atoms) == 0:
print("Warning: No boundary atoms found")
return np.array(atoms, dtype=int)
[docs]
def plot_wireframe(self, ax: plt.Axes = None, linewidth: float = 1, alpha: float = 0.5, color="black") -> None:
"""
Method to plot a wireframe of the system according to the detected bonds.
:param ax: Matplotlib axes object. If None, a new Axis object is created.
:param linewidth: Linewidth of the wireframe. Defaults to 1.
:param alpha: Sets wireframe transparency. Defaults to 0.5
:param color: Wireframe color. Defaults to black.
"""
if not self.bonds:
raise LookupError("plot_wireframe requires having computed the bonds of the system.")
if ax is None:
fig, ax = plt.subplots(1, 1)
for n, bond in enumerate(self.bonds):
x0, y0 = self.motif[bond[0], :2]
xneigh, yneigh = self.motif[bond[1], :2]
ax.plot([x0, xneigh], [y0, yneigh], "-", linewidth=linewidth, alpha=alpha, color=color)
# ####################################################################################
# ############################### System modifications ###############################
# ####################################################################################
[docs]
def supercell(self, update: bool = True, **ncells: dict) -> System:
"""
Routine to generate a supercell for a system using the number of cells
along each Bravais vectors specified in ncells.
:param update: Boolean parameter to update all of Crystal class atributes.
Defaults to True, only set to false when used inside other routines (reduce)
:param dict: Specify number of cells in some direction: n1, n2 and/or n3.
:return: Modified System object.
"""
if len(ncells) == 0:
print("Error: Reduce method must be called with at least one parameter (nx, ny or nz), exiting...")
sys.exit(1)
key_to_index = {"n1": 0, "n2": 1, "n3": 2}
for key in ncells.keys():
if key not in ["n1", "n2", "n3"]:
print("Error: Invalid input (must be n1, n2 or n3), exiting...")
sys.exit(1)
if key_to_index[key] > self.ndim:
print(f"Error: Axis {key} to reduce along not present (higher than system dimension)")
new_motif = self.motif
for n in range(1, ncells[key]):
motif_copy_displaced = np.copy(self.motif)
motif_copy_displaced[:, :3] += n * self.bravais_lattice[key_to_index[key]]
new_motif = np.append(new_motif, motif_copy_displaced, axis=0)
# Update Bravais lattice and motif
self.bravais_lattice[key_to_index[key]] *= ncells[key]
self.motif = new_motif
if update:
self.update()
return self
[docs]
def reduce(self, **ncells: dict) -> System:
"""
Routine to reduce the dimensionality of the System object along the specified
directions, by repeating unit cells along those directions until a given size
(number of unit cells) is reached. Thus we make the original system finite along those
directions.
:param ncells: Number of cells of direction(s) along which we want to reduce the
dimensionality of the system, n1, n2 and/or n3.
:return: Modified System object.
"""
key_to_index = {"n1": 0, "n2": 1, "n3": 2}
self.supercell(update=False, **ncells)
indices = [index for index in list(range(self.ndim))
if index not in [key_to_index[key] for key in ncells.keys()]]
if not indices:
self.bravais_lattice = None
self.boundary = "OBC"
else:
self.bravais_lattice = self.bravais_lattice[indices]
return self
[docs]
def ribbon(self, width: int, orientation: str = "horizontal", periodic: bool = False) -> System:
"""
Routine to generate a ribbon for a 2D crystal. It is designed to create rectangular ribbons,
based on a rectangular lattice. Therefore there must exist a combination of basis vectors such that
we can obtain a rectangular supercell. Otherwise the method returns an error.
:param width: Width of the ribbon (number of cells).
:param orientation: 'horizontal' or 'vertical'. Defaults to 'horizontal'.
:param periodic: Boolean to keep the ribbon periodic along the direction it is built. Defauts to False.
Turning this one would correspond to a nanotube instead of nanoribbon (finite).
:return: Modified System object.
"""
if orientation not in ["horizontal", "vertical"]:
raise ValueError("Wrong orientation provided, must be either vertical or horizontal, exiting...")
if self.ndim != 2:
print(f"Ribbons can not be generated for {self.ndim}D structures")
sys.exit(1)
else:
mesh_points = []
for _ in range(self.ndim):
mesh_points.append(list(range(-1, 2)))
combinations = np.array(np.meshgrid(*mesh_points)).T.reshape(-1, self.ndim)
rectangular_basis = np.zeros([2, 3])
all_possible_atoms = []
motif = []
# Calculate rectangular lattice basis vectors
for n, m in combinations:
vector = n * self.bravais_lattice[0] + m * self.bravais_lattice[1]
for atom in self.motif:
new_atom = np.copy(atom)
new_atom[:3] += vector
all_possible_atoms.append(new_atom)
if n == m == 0:
continue
vector_angle = np.arctan2(vector[1], vector[0])
if abs(vector_angle - 0.0) < 1E-10:
rectangular_basis[0, :] = vector
elif abs(vector_angle - np.pi/2) < 1E-10:
rectangular_basis[1, :] = vector
if not np.linalg.norm(rectangular_basis, axis=1).any():
print("Error: Could not generate a rectangular lattice, use reduce method directly. Exiting...")
sys.exit(1)
# Calculate new motif
for i, atom in enumerate(all_possible_atoms):
if (0 <= atom[0] < np.linalg.norm(rectangular_basis[0, :])) and \
(0 <= atom[1] < np.linalg.norm(rectangular_basis[1, :])):
motif.append(atom)
# Update system attributes
self.bravais_lattice = rectangular_basis
self.motif = motif
# Reduce system dimensionality
if not periodic:
if orientation == "horizontal":
self.reduce(n1=width)
else:
self.reduce(n2=width)
else:
if orientation == "horizontal":
self.supercell(n1=width)
else:
self.supercell(n2=width)
return self
[docs]
def extend_copy(self, vector: List[list, np.ndarray], n: int = 1) -> System:
"""
Method to extend the system by attaching a copy of the motif displaced by some input vector.
:param vector: Vector by which we displace the copy of the system.
:param n: Number of copies to attach. The n-th copy will be displaced by n*vector. Defaults to 1.
:return: Extended system.
"""
displaced_motif = np.copy(self.motif)
for _ in range(n):
displaced_motif[:, :3] += np.array(vector)
self.motif = np.concatenate((self.motif, displaced_motif))
return self
[docs]
def rotate(self, angle: float = 90) -> System:
"""
Method to rotate the whole system in the xy plane by some angle.
:param angle: Value of rotation angle in degrees. Defaults to 90 degrees.
:return: Rotated system.
"""
rotation_matrix = np.array([[np.cos(np.pi*angle/180), -np.sin(np.pi*angle/180), 0],
[np.sin(np.pi*angle/180), np.cos(np.pi*angle/180), 0],
[ 0, 0, 1]])
for n in range(self.natoms):
self.motif[n, :3] = rotation_matrix @ self.motif[n, :3]
rotated_bravais = np.zeros(self.bravais_lattice.shape)
for n in range(self.ndim):
rotated_bravais[n] = rotation_matrix @ self.bravais_lattice[n]
self.bravais_lattice = rotated_bravais
return self
[docs]
def restrict_lattice2rectangle(self) -> None:
"""
TO BE IMPLEMENTED YET (is it really necessary?).
"""
atoms = []
mesh_points = []
for i in range(self.ndim):
mesh_points.append(list(range(-1, 2)))
combinations = np.array(np.meshgrid(*mesh_points)).T.reshape(-1, self.ndim)
for n, m in combinations:
vector = n*self.bravais_lattice[0] + m*self.bravais_lattice[1]
for position in self.motif:
atom_position = vector + position
# ####################################################################################
# ################################### Hamiltonian ####################################
# ####################################################################################
[docs]
def initialize_hamiltonian(self):
"""
Generic implementation of initialization of the Hamiltonian. To be overwritten
by specific implementations of System.
"""
print("initialize_hamiltonian has to be implemented by child class")
[docs]
def hamiltonian_k(self, k: Union[list, np.ndarray]) -> np.ndarray:
"""
Generic implementation of hamiltonian_k H(k). To be overwritten
by specific implementations of System.
"""
print("Has to be implemented by child class")
[docs]
def solve(self, kpoints: list = None, neigval: int = None):
"""
Diagonalize the Hamiltonian to obtain the band structure and the eigenstates.
:param kpoints: List of kpoints where we want to diagonalize the Bloch Hamiltonian.
Defaults to None, which corresponds to [[0., 0., 0.]] for both PBC and OBC.
:param neigval: Number of eigenvalues to compute if using sparse matrices.
Defaults to None, which in turn is set to the filling.
:return: Spectrum object.
"""
# Import result here to avoid circular import
from . import result
if kpoints is None:
kpoints = np.array([[0., 0., 0.]])
if neigval is None and self.matrix_type == "sparse":
neigval = int(self.filling)
nk = len(kpoints)
eigen_energy = np.zeros([self.basisdim, nk])
eigen_states = []
if self.matrix_type == "dense":
solver = lambda h: np.linalg.eigh(h)
else:
solver = lambda h: scipy.sparse.linalg.eigsh(h, k=neigval)
for n, k in enumerate(kpoints):
hamiltonian = self.hamiltonian_k(k)
results = solver(hamiltonian)
eigen_energy[:, n] = results[0]
eigen_states.append(results[1])
return result.Spectrum(eigen_energy, np.array(eigen_states), kpoints, self)
[docs]
class FrozenClass:
"""
Class to enforce immutable attributes. Used for predefined models.
"""
_is_frozen = False
def __setattr__(self, key, value):
if self._is_frozen:
raise Exception("Predefined model can not be modified")
super().__setattr__(key, value)
def _freeze(self):
self._is_frozen = True
[docs]
def generate_all_combinations(ndim: int, n: int = 1) -> np.ndarray:
"""
Auxiliary routine to generate an array of combinations of possible neighbouring
unit cells.
:param ndim: Number of axis to make the combinations.
:param int: Values on each axis go from [-n, n]. Defaults to 1.
:return: Array with vectors of length ndim and combinations.
"""
mesh_points = []
for i in range(ndim):
mesh_points.append(list(range(-n, n+1)))
mesh_points = np.array(np.meshgrid(*mesh_points)).T.reshape(-1, ndim)
return mesh_points
[docs]
def generate_half_combinations(ndim: int) -> np.ndarray:
"""
Auxiliary routine to generate an array of combinations of possible neighbouring
unit cells. NB: It only generates half of them, since we are going to use hermiticity to
generate the Hamiltonian.
:param ndim: Number of axis to make the combinations.
:return: Half the combinations.
"""
all_combinations = generate_all_combinations(ndim)
# Eliminate vectors that are the inverse of others
points = []
for point in all_combinations:
if list(-point) not in points:
points.append(list(point))
return np.array(points)
[docs]
def generate_near_cells(bravais_lattice: Union[list, np.ndarray], n: int = 1, half: bool = False) -> np.ndarray:
"""
Auxiliary routine to generate the Bravais vectors corresponding to unit cells
neighbouring the origin one. NB: It only generates half of them, since we are going to use hermiticity to
generate the Hamiltonian.
:param bravais_lattice: Basis vectors of the Bravais lattice.
:param n: Values on each axis go from [-n, n]. Defaults to 1.
:param half: Generate only half the combinations. Defaults to False.
:return: Array with combinations of Bravais basis vectors.
"""
if bravais_lattice is None:
return np.array([[0., 0., 0.]])
ndim = len(bravais_lattice)
if not half:
mesh_points = generate_all_combinations(ndim, n)
else:
mesh_points = generate_half_combinations(ndim, n)
near_cells = np.zeros([len(mesh_points), 3])
for n, coefficients in enumerate(mesh_points):
cell_vector = np.array([0.0, 0.0, 0.0])
for i, coefficient in enumerate(coefficients):
cell_vector += (coefficient * bravais_lattice[i])
near_cells[n, :] = cell_vector
return near_cells