diff --git a/src/idea0/pyssmf/__init__.py b/src/idea0/pyssmf/__init__.py new file mode 100644 index 0000000..063849e --- /dev/null +++ b/src/idea0/pyssmf/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright: Dr. José M. Pizarro. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from .input import Input, ValidLatticeModels +from .runner import Runner diff --git a/src/pyssmf/hopping_pruning.py b/src/idea0/pyssmf/hopping_pruning.py similarity index 100% rename from src/pyssmf/hopping_pruning.py rename to src/idea0/pyssmf/hopping_pruning.py diff --git a/src/idea0/pyssmf/input.py b/src/idea0/pyssmf/input.py new file mode 100644 index 0000000..49c8f10 --- /dev/null +++ b/src/idea0/pyssmf/input.py @@ -0,0 +1,190 @@ +# +# Copyright: Dr. José M. Pizarro. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from abc import ABC +import logging +import json +import os +import numpy as np + + +class ValidLatticeModels(ABC): + """Abstract class that defines the valid lattice models covered in this code by specific strings.""" + + def __init__(self, logger: logging.Logger = logging.getLogger(__name__)): + self.logger = logger + self._valid_lattice_models = [ + 'linear', + 'square', + 'honeycomb', + 'triangular', + ] # TODO extend this + + +class Input(ValidLatticeModels): + def __init__(self, **kwargs): + """ + Reads the input arguments and stores then in self.data in a JSON file generated + in the working_directory. + - 'logger': the logger where the errors, warnings, etc. will be printed. + - 'code': 'pySSMF'. + - 'working_directory': working directory where the files are located. + If 'read_from_input_file' is true: + - 'input_file': path to the input JSON file in the working directory. + - 'input_data': read input data from input file. + Else: + If 'tb_model_file' is specified: + - 'tb_model': path to the tight-binding model file in the working directory. + - 'prune_threshold': if 'pruning' is True, it is set to the read or the + default 0.01 value. + If 'lattice_model' is specified: + - 'tb_model': the specific label for the model to be applied (defined + in self._valid_lattice_models). + - 'hoppings': list of values for the hoppings in order from nearest to + farthest hopping. + - 'n_hoppings': integer specifying how many hoppings are included. Only + supported up to 3. + - 'n_orbitals': integer specifying how many orbitals are included. + + Then, it stores the input parameters in a JSON in the working directory with the name + 'input_ssmf.json'. + """ + super().__init__() + # Initializing data + data = {'code': 'pySSMF'} + + # Check working_directory and stores it in data + if not kwargs.get('working_directory'): + self.logger.error( + 'Could not find specified the working_directory in the input.' + ) + return + data['working_directory'] = kwargs.get('working_directory') + + # Read from input file if provided in the argument + read_input_file = kwargs.get('read_from_input_file', False) + if read_input_file: + data['input_file'] = kwargs.get('input_file') + input_file = os.path.join( + kwargs.get('working_directory'), kwargs.get('input_file') + ) + data['input_data'] = self.read_from_file(input_file) + else: + # We check whether a model_file has been defined or a model_label + if kwargs.get('tb_model_file'): + data['tb_model'] = kwargs.get('tb_model_file') + # pruning only applies for tb_model_file cases + if kwargs.get('pruning', False): + data['prune_threshold'] = kwargs.get('prune_threshold', 0.01) + elif kwargs.get('lattice_model') in self._valid_lattice_models: + data['tb_model'] = kwargs.get('lattice_model') + hoppings = kwargs.get('hoppings', []) + # We check if 'hoppings' was empty + if len(hoppings) == 0: + n_hoppings = 1 + n_orbitals = 1 + hoppings = [[1.0]] + # Only up to 3 neighbors hoppings supported + n_hoppings = len(hoppings) + if n_hoppings > 3: + self.logger.error( + 'Maximum n_hoppings models supported is 3. Please, select ' + 'a smaller number.' + ) + return + n_orbitals = len(hoppings[0]) + # We check shape for all R point to be (n_orbitals, n_orbitals) + if not all( + np.shape(hop_point) == (n_orbitals, n_orbitals) + for hop_point in hoppings + ): + self.logger.error( + 'Dimensions of each hopping matrix do not coincide with' + '(n_orbitals, n_orbitals).', + data={'n_orbitals': n_orbitals}, + ) + # TODO improve this + onsite_energies = kwargs.get('onsite_energies', []) + if len(onsite_energies) == 0: + onsite_energies = [0.0] * n_orbitals + data['hoppings'] = hoppings + data['onsite_energies'] = onsite_energies + data['n_hoppings'] = n_hoppings + data['n_orbitals'] = n_orbitals + else: + self.logger.error( + 'Could not find the initial model. Please, check your inputs: ' + '1) define `model_file` pointing to your Wannier90 `*_hr.dat` ' + 'hoppings file, or 2) specify the `lattice_model` to study among the ' + 'accepted values.', + data={'lattice_model': self._valid_lattice_models}, + ) + + # KGrids + # For band structure calculations + data['n_k_path'] = kwargs.get('n_k_path', 90) + # For full_bz diagonalization + data['k_grid'] = kwargs.get('k_grid', [1, 1, 1]) + + # Plotting arguments + data['plot_hoppings'] = kwargs.get('plot_hoppings', False) + data['plot_bands'] = kwargs.get('plot_bands', False) + # DOS calculation and plotting + data['dos'] = kwargs.get('dos', False) + data['dos_gaussian_width'] = kwargs.get('dos_gaussian_width', 0.1) + data['dos_delta_energy'] = kwargs.get('dos_delta_energy', 0.01) + # Nominal number of electrons + data['n_electrons'] = kwargs.get('n_electrons', 1) + self.data = data + self.to_json() + + def to_json(self): + """ + Stores the input data in a JSON file in the working_directory. + """ + with open(f"{self.data.get('working_directory')}/input_ssmf.json", 'w') as file: + json.dump(self.data, file, indent=4) + + def read_from_file(self, input_file: str) -> dict: + """ + Reads the input data from a JSON file provided it is a pySSMF code input file. + + Args: + input_file (str): path to the input JSON file in the working directory. + + Returns: + (dict): dictionary with the input data read from the JSON input file. + """ + try: + with open(input_file, 'r') as file: + input_data = json.load(file) + except FileNotFoundError: + self.logger.error( + 'Input file not found.', + extra={'input_file': input_file}, + ) + except json.JSONDecodeError: + self.logger.error( + 'Failed to decode JSON in input file.', + extra={'input_file': input_file}, + ) + code_name = input_data.get('code', '') + if code_name != 'pySSMF': + self.logger.error( + 'Could not recognize the input JSON file as readable by the pySSMF code.', + extra={'input_file': input_file}, + ) + return input_data diff --git a/src/pyssmf/parsing.py b/src/idea0/pyssmf/parsing.py similarity index 100% rename from src/pyssmf/parsing.py rename to src/idea0/pyssmf/parsing.py diff --git a/src/idea0/pyssmf/runner.py b/src/idea0/pyssmf/runner.py new file mode 100644 index 0000000..a147fc0 --- /dev/null +++ b/src/idea0/pyssmf/runner.py @@ -0,0 +1,247 @@ +# +# Copyright: Dr. José M. Pizarro. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import numpy as np +import os +from scipy.stats import norm + +from pyssmf.input import ValidLatticeModels +from pyssmf.schema import Model +from pyssmf.parsing import MinimalWannier90Parser, ToyModels +from pyssmf.hopping_pruning import Pruner +from pyssmf.tb_hamiltonian import TBHamiltonian +from pyssmf.visualization import plot_hopping_matrices, plot_band_structure, plot_dos + + +class Runner(ValidLatticeModels): + """ + Class that runs the calculation. It reads the input data from the input file and + runs the SSMF calculation. + """ + + def __init__(self, **kwargs): + super().__init__() + self.data = kwargs.get('data', {}) + # Initializing the model class + self.model = Model() + + def parse_tb_model(self): + """ + Parses the tight-binding model from the input file. It can be obtained from a Wannier90 + tight-binding calculation or a toy lattice model. + """ + lattice_model = self.data.get('tb_model', '') + if '_hr.dat' in lattice_model: + model_file = os.path.join(self.data.get('working_directory'), lattice_model) + MinimalWannier90Parser().parse(model_file, self.model, self.logger) + elif lattice_model in self._valid_lattice_models: + onsite_energies = self.data.get('onsite_energies') + hoppings = self.data.get('hoppings') + ToyModels().parse( + lattice_model, onsite_energies, hoppings, self.model, self.logger + ) + else: + self.logger.error( + 'Could not recognize the input tight-binding model. Please ' + 'check your inputs.' + ) + return + self.logger.info('Tight-binding model parsed successfully!') + + def prune_hoppings(self): + """ + Prunes the hopping matrices by setting to zero all values below a certain `prune_threshold`. + """ + prune_threshold = self.data.get('prune_threshold') + if prune_threshold: + pruner = Pruner(self.model) + pruner.prune_by_threshold(prune_threshold, self.logger) + if self.data.get('plot_hoppings'): + plot_hopping_matrices(pruner.hopping_matrix_norms / pruner.max_value) + self.logger.info('Hopping pruning finished!') + + def calculate_fermi_level(self, eigenvalues, n_k_points): + """ + Calculates the Fermi level by ordering the eigenvalues and selecting the one that + corresponds to the filling of the system. + """ + n_orbitals = self.model.n_orbitals + if eigenvalues.shape != (n_k_points, n_orbitals): + self.logger.error( + 'Eigenvalues shape does not match (n_k_points, n_orbitals).' + ) + return + n_electrons = self.data.get('n_electrons', 1) + if n_electrons > n_orbitals: + self.logger.error( + 'Found n_electrons > n_orbitals, but n_electrons can only be <= n_orbitals. ' + 'The rule is that n_electrons / n_orbitals should represent the filling. ' + 'Half-filling is n_electrons / n_orbitals = 0.5.' + ) + return + filling = float(n_electrons / n_orbitals) # filling = 0.5 for half-filling + i_fermi = int( + filling * n_k_points * n_orbitals + ) # eigenvalue position of the Fermi level + # We evaluate the histogram for propper Fermi level extraction + _, edges = np.histogram( + eigenvalues, + bins=n_k_points * n_orbitals, + density=True, + ) + energies = (edges[:-1] + edges[1:]) / 2 + return energies[i_fermi] + + def gaussian_convolution( + self, + energies, + orbital_dos_histogram, + width: float = 0.1, + delta_energy: float = 0.01, + ): + """ + Convolutes / smoothes the histogram data with a Gaussian distribution function as defined + in scipy.stats.norm. The mesh of energies is also expanded (with delta_energy in eV) + to resolve better the Gaussians. + + Args: + energies (np.array): array containing the energies. Dimensions are (n_energies). + orbital_dos_histogram (np.array): array containing the orbital DOS histogram. Dimensions + are (n_energies, n_orbitals). + width (float): standard deviation or width of the Gaussian distribution. Defaults to + 0.1 eV. + delta_energy (float): the spacing of the new energies mesh. Defaults to 0.01 eV. + + Returns: + new_energies, convoluted_data: returns the new X and Y data for the convoluted data. + """ + energy_min = np.min(energies) - 2 * width + energy_max = np.max(energies) + 2 * width + new_energies = np.arange(energy_min, energy_max, delta_energy) + + n_orbitals = orbital_dos_histogram.shape[1] + + gaussian = norm(loc=0, scale=width) + + convoluted_data = np.zeros((len(new_energies), n_orbitals)) + for i, new_energy in enumerate(new_energies): + for j in range(n_orbitals): + convoluted_data[i, j] = np.sum( + orbital_dos_histogram[:, j] * gaussian.pdf(new_energy - energies) + ) + return new_energies, np.transpose(convoluted_data) + + def calculate_dos( + self, + eigenvalues, + eigenvectors, + bins: int = 100, + width: float = 0.1, + delta_energy: float = 0.01, + ): + """ + Calculates the orbital and total DOS from the eigenvalues and eigenvectors of the + tight-binding model. + + Args: + eigenvalues (np.ndarray): the eigenvalues of the tight-binding model. Dimensions are + (n_kpoints, n_orbitals). + eigenvectors (np.ndarray): the eigenvectors of the tight-binding model. Dimensions are + (n_kpoints, n_orbitals, n_orbitals). + bins (int, optional): the number of bins for the histogram. Defaults to 100. + width (float, optional): the width of the Gaussian distribution function. Defaults to + 0.1 eV. + delta_energy (float, optional): the spacing of the new energies mesh. Defaults to + 0.01 eV. + + Returns: + energies, orbital_dos, total_dos: the energies mesh, the orbital DOS, and the total DOS. + """ + # We create the orbital DOS histogram and append all orbital contributions together + orbital_dos_histogram = [] + for orbital in range(self.model.n_orbitals): + orbital_dos_contribution_histogram, bin_edges = np.histogram( + eigenvalues, + bins=bins, + density=True, + weights=np.abs(eigenvectors[:, orbital]) ** 2, + ) + energies = (bin_edges[:-1] + bin_edges[1:]) / 2 + orbital_dos_histogram.append(orbital_dos_contribution_histogram) + if not orbital_dos_histogram: + self.logger.warning( + 'Problem obtaining the orbital DOS histogram. Cannot resolve DOS.' + ) + # We convolute the histogram to obtain a smoother orbital DOS + energies, orbital_dos = self.gaussian_convolution( + energies, np.array(orbital_dos_histogram).T, width, delta_energy + ) + # We sum all orbital contributions to obtain the total DOS + total_dos = np.sum(orbital_dos, axis=0) + return energies, orbital_dos, total_dos + + def bz_diagonalization(self): + """ + Diagonalizes the tight-binding model in the full Brillouin zone and returns its + eigenvalues and eigenvectors. + """ + k_grid = self.data.get('k_grid', [1, 1, 1]) + tb_hamiltonian = TBHamiltonian(self.model, k_grid_type='full_bz', k_grid=k_grid) + kpoints = tb_hamiltonian.kpoints + n_k_points = tb_hamiltonian.n_k_points + eigenvalues, eigenvectors = tb_hamiltonian.diagonalize(kpoints) + + # Calculate Fermi level + fermi_level = self.calculate_fermi_level(eigenvalues, n_k_points) + + # Calculating and plotting DOS + if self.data.get('dos') and fermi_level: + width = self.data.get('dos_gaussian_width') + delta_energy = self.data.get('dos_delta_energy') + energies, orbital_dos, total_dos = self.calculate_dos( + eigenvalues, eigenvectors, n_k_points, width, delta_energy + ) + plot_dos(energies, fermi_level, orbital_dos, total_dos) + self.logger.info('DOS calculation finished!') + + self.logger.info('BZ diagonalization calculation finished!') + return eigenvalues, eigenvectors, fermi_level + + def calculate_band_structure(self, fermi_level: float = 0.0): + """ + Calculates the band structure of the tight-binding model in a given `n_k_path`. + """ + n_k_path = self.data.get('n_k_path', 90) + tb_hamiltonian = TBHamiltonian( + self.model, k_grid_type='bands', n_k_path=n_k_path + ) + special_points = tb_hamiltonian.k_path.special_points + kpoints = tb_hamiltonian.kpoints + eigenvalues, _ = tb_hamiltonian.diagonalize(kpoints) + plot_band_structure(eigenvalues, fermi_level, tb_hamiltonian, special_points) + self.logger.info('Band structure calculation finished!') + + def run(self): + self.parse_tb_model() + + self.prune_hoppings() + + eigenvalues, eigenvectors, fermi_level = self.bz_diagonalization() + + if self.data.get('plot_bands') and fermi_level: + self.calculate_band_structure(fermi_level) + + print('Finished!') diff --git a/src/pyssmf/schema.py b/src/idea0/pyssmf/schema.py similarity index 100% rename from src/pyssmf/schema.py rename to src/idea0/pyssmf/schema.py diff --git a/src/pyssmf/tb_hamiltonian.py b/src/idea0/pyssmf/tb_hamiltonian.py similarity index 99% rename from src/pyssmf/tb_hamiltonian.py rename to src/idea0/pyssmf/tb_hamiltonian.py index fd703e8..386c605 100644 --- a/src/pyssmf/tb_hamiltonian.py +++ b/src/idea0/pyssmf/tb_hamiltonian.py @@ -19,7 +19,6 @@ import ase from ase.spacegroup import get_spacegroup, spacegroup from ase.dft.kpoints import monkhorst_pack, BandPath - from nomad.atomutils import Formula from nomad.units import ureg diff --git a/src/pyssmf/utils/__init__.py b/src/idea0/pyssmf/utils/__init__.py similarity index 100% rename from src/pyssmf/utils/__init__.py rename to src/idea0/pyssmf/utils/__init__.py diff --git a/src/pyssmf/utils/utils.py b/src/idea0/pyssmf/utils/utils.py similarity index 100% rename from src/pyssmf/utils/utils.py rename to src/idea0/pyssmf/utils/utils.py diff --git a/src/pyssmf/visualization.py b/src/idea0/pyssmf/visualization.py similarity index 90% rename from src/pyssmf/visualization.py rename to src/idea0/pyssmf/visualization.py index e7b4782..978ba93 100644 --- a/src/pyssmf/visualization.py +++ b/src/idea0/pyssmf/visualization.py @@ -82,15 +82,16 @@ def update(val): plt.show() -def plot_band_structure(eigenvalues, tb_hamiltonian, special_points=None): +def plot_band_structure(eigenvalues, fermi_level, tb_hamiltonian, special_points=None): """ Plots the band structure of a Hamiltonian. Args: eigenvalues (np.ndarray): Eigenvalues of the Hamiltonian matrix of shape (Nk, Norb). + fermi_level (float): Fermi level. special_points (dict, optional): Dictionary of special points and their labels. """ - + # eigenvalues = eigenvalues - fermi_level num_bands = eigenvalues.shape[1] # Create a figure @@ -120,6 +121,7 @@ def plot_band_structure(eigenvalues, tb_hamiltonian, special_points=None): i += 1 plt.xticks(x_ticks, x_labels) + plt.axhline(y=fermi_level, color='k', linestyle='--') plt.xlim(0, len(eigenvalues) - 1) plt.xlabel('k-points') plt.ylabel('Energy (eV)') @@ -130,24 +132,28 @@ def plot_band_structure(eigenvalues, tb_hamiltonian, special_points=None): plt.show() -def plot_dos(energies, orbital_dos, total_dos): +def plot_dos(energies, fermi_level, orbital_dos, total_dos): """ Plots the density of states (DOS) of a tight-binding Hamiltonian. Args: energies: the energies at which the DOS is evaluated. + fermi_level: the Fermi level. orbital_dos: the orbital-resolved DOS. total_dos: the total DOS. """ # Create a figure plt.figure(figsize=(8, 6)) + # energies = energies - fermi_level plt.plot(energies, total_dos, label='Total DOS', color='k', linewidth=3.5) # Plot orbital-resolved DOS for i, orb_dos in enumerate(orbital_dos): plt.plot(energies, orb_dos, label=f'Orbital {i + 1}') + plt.axvline(x=fermi_level, color='k', linestyle='--') plt.xlabel('Energy (eV)') plt.ylabel('Density of States (DOS)') + plt.grid(True) plt.legend() plt.show() diff --git a/src/pyssmf/__init__.py b/src/pyssmf/__init__.py index 063849e..285ea6c 100644 --- a/src/pyssmf/__init__.py +++ b/src/pyssmf/__init__.py @@ -14,5 +14,6 @@ # limitations under the License. # -from .input import Input, ValidLatticeModels -from .runner import Runner +import structlog + +LOGGER = structlog.get_logger(__name__) diff --git a/src/pyssmf/input.py b/src/pyssmf/input.py index 9725c69..fefbfbf 100644 --- a/src/pyssmf/input.py +++ b/src/pyssmf/input.py @@ -20,138 +20,126 @@ import os import numpy as np +from . import LOGGER -class ValidLatticeModels(ABC): - """Abstract class that defines the valid lattice models covered in this code by specific strings.""" - def __init__(self, logger: logging.Logger = logging.getLogger(__name__)): - self.logger = logger - self._valid_lattice_models = [ - 'linear', - 'square', - 'honeycomb', - 'triangular', - ] # TODO extend this - - -class Input(ValidLatticeModels): +class Input: def __init__(self, **kwargs): """ - Reads the input arguments and stores then in self.data in a JSON file generated - in the working_directory. - - 'logger': the logger where the errors, warnings, etc. will be printed. - - 'code': 'pySSMF'. - - 'working_directory': working directory where the files are located. - If 'read_from_input_file' is true: - - 'input_file': path to the input JSON file in the working directory. - - 'input_data': read input data from input file. - Else: - If 'tb_model_file' is specified: - - 'tb_model': path to the tight-binding model file in the working directory. - - 'prune_threshold': if 'pruning' is True, it is set to the read or the - default 0.01 value. - If 'lattice_model' is specified: - - 'tb_model': the specific label for the model to be applied (defined - in self._valid_lattice_models). - - 'hoppings': list of values for the hoppings in order from nearest to - farthest hopping. - - 'n_hoppings': integer specifying how many hoppings are included. Only - supported up to 3. - - 'n_orbitals': integer specifying how many orbitals are included. + Reads the input arguments and stores then in a dictionary called `data` and in a + JSON file, `input_ssmf.json` generated in the working_directory. - Then, it stores the input parameters in a JSON in the working directory with the name - 'input_ssmf.json'. + Attributes: + data (dict): A dictionary that stores input arguments. The keys and their + corresponding values are as follows: + - 'code' (str): Always set to 'pySSMF'. + - 'working_directory' (str): The directory where the files are located. + - 'input_file' (str): Path to the input JSON file in the working directory. + Only present if 'read_from_input_file' is True. + - 'lattice_model' (str): the specific lattice model to be calculated. See + `valid_lattice_models` for accepted values. + - 'hoppings' (list): List of values for the hoppings in order from nearest to + farthest hopping. Only present if 'lattice_model' is specified. + - 'n_hoppings' (int): Integer specifying how many hoppings are included. + Only supported up to 3. Only present if 'lattice_model' is specified. + - 'n_orbitals' (int): Integer specifying how many orbitals are included. + Only present if 'lattice_model' is specified. """ super().__init__() - # Initializing data + # List of covered lattice toy models + # TODO extend this list + _valid_lattices = [ + 'linear', + ] + + # Reading input from an `input.json` file + read_input_file = kwargs.get('read_from_input_file', False) + if read_input_file: + input_file = os.path.join( + kwargs.get('working_directory'), kwargs.get('input_file') + ) + data = self.read_from_file(input_file) + data['input_file'] = kwargs.get('input_file') + self.data = data + self.to_json() + return + + # If `input_file` is not specified, we populate `data` with the passed arguments instead + # Initializing `data` data = {'code': 'pySSMF'} # Check working_directory and stores it in data if not kwargs.get('working_directory'): - self.logger.error( + raise KeyError( 'Could not find specified the working_directory in the input.' ) - return data['working_directory'] = kwargs.get('working_directory') - # Read from input file if provided in the argument - read_input_file = kwargs.get('read_from_input_file', False) - if read_input_file: - data['input_file'] = kwargs.get('input_file') - input_file = os.path.join( - kwargs.get('working_directory'), kwargs.get('input_file') + # Lattice models details + lattice_model_id = kwargs.get('lattice_model', '') + if lattice_model_id not in _valid_lattices: + raise ValueError(f'{lattice_model_id} is not a valid lattice model.') + data['lattice_model'] = lattice_model_id + # We check if 'hoppings' was empty + hoppings = kwargs.get('hoppings', []) + n_hoppings = len(hoppings) + if n_hoppings == 0: + n_hoppings = 1 + n_orbitals = 1 + hoppings = [[1.0]] + LOGGER.warning( + 'Argument `hoppings` was empty, so we consider a nearest neighbor, single-orbital model' + ) + # Only up to 3 neighbors hoppings supported + n_hoppings = len(hoppings) + if n_hoppings > 3: + raise ValueError( + 'Maximum n_hoppings models supported is 3. Please, select ' + 'a smaller number.' + ) + # We check shape for all Wigner-Seitz points hoppings to be (n_orbitals, n_orbitals) + n_orbitals = len(hoppings[0]) + if not all( + np.shape(hop_point) == (n_orbitals, n_orbitals) for hop_point in hoppings + ): + raise ValueError( + 'Dimensions of each hopping matrix do not coincide with' + '(n_orbitals, n_orbitals).', + data={'n_orbitals': n_orbitals}, ) - data['input_data'] = self.read_from_file(input_file) - else: - # We check whether a model_file has been defined or a model_label - if kwargs.get('tb_model_file'): - data['tb_model'] = kwargs.get('tb_model_file') - # pruning only applies for tb_model_file cases - if kwargs.get('pruning', False): - data['prune_threshold'] = kwargs.get('prune_threshold', 0.01) - elif kwargs.get('lattice_model') in self._valid_lattice_models: - data['tb_model'] = kwargs.get('lattice_model') - hoppings = kwargs.get('hoppings', []) - # We check if 'hoppings' was empty - if len(hoppings) == 0: - n_hoppings = 1 - n_orbitals = 1 - hoppings = [[1.0]] - # Only up to 3 neighbors hoppings supported - n_hoppings = len(hoppings) - if n_hoppings > 3: - self.logger.error( - 'Maximum n_hoppings models supported is 3. Please, select ' - 'a smaller number.' - ) - return - n_orbitals = len(hoppings[0]) - # We check shape for all R point to be (n_orbitals, n_orbitals) - if not all( - np.shape(hop_point) == (n_orbitals, n_orbitals) - for hop_point in hoppings - ): - self.logger.error( - 'Dimensions of each hopping matrix do not coincide with' - '(n_orbitals, n_orbitals).', - data={'n_orbitals': n_orbitals}, - ) - # TODO improve this - onsite_energies = kwargs.get('onsite_energies', []) - if len(onsite_energies) == 0: - onsite_energies = [0.0] * n_orbitals - data['hoppings'] = hoppings - data['onsite_energies'] = onsite_energies - data['n_hoppings'] = n_hoppings - data['n_orbitals'] = n_orbitals - else: - self.logger.error( - 'Could not find the initial model. Please, check your inputs: ' - '1) define `model_file` pointing to your Wannier90 `*_hr.dat` ' - 'hoppings file, or 2) specify the `lattice_model` to study among the ' - 'accepted values.', - data={'lattice_model': self._valid_lattice_models}, - ) + # Extracting `onsite_energies`, `hoppings` + onsite_energies = kwargs.get('onsite_energies', []) + if len(onsite_energies) == 0: + onsite_energies = [0.0] * n_orbitals + LOGGER.warning( + 'Attribute `onsite_energies` was empty, so we consider all zeros with the dimensions of `(n_orbitals)`.' + ) + data['hoppings'] = hoppings + data['onsite_energies'] = onsite_energies + data['n_hoppings'] = n_hoppings + data['n_orbitals'] = n_orbitals - # KGrids - # For band structure calculations - data['n_k_path'] = kwargs.get('n_k_path', 90) - # For full_bz diagonalization - data['k_grid'] = kwargs.get('k_grid', [1, 1, 1]) + # KGrids + # For band structure calculations + data['n_k_path'] = kwargs.get('n_k_path', 90) + # For full_bz diagonalization + data['k_grid'] = kwargs.get('k_grid', [1, 1, 1]) - # Plotting arguments - data['plot_hoppings'] = kwargs.get('plot_hoppings', False) - data['plot_bands'] = kwargs.get('plot_bands', False) - # DOS calculation and plotting - data['dos'] = kwargs.get('dos', False) - data['dos_gaussian_width'] = kwargs.get('dos_gaussian_width', 0.1) - data['dos_delta_energy'] = kwargs.get('dos_delta_energy', 0.01) + # Plotting arguments + data['plot_hoppings'] = kwargs.get('plot_hoppings', False) + data['plot_bands'] = kwargs.get('plot_bands', False) + # DOS calculation and plotting + data['dos'] = kwargs.get('dos', False) + data['dos_gaussian_width'] = kwargs.get('dos_gaussian_width', 0.1) + data['dos_delta_energy'] = kwargs.get('dos_delta_energy', 0.01) + # Nominal number of electrons + data['n_electrons'] = kwargs.get('n_electrons', 1) self.data = data self.to_json() - def to_json(self): + def to_json(self) -> None: """ - Stores the input data in a JSON file in the working_directory. + Stores the input data in a JSON file in the working directory. """ with open(f"{self.data.get('working_directory')}/input_ssmf.json", 'w') as file: json.dump(self.data, file, indent=4) @@ -169,19 +157,14 @@ def read_from_file(self, input_file: str) -> dict: try: with open(input_file, 'r') as file: input_data = json.load(file) - except FileNotFoundError: - self.logger.error( - 'Input file not found.', - extra={'input_file': input_file}, - ) - except json.JSONDecodeError: - self.logger.error( - 'Failed to decode JSON in input file.', + except (FileNotFoundError, json.JSONDecodeError): + raise FileNotFoundError( + 'Input file not found or failed to decode JSON input file.', extra={'input_file': input_file}, ) code_name = input_data.get('code', '') if code_name != 'pySSMF': - self.logger.error( + raise ValueError( 'Could not recognize the input JSON file as readable by the pySSMF code.', extra={'input_file': input_file}, ) diff --git a/src/pyssmf/runner.py b/src/pyssmf/runner.py index bb75baf..8b7afd8 100644 --- a/src/pyssmf/runner.py +++ b/src/pyssmf/runner.py @@ -14,197 +14,12 @@ # limitations under the License. # -import numpy as np -import os -from scipy.stats import norm -from pyssmf.input import ValidLatticeModels -from pyssmf.schema import Model -from pyssmf.parsing import MinimalWannier90Parser, ToyModels -from pyssmf.hopping_pruning import Pruner -from pyssmf.tb_hamiltonian import TBHamiltonian -from pyssmf.visualization import plot_hopping_matrices, plot_band_structure, plot_dos - - -class Runner(ValidLatticeModels): +class Runner: """ - Class that runs the calculation. It reads the input data from the input file and - runs the SSMF calculation. + Class responsible of running a calculation. It reads the input data passed in the JSON + input file and runs the different possible calculations related with pySSMF. """ def __init__(self, **kwargs): super().__init__() - self.data = kwargs.get('data', {}) - # Initializing the model class - self.model = Model() - - def parse_tb_model(self): - """ - Parses the tight-binding model from the input file. It can be obtained from a Wannier90 - tight-binding calculation or a toy lattice model. - """ - lattice_model = self.data.get('tb_model', '') - if '_hr.dat' in lattice_model: - model_file = os.path.join(self.data.get('working_directory'), lattice_model) - MinimalWannier90Parser().parse(model_file, self.model, self.logger) - elif lattice_model in self._valid_lattice_models: - onsite_energies = self.data.get('onsite_energies') - hoppings = self.data.get('hoppings') - ToyModels().parse( - lattice_model, onsite_energies, hoppings, self.model, self.logger - ) - else: - self.logger.error( - 'Could not recognize the input tight-binding model. Please ' - 'check your inputs.' - ) - return - self.logger.info('Tight-binding model parsed successfully!') - - def prune_hoppings(self): - """ - Prunes the hopping matrices by setting to zero all values below a certain `prune_threshold`. - """ - prune_threshold = self.data.get('prune_threshold') - if prune_threshold: - pruner = Pruner(self.model) - pruner.prune_by_threshold(prune_threshold, self.logger) - if self.data.get('plot_hoppings'): - plot_hopping_matrices(pruner.hopping_matrix_norms / pruner.max_value) - self.logger.info('Hopping pruning finished!') - - def calculate_band_structure(self): - """ - Calculates the band structure of the tight-binding model in a given `n_k_path`. - """ - n_k_path = self.data.get('n_k_path', 90) - tb_hamiltonian = TBHamiltonian( - self.model, k_grid_type='bands', n_k_path=n_k_path - ) - special_points = tb_hamiltonian.k_path.special_points - kpoints = tb_hamiltonian.kpoints - eigenvalues, _ = tb_hamiltonian.diagonalize(kpoints) - plot_band_structure(eigenvalues, tb_hamiltonian, special_points) - self.logger.info('Band structure calculation finished!') - - def gaussian_convolution( - self, - energies, - orbital_dos_histogram, - width: float = 0.1, - delta_energy: float = 0.01, - ): - """ - Convolutes / smoothes the histogram data with a Gaussian distribution function as defined - in scipy.stats.norm. The mesh of energies is also expanded (with delta_energy in eV) - to resolve better the Gaussians. - - Args: - energies (np.array): array containing the energies. Dimensions are (n_energies). - orbital_dos_histogram (np.array): array containing the orbital DOS histogram. Dimensions - are (n_energies, n_orbitals). - width (float): standard deviation or width of the Gaussian distribution. Defaults to - 0.1 eV. - delta_energy (float): the spacing of the new energies mesh. Defaults to 0.01 eV. - - Returns: - new_energies, convoluted_data: returns the new X and Y data for the convoluted data. - """ - energy_min = np.min(energies) - 2 * width - energy_max = np.max(energies) + 2 * width - new_energies = np.arange(energy_min, energy_max, delta_energy) - - n_orbitals = orbital_dos_histogram.shape[1] - - gaussian = norm(loc=0, scale=width) - - convoluted_data = np.zeros((len(new_energies), n_orbitals)) - for i, new_energy in enumerate(new_energies): - for j in range(n_orbitals): - convoluted_data[i, j] = np.sum( - orbital_dos_histogram[:, j] * gaussian.pdf(new_energy - energies) - ) - return new_energies, np.transpose(convoluted_data) - - def calculate_dos( - self, - eigenvalues, - eigenvectors, - bins: int = 100, - width: float = 0.1, - delta_energy: float = 0.01, - ): - """ - Calculates the orbital and total DOS from the eigenvalues and eigenvectors of the - tight-binding model. - - Args: - eigenvalues (np.ndarray): the eigenvalues of the tight-binding model. Dimensions are - (n_kpoints, n_orbitals). - eigenvectors (np.ndarray): the eigenvectors of the tight-binding model. Dimensions are - (n_kpoints, n_orbitals, n_orbitals). - bins (int, optional): the number of bins for the histogram. Defaults to 100. - width (float, optional): the width of the Gaussian distribution function. Defaults to - 0.1 eV. - delta_energy (float, optional): the spacing of the new energies mesh. Defaults to - 0.01 eV. - - Returns: - energies, orbital_dos, total_dos: the energies mesh, the orbital DOS, and the total DOS. - """ - # We create the orbital DOS histogram and append all orbital contributions together - orbital_dos_histogram = [] - for orbital in range(self.model.n_orbitals): - orbital_dos_contribution_histogram, bin_edges = np.histogram( - eigenvalues, - bins=bins, - density=True, - weights=np.abs(eigenvectors[:, orbital]) ** 2, - ) - energies = (bin_edges[:-1] + bin_edges[1:]) / 2 - orbital_dos_histogram.append(orbital_dos_contribution_histogram) - if not orbital_dos_histogram: - self.logger.warning( - 'Problem obtaining the orbital DOS histogram. Cannot resolve DOS.' - ) - # We convolute the histogram to obtain a smoother orbital DOS - energies, orbital_dos = self.gaussian_convolution( - energies, np.array(orbital_dos_histogram).T, width, delta_energy - ) - # We sum all orbital contributions to obtain the total DOS - total_dos = np.sum(orbital_dos, axis=0) - return energies, orbital_dos, total_dos - - def bz_diagonalization(self): - """ - Diagonalizes the tight-binding model in the full Brillouin zone and returns its - eigenvalues and eigenvectors. - """ - k_grid = self.data.get('k_grid', [1, 1, 1]) - tb_hamiltonian = TBHamiltonian(self.model, k_grid_type='full_bz', k_grid=k_grid) - kpoints = tb_hamiltonian.kpoints - eigenvalues, eigenvectors = tb_hamiltonian.diagonalize(kpoints) - - # Calculating and plotting DOS - if self.data.get('dos'): - bins = int(np.linalg.norm(k_grid)) - width = self.data.get('dos_gaussian_width') - delta_energy = self.data.get('dos_delta_energy') - energies, orbital_dos, total_dos = self.calculate_dos( - eigenvalues, eigenvectors, bins, width, delta_energy - ) - plot_dos(energies, orbital_dos, total_dos) - self.logger.info('DOS calculation finished!') - - self.logger.info('BZ diagonalization calculation finished!') - return eigenvalues, eigenvectors - - def run(self): - self.parse_tb_model() - - self.prune_hoppings() - - if self.data.get('plot_bands'): - self.calculate_band_structure() - - self.bz_diagonalization()