From 51a0d942ecf423197713d6187c4fe6e25ecc1caa Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 16 May 2024 18:56:16 +0200 Subject: [PATCH 01/13] Small adjustments to make graphene work --- malada/providers/supercell.py | 5 ++++- malada/utils/convergence_guesses.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/malada/providers/supercell.py b/malada/providers/supercell.py index 2c513fd..ddd5957 100644 --- a/malada/providers/supercell.py +++ b/malada/providers/supercell.py @@ -7,7 +7,10 @@ from ase.units import m, kg, Bohr from shutil import copyfile import numpy as np -from mp_api.client import MPRester +try: + from mp_api.client import MPRester +except: + pass class SuperCellProvider(Provider): diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 3655eb4..9ade087 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -4,7 +4,8 @@ cutoff_guesses_qe = {"Fe": [40, 50, 60, 70, 80, 90, 100], "Be": [40, 50, 60, 70], "Al": [20, 30, 40, 50, 60, 70], - "H": [26, 36, 46, 56, 66, 76]} + "H": [26, 36, 46, 56, 66, 76], + "C": [70, 80, 90, 100, 110, 120]} cutoff_guesses_vasp = {"Fe": [268, 368, 468, 568, 668, 768], "Al": [240, 340, 440, 540], @@ -14,4 +15,5 @@ kpoints_guesses = {"Fe": [2, 3, 4], "Be": [2, 3, 4, 5, 6], "Al": [2, 3, 4], - "H": [2, 3, 4, 5]} + "H": [2, 3, 4, 5], + "C": [1, 2, 3, 4]} From 2b6c33221b9d2353df1576f3205d22100a55d829 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 16 May 2024 20:31:58 +0200 Subject: [PATCH 02/13] Added automated submit all and cleanup option for slurm parameters --- malada/providers/dftconvergence.py | 19 ++++++++++++++++++- malada/runners/slurm_creator.py | 2 ++ malada/utils/slurmparams.py | 9 +++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/malada/providers/dftconvergence.py b/malada/providers/dftconvergence.py index 739d9ab..a6a62b8 100644 --- a/malada/providers/dftconvergence.py +++ b/malada/providers/dftconvergence.py @@ -54,7 +54,7 @@ def __init__(self, parameters, external_convergence_results=None, self.converged_cutoff = predefined_cutoff self.converged_kgrid = predefined_kgrid - def provide(self, provider_path, supercell_file): + def provide(self, provider_path, supercell_file, create_submit_script=True): """ Provide DFT parameters converged to within user specification. @@ -106,6 +106,14 @@ def provide(self, provider_path, supercell_file): fixed_kpoints=(1, 1, 1)) else: if self.parameters.run_system == "slurm_creator": + all_submit_file = open( + os.path.join(provider_path, "submit_all.sh"), mode='w') + all_submit_file.write("#!/bin/bash\n\n") + for cutoff_folder in cutoff_folders: + all_submit_file.write("cd "+cutoff_folder.split("/")[-2]+"\n") + all_submit_file.write("sbatch submit.slurm\n") + all_submit_file.write("cd ..\n") + all_submit_file.close() print("Run scripts created, please run via slurm.\n" "Quitting now.") quit() @@ -138,6 +146,15 @@ def provide(self, provider_path, supercell_file): fixed_cutoff=self.converged_cutoff) else: if self.parameters.run_system == "slurm_creator": + all_submit_file = open( + os.path.join(provider_path, "submit_all.sh"), mode='w') + all_submit_file.write("#!/bin/bash\n\n") + for kpoint_folder in kpoints_folders: + all_submit_file.write("cd "+kpoint_folder.split("/")[-2]+"\n") + all_submit_file.write("sbatch submit.slurm\n") + all_submit_file.write("cd ..\n") + all_submit_file.close() + print("Run scripts created, please run via slurm.\n" "Quitting now.") quit() diff --git a/malada/runners/slurm_creator.py b/malada/runners/slurm_creator.py index 2134abe..f055a9b 100644 --- a/malada/runners/slurm_creator.py +++ b/malada/runners/slurm_creator.py @@ -120,6 +120,8 @@ def run_folder(self, folder, calculation_type): submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ slurm_params.scf_executable+" \n") + submit_file.write(slurm_params.cleanup_string) + submit_file.write("\n") submit_file.close() diff --git a/malada/utils/slurmparams.py b/malada/utils/slurmparams.py index 8e8df85..d5b91f1 100644 --- a/malada/utils/slurmparams.py +++ b/malada/utils/slurmparams.py @@ -43,6 +43,7 @@ def __init__(self): self.execution_time = 0 self.partition_string = "" self.mpi_runner = "mpirun" + self.cleanup_string = "" # TODO: Maybe some consistency checks here? self.tasks_per_node = 0 @@ -79,6 +80,9 @@ def save(self, filename): node = SubElement(top, "nodes", {"type": "int"}) node.text = str(self.nodes) + node = SubElement(top, "cleanup_string", + {"type": "string"}) + node.text = self.cleanup_string rough_string = tostring(top, 'utf-8') reparsed = minidom.parseString(rough_string) with open(filename, "w") as f: @@ -114,6 +118,11 @@ def from_xml(cls, filename): new_object.partition_string = filecontents.find("partition_string").text new_object.tasks_per_node = int(filecontents.find("tasks_per_node").text) new_object.nodes = int(filecontents.find("nodes").text) + try: + new_object.cleanup_string = filecontents.find("cleanup_string").text + except: + pass + return new_object def get_mpirunner_process_params(self): From 10aca6260d6f4df764a4178c1640a9e4e4eb4d86 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Fri, 26 Jul 2024 16:19:46 +0200 Subject: [PATCH 03/13] Enabled 2D calculations and made some adjustments for graphene workflow --- malada/providers/dft.py | 27 ++++++++++++++++++++------- malada/providers/provider.py | 8 ++++++-- malada/utils/parameters.py | 1 + 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/malada/providers/dft.py b/malada/providers/dft.py index a250c09..854f3a8 100644 --- a/malada/providers/dft.py +++ b/malada/providers/dft.py @@ -34,7 +34,7 @@ def __init__(self, parameters, external_calculation_folders=None): def provide(self, provider_path, dft_convergence_file, ldos_convergence_file, possible_snapshots_file, do_postprocessing=True, numbering_starts_at=0, - parsing_starts_at=0): + parsing_starts_at=0, ignore_atom_number=False): """ Provide a set of DFT calculations on predefined snapshots. @@ -68,6 +68,10 @@ def provide(self, provider_path, dft_convergence_file, parsing_starts_at : int Overwrites numbering_starts_at. Number from which to start processing snapshots. + + ignore_atom_number : bool + If True, a convergence file for a different number of atoms + can be loaded. """ if parsing_starts_at > 0: numbering_starts_at = parsing_starts_at @@ -87,7 +91,8 @@ def provide(self, provider_path, dft_convergence_file, all_valid_snapshots[snapshot_number-numbering_starts_at+parsing_starts_at], snapshot_path, "snapshot"+str(snapshot_number), - do_postprocessing) + do_postprocessing, + ignore_atom_number) for i in range(0, self.parameters.number_of_snapshots): # Run the individul files. snapshot_number = i + numbering_starts_at @@ -103,11 +108,13 @@ def provide(self, provider_path, dft_convergence_file, def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, atoms, snapshot_path, snapshot_name, - do_postprocessing): + do_postprocessing, ignore_atom_number): # Get cluster info # TODO: Use DFT kgrid for scf and LDOS kgrid for NSCF calculations. - cutoff, kgrid = self._read_convergence(dft_convergence_file) - ldos_params, kgrid = self.__read_ldos_convergence(ldos_convergence_file) + cutoff, kgrid = self._read_convergence(dft_convergence_file, + ignore_atom_number) + ldos_params, kgrid = self.__read_ldos_convergence(ldos_convergence_file, + ignore_atom_number) # Create folder if not os.path.exists(snapshot_path): os.makedirs(snapshot_path) @@ -147,6 +154,8 @@ def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, qe_input_data["tstress"] = False if self.parameters.dft_calculate_force is False: qe_input_data["tprnfor"] = False + if self.parameters.dft_assume_two_dimensional: + qe_input_data["assume_isolated"] = "2D" id_string = self.parameters.element+"_"+snapshot_name ase.io.write(os.path.join(snapshot_path,id_string+".pw.scf.in"), @@ -187,6 +196,7 @@ def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, ldos_file.write(" delta_e=" + str(deltae) + ",\n") ldos_file.write( " degauss_ldos=" + str(deltae * smearing_factor) + ",\n") + ldos_file.write(" use_gauss_ldos=.true.\n") ldos_file.write("/\n") ldos_file.write("&plot\n") ldos_file.write(" iflag=3,\n") @@ -214,16 +224,19 @@ def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, ldos_file.close() dens_file.close() - def __read_ldos_convergence(self, filename): + def __read_ldos_convergence(self, filename, ignore_atom_number): # Parse the XML file and first check for consistency. filecontents = ET.parse(filename).getroot() dftparams = filecontents.find("calculationparameters") + number_of_atoms_check = (int(dftparams.find("number_of_atoms").text) + != self.parameters.number_of_atoms) if ( + ignore_atom_number is False) else False if dftparams.find("element").text != self.parameters.element or \ dftparams.find("crystal_structure").text != self.parameters.crystal_structure or \ dftparams.find("dft_calculator").text != self.parameters.dft_calculator or \ float(dftparams.find("temperature").text) != self.parameters.temperature or \ - int(dftparams.find("number_of_atoms").text) != self.parameters.number_of_atoms: + number_of_atoms_check: raise Exception("Incompatible convergence parameters provided.") ldos_params = {"ldos_offset_eV": float(filecontents.find("ldos_offset_eV").text), "ldos_spacing_eV": float(filecontents.find("ldos_spacing_eV").text), diff --git a/malada/providers/provider.py b/malada/providers/provider.py index a7bff70..4f02e35 100644 --- a/malada/providers/provider.py +++ b/malada/providers/provider.py @@ -30,16 +30,20 @@ def provide(self, provider_path): """ pass - def _read_convergence(self, filename): + def _read_convergence(self, filename, ignore_atom_number): # Parse the XML file and first check for consistency. filecontents = ET.parse(filename).getroot() dftparams = filecontents.find("calculationparameters") + number_of_atoms_check = (int(dftparams.find("number_of_atoms").text) + != self.parameters.number_of_atoms) if ( + ignore_atom_number is False) else False + if dftparams.find("element").text != self.parameters.element or \ dftparams.find("crystal_structure").text != self.parameters.crystal_structure or \ dftparams.find("dft_calculator").text != self.parameters.dft_calculator or \ float(dftparams.find("temperature").text) != self.parameters.temperature or \ - int(dftparams.find("number_of_atoms").text) != self.parameters.number_of_atoms: + number_of_atoms_check: raise Exception("Incompatible convergence parameters provided.") cutoff_energy = int(filecontents.find("cutoff_energy").text) diff --git a/malada/utils/parameters.py b/malada/utils/parameters.py index 9c656b5..7be8ddb 100644 --- a/malada/utils/parameters.py +++ b/malada/utils/parameters.py @@ -148,6 +148,7 @@ def __init__(self): self.dft_calculate_force = True self.dft_use_inversion_symmetry = False self.dft_mixing_beta = 0.1 + self.dft_assume_two_dimensional = True # Information about MD parsing. self.snapshot_parsing_beginning = -1 From 448e5396ea8ee36cc1ef825b9156b0f9c55a6300 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Wed, 16 Oct 2024 08:37:07 +0200 Subject: [PATCH 04/13] Small bugfix for cif parser --- malada/providers/supercell.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/malada/providers/supercell.py b/malada/providers/supercell.py index ddd5957..b7774ae 100644 --- a/malada/providers/supercell.py +++ b/malada/providers/supercell.py @@ -214,10 +214,13 @@ def get_transformation_matrix(self, cif_file): ) # matrix which creates supercell transform_ratios = [1, 1, 1] + i = 0 while atoms_over_nsites > 1: - for i in range(3): - transform_ratios[i] *= 2 - atoms_over_nsites /= 2 + transform_ratios[i] *= 2 + atoms_over_nsites /= 2 + i += 1 + if i > 2: + i = 0 transformation_matrix = [ [transform_ratios[0], 0, 0], From cac2a36dd82c836ff85e7dcfaa42845dce1ab718 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 17 Oct 2024 09:39:51 +0200 Subject: [PATCH 05/13] Changed default of 2D materials --- malada/utils/convergence_guesses.py | 6 ++++-- malada/utils/parameters.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 9ade087..6328468 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -10,10 +10,12 @@ cutoff_guesses_vasp = {"Fe": [268, 368, 468, 568, 668, 768], "Al": [240, 340, 440, 540], "Be": [248, 258, 268, 278], - "H": [400, 410, 420, 430, 440, 450]} + "H": [400, 410, 420, 430, 440, 450], + "Li": [140, 150, 160, 170],} kpoints_guesses = {"Fe": [2, 3, 4], "Be": [2, 3, 4, 5, 6], "Al": [2, 3, 4], "H": [2, 3, 4, 5], - "C": [1, 2, 3, 4]} + "C": [1, 2, 3, 4], + "Li": [1, 2, 3, 4],} diff --git a/malada/utils/parameters.py b/malada/utils/parameters.py index 7be8ddb..d4a9d14 100644 --- a/malada/utils/parameters.py +++ b/malada/utils/parameters.py @@ -148,7 +148,7 @@ def __init__(self): self.dft_calculate_force = True self.dft_use_inversion_symmetry = False self.dft_mixing_beta = 0.1 - self.dft_assume_two_dimensional = True + self.dft_assume_two_dimensional = False # Information about MD parsing. self.snapshot_parsing_beginning = -1 From e2c2e9ee66d168c7842c16a761d5ee657f85e6e0 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 17 Oct 2024 11:13:44 +0200 Subject: [PATCH 06/13] Small bugfix --- malada/providers/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malada/providers/provider.py b/malada/providers/provider.py index 4f02e35..8f221c2 100644 --- a/malada/providers/provider.py +++ b/malada/providers/provider.py @@ -30,7 +30,7 @@ def provide(self, provider_path): """ pass - def _read_convergence(self, filename, ignore_atom_number): + def _read_convergence(self, filename, ignore_atom_number=False): # Parse the XML file and first check for consistency. filecontents = ET.parse(filename).getroot() From e35c8aa91d13f4f1cdcbff5b8ead344883b260b1 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Fri, 18 Oct 2024 11:17:21 +0200 Subject: [PATCH 07/13] Added convergence guess for Li --- malada/utils/convergence_guesses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 6328468..4c0de62 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -5,7 +5,8 @@ "Be": [40, 50, 60, 70], "Al": [20, 30, 40, 50, 60, 70], "H": [26, 36, 46, 56, 66, 76], - "C": [70, 80, 90, 100, 110, 120]} + "C": [70, 80, 90, 100, 110, 120], + "Li": [50, 60, 70, 80, 90, 100]} cutoff_guesses_vasp = {"Fe": [268, 368, 468, 568, 668, 768], "Al": [240, 340, 440, 540], From 192f6708ac42cc507477874548cb55863a051c36 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Mon, 21 Oct 2024 11:28:39 +0200 Subject: [PATCH 08/13] A few changes to accomodate Boron --- malada/providers/supercell.py | 7 ++-- malada/utils/__init__.py | 1 - malada/utils/cell_transformations.py | 49 ---------------------------- malada/utils/convergence_guesses.py | 3 +- 4 files changed, 5 insertions(+), 55 deletions(-) delete mode 100644 malada/utils/cell_transformations.py diff --git a/malada/providers/supercell.py b/malada/providers/supercell.py index b7774ae..e1e0b95 100644 --- a/malada/providers/supercell.py +++ b/malada/providers/supercell.py @@ -1,5 +1,4 @@ """Provider for creation of supercell from crystal structure.""" -from malada.utils import structure_to_transformation from .provider import Provider import os import ase.io @@ -206,11 +205,11 @@ def get_transformation_matrix(self, cif_file): "It must contain _cell_formula_units_Z." ) atoms_over_nsites = self.parameters.number_of_atoms / nsites - if atoms_over_nsites % 2 != 0: + if np.log2(atoms_over_nsites) % 1 != 0: raise Exception( - "Number of atoms together with crystal structure " + "Number of atoms with this crystal structure " "is not supported. The ratio of these two values " - "must be an even number." + "must be a power of 2." ) # matrix which creates supercell transform_ratios = [1, 1, 1] diff --git a/malada/utils/__init__.py b/malada/utils/__init__.py index d15debe..ccb1bb5 100644 --- a/malada/utils/__init__.py +++ b/malada/utils/__init__.py @@ -1,7 +1,6 @@ """MALADA utils provides utilities for building data acquistion pipelines.""" from .parameters import Parameters -from .cell_transformations import * from .convergence_guesses import * from .custom_converter import * from .slurmparams import SlurmParameters diff --git a/malada/utils/cell_transformations.py b/malada/utils/cell_transformations.py deleted file mode 100644 index d91ca5e..0000000 --- a/malada/utils/cell_transformations.py +++ /dev/null @@ -1,49 +0,0 @@ -"""WILL BE DELETED: Transformation matrices for supercell creation.""" - -# TODO: I know this can be replaced by a mathematical expression. This is just -# for the first tests. -# Also it is not really correct in its current form. - - -two_atoms_per_cell = {2: [[1, 0, 0], - [0, 1, 0], - [0, 0, 1]], - 4: [[2, 0, 0], - [0, 1, 0], - [0, 0, 1]], - 16: [[2, 0, 0], - [0, 2, 0], - [0, 0, 2]], - 32: [[4, 0, 0], - [0, 2, 0], - [0, 0, 2]], - 64: [[4, 0, 0], - [0, 4, 0], - [0, 0, 2]], - 128: [[4, 0, 0], - [0, 4, 0], - [0, 0, 4]], - 256: [[8, 0, 0], - [0, 4, 0], - [0, 0, 4]]} -four_atoms_per_cell = {4: [[1, 0, 0], - [0, 1, 0], - [0, 0, 1]], - 16: [[2, 0, 0], - [0, 2, 0], - [0, 0, 1]], - 32: [[2, 0, 0], - [0, 2, 0], - [0, 0, 2]], - 64: [[4, 0, 0], - [0, 2, 0], - [0, 0, 2]], - 128: [[4, 0, 0], - [0, 4, 0], - [0, 0, 2]], - 256: [[4, 0, 0], - [0, 4, 0], - [0, 0, 4]]} -structure_to_transformation = {"bcc": two_atoms_per_cell, - "hcp": two_atoms_per_cell, - "fcc": four_atoms_per_cell} diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 4c0de62..9355da6 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -12,7 +12,8 @@ "Al": [240, 340, 440, 540], "Be": [248, 258, 268, 278], "H": [400, 410, 420, 430, 440, 450], - "Li": [140, 150, 160, 170],} + "Li": [140, 150, 160, 170], + "B": [310, 320, 330, 340]} kpoints_guesses = {"Fe": [2, 3, 4], "Be": [2, 3, 4, 5, 6], From 38c8a14b957a21c52598f5642d32cf265b5b85ce Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Tue, 22 Oct 2024 08:54:34 +0200 Subject: [PATCH 09/13] Added convergence guesses for B --- malada/utils/convergence_guesses.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 9355da6..59df740 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -6,7 +6,8 @@ "Al": [20, 30, 40, 50, 60, 70], "H": [26, 36, 46, 56, 66, 76], "C": [70, 80, 90, 100, 110, 120], - "Li": [50, 60, 70, 80, 90, 100]} + "Li": [50, 60, 70, 80, 90, 100], + "B": [50, 60, 70, 80, 90, 100],} cutoff_guesses_vasp = {"Fe": [268, 368, 468, 568, 668, 768], "Al": [240, 340, 440, 540], @@ -20,4 +21,6 @@ "Al": [2, 3, 4], "H": [2, 3, 4, 5], "C": [1, 2, 3, 4], - "Li": [1, 2, 3, 4],} + "Li": [1, 2, 3, 4], + "B": [1, 2, 3, 4]} + From 4df2f7fc82b4260ad4596a057f7d5f4e8cf6415f Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 24 Oct 2024 19:16:26 +0200 Subject: [PATCH 10/13] Small change to make things consisten with MALA --- malada/providers/snapshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malada/providers/snapshots.py b/malada/providers/snapshots.py index 5db266f..127fe91 100644 --- a/malada/providers/snapshots.py +++ b/malada/providers/snapshots.py @@ -300,7 +300,7 @@ def analyze_distance_metric(self, trajectory): # distance metric usef for the snapshot parsing (realspace similarity # of the snapshot), we first find the center of the equilibrated part # of the trajectory and calculate the differences w.r.t to to it. - center = int((np.shape(self.distance_metrics_denoised)[0]-self.first_snapshot)/2) + center = int((np.shape(self.distance_metrics_denoised)[0]-self.first_snapshot)/2) + self.first_snapshot width = int(self.parameters.distance_metrics_estimated_equilibrium * np.shape(self.distance_metrics_denoised)[0]) self.distances_realspace = [] From 0c385092133c2cc8ec7e6bc93e6808ce0db03260 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 24 Oct 2024 19:19:07 +0200 Subject: [PATCH 11/13] Blackified the code --- examples/ex01_reduced_pipeline.py | 26 +- malada/__init__.py | 16 +- malada/pipeline/pipeline.py | 130 +++--- malada/providers/crystalstructure.py | 16 +- malada/providers/dft.py | 196 ++++++--- malada/providers/dftconvergence.py | 622 ++++++++++++++++++--------- malada/providers/ldosconvergence.py | 30 +- malada/providers/md.py | 161 ++++--- malada/providers/mdperformance.py | 35 +- malada/providers/provider.py | 90 ++-- malada/providers/snapshots.py | 257 +++++++---- malada/providers/supercell.py | 61 ++- malada/runners/__init__.py | 1 + malada/runners/bashrunner.py | 49 ++- malada/runners/runner.py | 1 + malada/runners/runner_interface.py | 2 +- malada/runners/slurm_creator.py | 151 +++++-- malada/utils/convergence_guesses.py | 48 ++- malada/utils/custom_converter.py | 14 +- malada/utils/parameters.py | 7 +- malada/utils/slurmparams.py | 53 +-- malada/utils/vasp_utils.py | 6 +- malada/version.py | 2 +- pyproject.toml | 6 + 24 files changed, 1297 insertions(+), 683 deletions(-) create mode 100644 pyproject.toml diff --git a/examples/ex01_reduced_pipeline.py b/examples/ex01_reduced_pipeline.py index 436d0aa..40ddba6 100644 --- a/examples/ex01_reduced_pipeline.py +++ b/examples/ex01_reduced_pipeline.py @@ -21,8 +21,9 @@ params.pseudopotential["name"] = "Be.pbe-n-rrkjus_psl.1.0.0.UPF" # This path has to be altered updated with your path. -params.pseudopotential["path"] = "/home/fiedlerl/tools/pslibrary/" \ - "pbe/PSEUDOPOTENTIALS/" +params.pseudopotential["path"] = ( + "/home/fiedlerl/tools/pslibrary/" "pbe/PSEUDOPOTENTIALS/" +) # These are technical parameters for DFT and MD. params.maximum_kpoint_try = 3 @@ -41,18 +42,19 @@ # Currently, both the crystal structure and LDOS configuration have to be # provided by the user. -crystal_structure = malada.CrystalStructureProvider(params, - external_cif_file= - "./ex01_inputs/Be_bcc.cif") +crystal_structure = malada.CrystalStructureProvider( + params, external_cif_file="./ex01_inputs/Be_bcc.cif" +) -ldosconvergence = malada.LDOSConvergenceProvider(params, - external_ldos_configuration= - "./ex01_inputs/ldosconf.xml") +ldosconvergence = malada.LDOSConvergenceProvider( + params, external_ldos_configuration="./ex01_inputs/ldosconf.xml" +) # After defining parameters and important files, the pipeline can be # instantiated and run. -pipeline = malada.DataPipeline(params, - crystal_structure_provider=crystal_structure, - ldos_configuration_provider=ldosconvergence) +pipeline = malada.DataPipeline( + params, + crystal_structure_provider=crystal_structure, + ldos_configuration_provider=ldosconvergence, +) pipeline.run() - diff --git a/malada/__init__.py b/malada/__init__.py index bf75d65..af7389d 100644 --- a/malada/__init__.py +++ b/malada/__init__.py @@ -4,13 +4,17 @@ Can be used to automate a data acquistion pipeline for MALA. """ - from .version import __version__ from .utils import Parameters, SlurmParameters -from .providers import CrystalStructureProvider, SuperCellProvider,\ - DFTConvergenceProvider, MDPerformanceProvider,\ - MDProvider, SnapshotsProvider, LDOSConvergenceProvider,\ - DFTProvider +from .providers import ( + CrystalStructureProvider, + SuperCellProvider, + DFTConvergenceProvider, + MDPerformanceProvider, + MDProvider, + SnapshotsProvider, + LDOSConvergenceProvider, + DFTProvider, +) from .pipeline import DataPipeline from .runners import Runner, BashRunner, RunnerInterface - diff --git a/malada/pipeline/pipeline.py b/malada/pipeline/pipeline.py index e26190b..085e910 100644 --- a/malada/pipeline/pipeline.py +++ b/malada/pipeline/pipeline.py @@ -1,7 +1,15 @@ """Data generation Pipeline.""" -from malada import CrystalStructureProvider, SuperCellProvider, \ - DFTConvergenceProvider, MDPerformanceProvider, MDProvider, \ - SnapshotsProvider, LDOSConvergenceProvider, DFTProvider + +from malada import ( + CrystalStructureProvider, + SuperCellProvider, + DFTConvergenceProvider, + MDPerformanceProvider, + MDProvider, + SnapshotsProvider, + LDOSConvergenceProvider, + DFTProvider, +) import os @@ -39,42 +47,45 @@ class DataPipeline: Provider for final DFT calculations and LDOS calculation """ - def __init__(self, parameters, - crystal_structure_provider: CrystalStructureProvider = None, - supercell_provider: SuperCellProvider = None, - dft_convergence_provider: DFTConvergenceProvider = None, - md_performance_provider: MDPerformanceProvider = None, - md_provider: MDProvider = None, - snapshots_provider: SnapshotsProvider = None, - ldos_configuration_provider: LDOSConvergenceProvider = None, - dft_provider: DFTProvider = None, - ): + def __init__( + self, + parameters, + crystal_structure_provider: CrystalStructureProvider = None, + supercell_provider: SuperCellProvider = None, + dft_convergence_provider: DFTConvergenceProvider = None, + md_performance_provider: MDPerformanceProvider = None, + md_provider: MDProvider = None, + snapshots_provider: SnapshotsProvider = None, + ldos_configuration_provider: LDOSConvergenceProvider = None, + dft_provider: DFTProvider = None, + ): self.parameters = parameters # Create the providers object that were not otherwise specified. if crystal_structure_provider is None: - self.crystal_structure_provider = \ - CrystalStructureProvider(self.parameters) + self.crystal_structure_provider = CrystalStructureProvider( + self.parameters + ) else: self.crystal_structure_provider = crystal_structure_provider if supercell_provider is None: - self.supercell_provider = \ - SuperCellProvider(self.parameters) + self.supercell_provider = SuperCellProvider(self.parameters) else: self.supercell_provider = supercell_provider if dft_convergence_provider is None: - self.dft_convergence_provider = \ - DFTConvergenceProvider(self.parameters) + self.dft_convergence_provider = DFTConvergenceProvider( + self.parameters + ) else: self.dft_convergence_provider = dft_convergence_provider if md_performance_provider is None: - self.md_performance_provider = \ - MDPerformanceProvider(self.parameters) + self.md_performance_provider = MDPerformanceProvider( + self.parameters + ) else: self.md_performance_provider = md_performance_provider if md_provider is None: - self.md_provider = \ - MDProvider(self.parameters) + self.md_provider = MDProvider(self.parameters) else: self.md_provider = md_provider if snapshots_provider is None: @@ -82,7 +93,9 @@ def __init__(self, parameters, else: self.snapshots_provider = snapshots_provider if ldos_configuration_provider is None: - self.ldos_configuration_provider = LDOSConvergenceProvider(self.parameters) + self.ldos_configuration_provider = LDOSConvergenceProvider( + self.parameters + ) else: self.ldos_configuration_provider = ldos_configuration_provider if dft_provider is None: @@ -94,8 +107,9 @@ def run(self): """Run a full data generation pipeline.""" # Step one: Get the crystal structure. print("Getting the crystal structure...") - path00 = os.path.join(self.parameters.base_folder, - "00_crystal_structure") + path00 = os.path.join( + self.parameters.base_folder, "00_crystal_structure" + ) if not os.path.exists(path00): os.makedirs(path00) self.crystal_structure_provider.provide(path00) @@ -104,20 +118,24 @@ def run(self): # Step two: Build the supercell. print("Building supercell...") - path01 = os.path.join(self.parameters.base_folder, - "01_supercell") + path01 = os.path.join(self.parameters.base_folder, "01_supercell") if not os.path.exists(path01): os.makedirs(path01) - self.supercell_provider.provide(path01, self.crystal_structure_provider.cif_file) + self.supercell_provider.provide( + path01, self.crystal_structure_provider.cif_file + ) print("Building supercell: Done.") # Step three: Converge DFT parameters. print("Converging DFT parameters...") - path02 = os.path.join(self.parameters.base_folder, "02_dft_convergence") + path02 = os.path.join( + self.parameters.base_folder, "02_dft_convergence" + ) if not os.path.exists(path02): os.makedirs(path02) - self.dft_convergence_provider.provide(path02, - self.supercell_provider.supercell_file) + self.dft_convergence_provider.provide( + path02, self.supercell_provider.supercell_file + ) print("Converging DFT parameters: Done.") # Step four: Get optimal MD run parameters. @@ -125,9 +143,9 @@ def run(self): path03 = os.path.join(self.parameters.base_folder, "03_md_performance") if not os.path.exists(path03): os.makedirs(path03) - self.md_performance_provider.provide(path03, - self.dft_convergence_provider. - convergence_results_file) + self.md_performance_provider.provide( + path03, self.dft_convergence_provider.convergence_results_file + ) print("Testing MD performance: Done.") # Step five: Get/Calculate a MD trajectory. @@ -135,10 +153,12 @@ def run(self): path04 = os.path.join(self.parameters.base_folder, "04_md") if not os.path.exists(path04): os.makedirs(path04) - self.md_provider.provide(path04, self.supercell_provider.supercell_file, - self.dft_convergence_provider. - convergence_results_file, self. - md_performance_provider.md_performance_xml) + self.md_provider.provide( + path04, + self.supercell_provider.supercell_file, + self.dft_convergence_provider.convergence_results_file, + self.md_performance_provider.md_performance_xml, + ) print("Getting MD trajectory: Done.") # Step six: Parsing MD trajectory for snapshots. @@ -146,28 +166,36 @@ def run(self): path05 = os.path.join(self.parameters.base_folder, "05_snapshots") if not os.path.exists(path05): os.makedirs(path05) - self.snapshots_provider.provide(path05, - self.md_provider.trajectory_file, - self.md_provider.temperature_file) + self.snapshots_provider.provide( + path05, + self.md_provider.trajectory_file, + self.md_provider.temperature_file, + ) print("Parsing snapshots from MD trajectory: Done.") # Step seven: Determining the LDOS parameters. print("Determining LDOS parameters...") - path06 = os.path.join(self.parameters.base_folder, "06_ldosconfiguration") + path06 = os.path.join( + self.parameters.base_folder, "06_ldosconfiguration" + ) if not os.path.exists(path06): os.makedirs(path06) - self.ldos_configuration_provider.provide(path06, - self.snapshots_provider.snapshot_file, - self.dft_convergence_provider.convergence_results_file) + self.ldos_configuration_provider.provide( + path06, + self.snapshots_provider.snapshot_file, + self.dft_convergence_provider.convergence_results_file, + ) print("Determining LDOS parameters: Done.") # Step eight (final step): Perfroming the necessary DFT calculations. print("Performing DFT calculation...") - path07 = os.path.join(self.parameters.base_folder, - "07_dft") + path07 = os.path.join(self.parameters.base_folder, "07_dft") if not os.path.exists(path07): os.makedirs(path07) - self.dft_provider.provide(path07,self.dft_convergence_provider.convergence_results_file, - self.ldos_configuration_provider.ldos_configuration_file, - self.snapshots_provider.snapshot_file) + self.dft_provider.provide( + path07, + self.dft_convergence_provider.convergence_results_file, + self.ldos_configuration_provider.ldos_configuration_file, + self.snapshots_provider.snapshot_file, + ) print("Performing DFT calculation: Done.") diff --git a/malada/providers/crystalstructure.py b/malada/providers/crystalstructure.py index 8d4c491..ddc54d5 100644 --- a/malada/providers/crystalstructure.py +++ b/malada/providers/crystalstructure.py @@ -1,4 +1,5 @@ """Provider for crystal structures.""" + from .provider import Provider import os from shutil import copyfile @@ -35,12 +36,17 @@ def provide(self, provider_path): provider_path : string Path in which to operate in. """ - file_name = self.parameters.element + \ - "_" + self.parameters.crystal_structure + ".cif" - self.cif_file = os.path.join(provider_path,file_name) + file_name = ( + self.parameters.element + + "_" + + self.parameters.crystal_structure + + ".cif" + ) + self.cif_file = os.path.join(provider_path, file_name) if self.external_cif_file is None: - raise Exception("Currently there is no way to provide a cif file" - "on the fly.") + raise Exception( + "Currently there is no way to provide a cif file" "on the fly." + ) else: copyfile(self.external_cif_file, self.cif_file) diff --git a/malada/providers/dft.py b/malada/providers/dft.py index 854f3a8..7f1b08f 100644 --- a/malada/providers/dft.py +++ b/malada/providers/dft.py @@ -1,4 +1,5 @@ """Provider for DFT calculations to get energies and LDOS.""" + from .provider import Provider import os from shutil import copyfile @@ -31,10 +32,17 @@ def __init__(self, parameters, external_calculation_folders=None): self.external_calculation_folders = external_calculation_folders self.calculation_folders = None - def provide(self, provider_path, dft_convergence_file, - ldos_convergence_file, possible_snapshots_file, - do_postprocessing=True, numbering_starts_at=0, - parsing_starts_at=0, ignore_atom_number=False): + def provide( + self, + provider_path, + dft_convergence_file, + ldos_convergence_file, + possible_snapshots_file, + do_postprocessing=True, + numbering_starts_at=0, + parsing_starts_at=0, + ignore_atom_number=False, + ): """ Provide a set of DFT calculations on predefined snapshots. @@ -79,24 +87,35 @@ def provide(self, provider_path, dft_convergence_file, if self.external_calculation_folders is None: # Here we have to perform the actucal calculation. if self.parameters.dft_calculator != "qe": - raise Exception("Currently only QE is supported for " - "DFT calculations.") + raise Exception( + "Currently only QE is supported for " "DFT calculations." + ) dft_runner = malada.RunnerInterface(self.parameters) all_valid_snapshots = ase.io.Trajectory(possible_snapshots_file) for i in range(0, self.parameters.number_of_snapshots): snapshot_number = i + numbering_starts_at - snapshot_path = os.path.join(provider_path,"snapshot"+str(snapshot_number)) - self.__create_dft_run(dft_convergence_file, - ldos_convergence_file, - all_valid_snapshots[snapshot_number-numbering_starts_at+parsing_starts_at], - snapshot_path, - "snapshot"+str(snapshot_number), - do_postprocessing, - ignore_atom_number) + snapshot_path = os.path.join( + provider_path, "snapshot" + str(snapshot_number) + ) + self.__create_dft_run( + dft_convergence_file, + ldos_convergence_file, + all_valid_snapshots[ + snapshot_number + - numbering_starts_at + + parsing_starts_at + ], + snapshot_path, + "snapshot" + str(snapshot_number), + do_postprocessing, + ignore_atom_number, + ) for i in range(0, self.parameters.number_of_snapshots): # Run the individul files. snapshot_number = i + numbering_starts_at - snapshot_path = os.path.join(provider_path,"snapshot"+str(snapshot_number)) + snapshot_path = os.path.join( + provider_path, "snapshot" + str(snapshot_number) + ) print("Running DFT in", snapshot_path) if do_postprocessing: dft_runner.run_folder(snapshot_path, "dft+pp") @@ -106,35 +125,46 @@ def provide(self, provider_path, dft_convergence_file, # Here, we have to do a consistency check. pass - def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, - atoms, snapshot_path, snapshot_name, - do_postprocessing, ignore_atom_number): + def __create_dft_run( + self, + dft_convergence_file, + ldos_convergence_file, + atoms, + snapshot_path, + snapshot_name, + do_postprocessing, + ignore_atom_number, + ): # Get cluster info # TODO: Use DFT kgrid for scf and LDOS kgrid for NSCF calculations. - cutoff, kgrid = self._read_convergence(dft_convergence_file, - ignore_atom_number) - ldos_params, kgrid = self.__read_ldos_convergence(ldos_convergence_file, - ignore_atom_number) + cutoff, kgrid = self._read_convergence( + dft_convergence_file, ignore_atom_number + ) + ldos_params, kgrid = self.__read_ldos_convergence( + ldos_convergence_file, ignore_atom_number + ) # Create folder if not os.path.exists(snapshot_path): os.makedirs(snapshot_path) - qe_pseudopotentials = {self.parameters.element: - self.parameters.pseudopotential["name"]} + qe_pseudopotentials = { + self.parameters.element: self.parameters.pseudopotential["name"] + } nbands = self._get_number_of_bands() outdir = "temp" qe_input_data = { - "occupations": 'smearing', - "calculation": 'scf', - "restart_mode": 'from_scratch', - "verbosity": 'high', + "occupations": "smearing", + "calculation": "scf", + "restart_mode": "from_scratch", + "verbosity": "high", "prefix": self.parameters.element, "pseudo_dir": self.parameters.pseudopotential["path"], "outdir": outdir, "ibrav": 0, - "smearing": 'fermi-dirac', - "degauss": round(kelvin_to_rydberg( - self.parameters.temperature), 7), + "smearing": "fermi-dirac", + "degauss": round( + kelvin_to_rydberg(self.parameters.temperature), 7 + ), "ecutrho": cutoff * 4, "ecutwfc": cutoff, "tstress": True, @@ -142,11 +172,11 @@ def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, "nbnd": nbands, "mixing_mode": "plain", "mixing_beta": self.parameters.dft_mixing_beta, - "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry * self.parameters.number_of_atoms, + "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry + * self.parameters.number_of_atoms, # "verbosity" : "high", # This is maybe a bit high "nosym": True, - "noinv": True - + "noinv": True, } if self.parameters.dft_use_inversion_symmetry is True: qe_input_data["noinv"] = False @@ -157,67 +187,78 @@ def __create_dft_run(self, dft_convergence_file, ldos_convergence_file, if self.parameters.dft_assume_two_dimensional: qe_input_data["assume_isolated"] = "2D" - id_string = self.parameters.element+"_"+snapshot_name - ase.io.write(os.path.join(snapshot_path,id_string+".pw.scf.in"), + id_string = self.parameters.element + "_" + snapshot_name + ase.io.write( + os.path.join(snapshot_path, id_string + ".pw.scf.in"), atoms, - "espresso-in", input_data=qe_input_data, pseudopotentials= \ - qe_pseudopotentials, kpts=kgrid) + "espresso-in", + input_data=qe_input_data, + pseudopotentials=qe_pseudopotentials, + kpts=kgrid, + ) emin = ldos_params["ldos_offset_eV"] deltae = ldos_params["ldos_spacing_eV"] - emax = ldos_params["ldos_length"]*ldos_params["ldos_spacing_eV"]+ldos_params["ldos_offset_eV"] + emax = ( + ldos_params["ldos_length"] * ldos_params["ldos_spacing_eV"] + + ldos_params["ldos_offset_eV"] + ) smearing_factor = ldos_params["smearing_factor"] if do_postprocessing: # DOS file dos_file = open( - os.path.join(snapshot_path,id_string+".dos.in"), - mode='w') + os.path.join(snapshot_path, id_string + ".dos.in"), mode="w" + ) dos_file.write("&dos\n") - dos_file.write(" outdir='" + outdir+"',\n") + dos_file.write(" outdir='" + outdir + "',\n") dos_file.write(" prefix='" + self.parameters.element + "',\n") dos_file.write(" Emin=" + str(emin) + ",\n") dos_file.write(" Emax=" + str(emax) + ",\n") dos_file.write(" DeltaE=" + str(deltae) + ",\n") dos_file.write( - " degauss=" + str(deltae * smearing_factor / Rydberg) + ",\n") + " degauss=" + str(deltae * smearing_factor / Rydberg) + ",\n" + ) dos_file.write(" fildos='" + id_string + ".dos'\n") dos_file.write("/\n") # LDOS file - ldos_file = open(os.path.join(snapshot_path,id_string+".pp.ldos.in"), - mode='w') + ldos_file = open( + os.path.join(snapshot_path, id_string + ".pp.ldos.in"), + mode="w", + ) ldos_file.write("&inputpp\n") - ldos_file.write(" outdir='" + outdir+"',\n") + ldos_file.write(" outdir='" + outdir + "',\n") ldos_file.write(" prefix='" + self.parameters.element + "',\n") ldos_file.write(" plot_num=3,\n") ldos_file.write(" emin=" + str(emin) + ",\n") ldos_file.write(" emax=" + str(emax) + ",\n") ldos_file.write(" delta_e=" + str(deltae) + ",\n") ldos_file.write( - " degauss_ldos=" + str(deltae * smearing_factor) + ",\n") + " degauss_ldos=" + str(deltae * smearing_factor) + ",\n" + ) ldos_file.write(" use_gauss_ldos=.true.\n") ldos_file.write("/\n") ldos_file.write("&plot\n") ldos_file.write(" iflag=3,\n") ldos_file.write(" output_format=6,\n") - ldos_file.write( - " fileout='" + id_string + "_ldos.cube',\n") + ldos_file.write(" fileout='" + id_string + "_ldos.cube',\n") ldos_file.write("/\n") # Density file - dens_file = open(os.path.join(snapshot_path,id_string+".pp.dens.in"), - mode='w') + dens_file = open( + os.path.join(snapshot_path, id_string + ".pp.dens.in"), + mode="w", + ) dens_file.write("&inputpp\n") - dens_file.write(" outdir='" + outdir+"',\n") + dens_file.write(" outdir='" + outdir + "',\n") dens_file.write(" prefix='" + self.parameters.element + "',\n") dens_file.write(" plot_num=0,\n") dens_file.write("/\n") dens_file.write("&plot\n") dens_file.write(" iflag=3,\n") dens_file.write(" output_format=6,\n") - dens_file.write( - " fileout='" + id_string + "_dens.cube',\n") + dens_file.write(" fileout='" + id_string + "_dens.cube',\n") dens_file.write("/\n") dos_file.close() @@ -229,21 +270,40 @@ def __read_ldos_convergence(self, filename, ignore_atom_number): # Parse the XML file and first check for consistency. filecontents = ET.parse(filename).getroot() dftparams = filecontents.find("calculationparameters") - number_of_atoms_check = (int(dftparams.find("number_of_atoms").text) - != self.parameters.number_of_atoms) if ( - ignore_atom_number is False) else False - if dftparams.find("element").text != self.parameters.element or \ - dftparams.find("crystal_structure").text != self.parameters.crystal_structure or \ - dftparams.find("dft_calculator").text != self.parameters.dft_calculator or \ - float(dftparams.find("temperature").text) != self.parameters.temperature or \ - number_of_atoms_check: + number_of_atoms_check = ( + ( + int(dftparams.find("number_of_atoms").text) + != self.parameters.number_of_atoms + ) + if (ignore_atom_number is False) + else False + ) + if ( + dftparams.find("element").text != self.parameters.element + or dftparams.find("crystal_structure").text + != self.parameters.crystal_structure + or dftparams.find("dft_calculator").text + != self.parameters.dft_calculator + or float(dftparams.find("temperature").text) + != self.parameters.temperature + or number_of_atoms_check + ): raise Exception("Incompatible convergence parameters provided.") - ldos_params = {"ldos_offset_eV": float(filecontents.find("ldos_offset_eV").text), - "ldos_spacing_eV": float(filecontents.find("ldos_spacing_eV").text), - "ldos_length": int(filecontents.find("ldos_length").text), - "smearing_factor": float(filecontents.find("smearing_factor").text),} + ldos_params = { + "ldos_offset_eV": float(filecontents.find("ldos_offset_eV").text), + "ldos_spacing_eV": float( + filecontents.find("ldos_spacing_eV").text + ), + "ldos_length": int(filecontents.find("ldos_length").text), + "smearing_factor": float( + filecontents.find("smearing_factor").text + ), + } kpoints = filecontents.find("kpoints") - kgrid = (int(kpoints.find("kx").text),int(kpoints.find("ky").text), - int(kpoints.find("kz").text)) + kgrid = ( + int(kpoints.find("kx").text), + int(kpoints.find("ky").text), + int(kpoints.find("kz").text), + ) return ldos_params, kgrid diff --git a/malada/providers/dftconvergence.py b/malada/providers/dftconvergence.py index a6a62b8..943b976 100644 --- a/malada/providers/dftconvergence.py +++ b/malada/providers/dftconvergence.py @@ -1,4 +1,5 @@ """Provider for optimized DFT calculation parameters.""" + import os import numpy as np import glob @@ -43,9 +44,14 @@ class DFTConvergenceProvider(Provider): no attempt will be made to find a more optimal one. """ - def __init__(self, parameters, external_convergence_results=None, - external_convergence_folder=None, - predefined_kgrid=None, predefined_cutoff=None): + def __init__( + self, + parameters, + external_convergence_results=None, + external_convergence_folder=None, + predefined_kgrid=None, + predefined_cutoff=None, + ): super(DFTConvergenceProvider, self).__init__(parameters) self.parameters = parameters self.external_convergence_results = external_convergence_results @@ -54,7 +60,9 @@ def __init__(self, parameters, external_convergence_results=None, self.converged_cutoff = predefined_cutoff self.converged_kgrid = predefined_kgrid - def provide(self, provider_path, supercell_file, create_submit_script=True): + def provide( + self, provider_path, supercell_file, create_submit_script=True + ): """ Provide DFT parameters converged to within user specification. @@ -67,11 +75,17 @@ def provide(self, provider_path, supercell_file, create_submit_script=True): supercell_file """ - file_name = self.parameters.element + \ - str(self.parameters.number_of_atoms) + \ - "_" + self.parameters.crystal_structure +\ - "_" + str(self.parameters.temperature) +\ - "_" + self.parameters.dft_calculator+".conv.xml" + file_name = ( + self.parameters.element + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + "_" + + str(self.parameters.temperature) + + "_" + + self.parameters.dft_calculator + + ".conv.xml" + ) self.convergence_results_file = os.path.join(provider_path, file_name) # Instantiate a runner. @@ -85,86 +99,116 @@ def provide(self, provider_path, supercell_file, create_submit_script=True): # Create, run and analyze. cutoff_try = 0 kpoint_try = 0 - while (self.converged_cutoff is None and cutoff_try - < self.parameters.maximum_cutoff_try): + while ( + self.converged_cutoff is None + and cutoff_try < self.parameters.maximum_cutoff_try + ): # First, we create the inputs. - cutoff_folders = self.\ - __create_dft_convergence_inputs("cutoff", supercell_file, - provider_path, cutoff_try) + cutoff_folders = self.__create_dft_convergence_inputs( + "cutoff", supercell_file, provider_path, cutoff_try + ) # Then we run. for cutoff_folder in cutoff_folders: print("Running DFT in", cutoff_folder) - dft_runner.run_folder(cutoff_folder, - "dft") + dft_runner.run_folder(cutoff_folder, "dft") # Afterwards, we check if running was succesful. - if self.__check_run_success(provider_path, "cutoff", fixed_kpoints=(1,1,1)): - self.converged_cutoff = self.__analyze_convergence_runs(provider_path, "cutoff", - supercell_file, - fixed_kpoints=(1, 1, 1)) + if self.__check_run_success( + provider_path, "cutoff", fixed_kpoints=(1, 1, 1) + ): + self.converged_cutoff = ( + self.__analyze_convergence_runs( + provider_path, + "cutoff", + supercell_file, + fixed_kpoints=(1, 1, 1), + ) + ) else: if self.parameters.run_system == "slurm_creator": all_submit_file = open( - os.path.join(provider_path, "submit_all.sh"), mode='w') + os.path.join(provider_path, "submit_all.sh"), + mode="w", + ) all_submit_file.write("#!/bin/bash\n\n") for cutoff_folder in cutoff_folders: - all_submit_file.write("cd "+cutoff_folder.split("/")[-2]+"\n") + all_submit_file.write( + "cd " + cutoff_folder.split("/")[-2] + "\n" + ) all_submit_file.write("sbatch submit.slurm\n") all_submit_file.write("cd ..\n") all_submit_file.close() - print("Run scripts created, please run via slurm.\n" - "Quitting now.") + print( + "Run scripts created, please run via slurm.\n" + "Quitting now." + ) quit() else: raise Exception("DFT calculations failed.") if self.converged_cutoff is None: - print("Could not find an aedaquate cutoff energy, " - "trying again with larger cutoff energies. " - "This will be try nr. " - +str(cutoff_try+2)) + print( + "Could not find an aedaquate cutoff energy, " + "trying again with larger cutoff energies. " + "This will be try nr. " + str(cutoff_try + 2) + ) cutoff_try += 1 # Second: cutoffs. # Create, run and analyze. - while (self.converged_kgrid is None and kpoint_try - < self.parameters.maximum_kpoint_try): - kpoints_folders = self. \ - __create_dft_convergence_inputs("kpoints", supercell_file, - provider_path, kpoint_try) + while ( + self.converged_kgrid is None + and kpoint_try < self.parameters.maximum_kpoint_try + ): + kpoints_folders = self.__create_dft_convergence_inputs( + "kpoints", supercell_file, provider_path, kpoint_try + ) for kpoint_folder in kpoints_folders: print("Running DFT in", kpoint_folder) - dft_runner.run_folder(kpoint_folder, - "dft") + dft_runner.run_folder(kpoint_folder, "dft") # Afterwards, we check if running was succesful. - if self.__check_run_success(provider_path, "kpoints", fixed_cutoff=self.converged_cutoff): + if self.__check_run_success( + provider_path, + "kpoints", + fixed_cutoff=self.converged_cutoff, + ): self.converged_kgrid = self.__analyze_convergence_runs( provider_path, - "kpoints", supercell_file, - fixed_cutoff=self.converged_cutoff) + "kpoints", + supercell_file, + fixed_cutoff=self.converged_cutoff, + ) else: if self.parameters.run_system == "slurm_creator": all_submit_file = open( - os.path.join(provider_path, "submit_all.sh"), mode='w') + os.path.join(provider_path, "submit_all.sh"), + mode="w", + ) all_submit_file.write("#!/bin/bash\n\n") for kpoint_folder in kpoints_folders: - all_submit_file.write("cd "+kpoint_folder.split("/")[-2]+"\n") + all_submit_file.write( + "cd " + kpoint_folder.split("/")[-2] + "\n" + ) all_submit_file.write("sbatch submit.slurm\n") all_submit_file.write("cd ..\n") all_submit_file.close() - print("Run scripts created, please run via slurm.\n" - "Quitting now.") + print( + "Run scripts created, please run via slurm.\n" + "Quitting now." + ) quit() else: raise Exception("DFT calculations failed.") if self.converged_kgrid is None: - print("Could not find an aedaquate k-grid, trying again" - "with larger k-grids. This will be try nr. " - +str(kpoint_try+2)) + print( + "Could not find an aedaquate k-grid, trying again" + "with larger k-grids. This will be try nr. " + + str(kpoint_try + 2) + ) kpoint_try += 1 else: # TODO: Add a consistency check here. It could be the provided @@ -173,22 +217,33 @@ def provide(self, provider_path, supercell_file, create_submit_script=True): # Analyze the convergence analsyis. # First: cutoffs. if self.converged_cutoff is None: - self.converged_cutoff = self.__analyze_convergence_runs(self.external_convergence_folder, "cutoff", - supercell_file, fixed_kpoints=(1, 1, 1)) + self.converged_cutoff = self.__analyze_convergence_runs( + self.external_convergence_folder, + "cutoff", + supercell_file, + fixed_kpoints=(1, 1, 1), + ) if self.converged_cutoff is None: - raise Exception("Provided convergence data not sufficient," - "please perform additional calculations" - " with larger cutoff energies.") + raise Exception( + "Provided convergence data not sufficient," + "please perform additional calculations" + " with larger cutoff energies." + ) # Second: kgrid. if self.converged_kgrid is None: - self.converged_kgrid = self.__analyze_convergence_runs(self.external_convergence_folder, - "kpoints",supercell_file, - fixed_cutoff=self.converged_cutoff) + self.converged_kgrid = self.__analyze_convergence_runs( + self.external_convergence_folder, + "kpoints", + supercell_file, + fixed_cutoff=self.converged_cutoff, + ) if self.converged_kgrid is None: - raise Exception("Provided convergence data not sufficient," - "please perform additional calculations" - " with larger k-grids.") + raise Exception( + "Provided convergence data not sufficient," + "please perform additional calculations" + " with larger k-grids." + ) # Print the output. unit = "eV" if self.parameters.dft_calculator == "vasp" else "Ry" print("Converged energy cutoff: ", self.converged_cutoff, unit) @@ -197,111 +252,155 @@ def provide(self, provider_path, supercell_file, create_submit_script=True): # Write the output to xml. self.__write_to_xml() else: - copyfile(self.external_convergence_results, - self.convergence_results_file) + copyfile( + self.external_convergence_results, + self.convergence_results_file, + ) print("Getting <>.xml file from disc.") - def __write_to_xml(self): - top = Element('dftparameters') + top = Element("dftparameters") calculationparameters = SubElement(top, "calculationparameters") - cpnode = SubElement(calculationparameters, "element", - {"type": "string"}) + cpnode = SubElement( + calculationparameters, "element", {"type": "string"} + ) cpnode.text = self.parameters.element - cpnode = SubElement(calculationparameters, "number_of_atoms", - {"type": "int"}) + cpnode = SubElement( + calculationparameters, "number_of_atoms", {"type": "int"} + ) cpnode.text = str(self.parameters.number_of_atoms) - cpnode = SubElement(calculationparameters, "crystal_structure", - {"type": "string"}) + cpnode = SubElement( + calculationparameters, "crystal_structure", {"type": "string"} + ) cpnode.text = self.parameters.crystal_structure - cpnode = SubElement(calculationparameters, "temperature", - {"type": "float"}) + cpnode = SubElement( + calculationparameters, "temperature", {"type": "float"} + ) cpnode.text = str(self.parameters.temperature) - cpnode = SubElement(calculationparameters, "dft_calculator", - {"type": "string"}) + cpnode = SubElement( + calculationparameters, "dft_calculator", {"type": "string"} + ) cpnode.text = self.parameters.dft_calculator - cutoff = SubElement(top, "cutoff_energy", {'type': "int"}) + cutoff = SubElement(top, "cutoff_energy", {"type": "int"}) cutoff.text = str(self.converged_cutoff) kpoints = SubElement(top, "kpoints") - kx = SubElement(kpoints, "kx", {'type': "int"}) + kx = SubElement(kpoints, "kx", {"type": "int"}) kx.text = str(self.converged_kgrid[0]) - ky = SubElement(kpoints, "ky", {'type': "int"}) + ky = SubElement(kpoints, "ky", {"type": "int"}) ky.text = str(self.converged_kgrid[1]) - kz = SubElement(kpoints, "kz", {'type': "int"}) + kz = SubElement(kpoints, "kz", {"type": "int"}) kz.text = str(self.converged_kgrid[2]) - rough_string = tostring(top, 'utf-8') + rough_string = tostring(top, "utf-8") reparsed = minidom.parseString(rough_string) with open(self.convergence_results_file, "w") as f: f.write(reparsed.toprettyxml(indent=" ")) - def __check_input_correctness(self, atoms): # Check if what we read made sense (assuming there was specified # user input.) - if self.parameters.element is not None \ - and self.parameters.number_of_atoms is not None: + if ( + self.parameters.element is not None + and self.parameters.number_of_atoms is not None + ): if not np.all( - atoms.get_atomic_numbers() == - atoms.get_atomic_numbers()): + atoms.get_atomic_numbers() == atoms.get_atomic_numbers() + ): raise Exception( - "Mismatch between user input and provided file.") - if atoms.get_chemical_symbols()[0] != \ - self.parameters.element: + "Mismatch between user input and provided file." + ) + if atoms.get_chemical_symbols()[0] != self.parameters.element: raise Exception( - "Mismatch between user input and provided file.") + "Mismatch between user input and provided file." + ) if len(atoms) != self.parameters.number_of_atoms: raise Exception( - "Mismatch between user input and provided file.") + "Mismatch between user input and provided file." + ) - def __create_dft_convergence_inputs(self, parameters_to_converge, posfile, - working_directory, try_number): + def __create_dft_convergence_inputs( + self, parameters_to_converge, posfile, working_directory, try_number + ): atoms_Angstrom = ase.io.read(posfile, format="vasp") self.__check_input_correctness(atoms_Angstrom) # This is needed for QE. - scaling = int(np.round( - atoms_Angstrom.cell.cellpar()[0] / atoms_Angstrom.cell.cellpar()[ - 1])) + scaling = int( + np.round( + atoms_Angstrom.cell.cellpar()[0] + / atoms_Angstrom.cell.cellpar()[1] + ) + ) # Determine parameters to converge. converge_list = [] if parameters_to_converge == "kpoints": if try_number == 0: for grids in kpoints_guesses[self.parameters.element]: - converge_list.append(self.__k_edge_length_to_grid(grids, atoms_Angstrom)) + converge_list.append( + self.__k_edge_length_to_grid(grids, atoms_Angstrom) + ) else: - spacing = kpoints_guesses[self.parameters.element][-1]-\ - kpoints_guesses[self.parameters.element][-2] - converge_list.append(self.__k_edge_length_to_grid(kpoints_guesses[self.parameters.element][-1]+spacing*(try_number-1)*2+1*spacing, atoms_Angstrom)) - converge_list.append(self.__k_edge_length_to_grid(kpoints_guesses[self.parameters.element][-1]+spacing*(try_number-1)*2+2*spacing, atoms_Angstrom)) + spacing = ( + kpoints_guesses[self.parameters.element][-1] + - kpoints_guesses[self.parameters.element][-2] + ) + converge_list.append( + self.__k_edge_length_to_grid( + kpoints_guesses[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 1 * spacing, + atoms_Angstrom, + ) + ) + converge_list.append( + self.__k_edge_length_to_grid( + kpoints_guesses[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 2 * spacing, + atoms_Angstrom, + ) + ) cutoff = self.converged_cutoff else: if self.parameters.dft_calculator == "qe": if try_number == 0: converge_list = cutoff_guesses_qe[self.parameters.element] else: - spacing = cutoff_guesses_qe[self.parameters.element][-1] - \ - cutoff_guesses_qe[self.parameters.element][-2] + spacing = ( + cutoff_guesses_qe[self.parameters.element][-1] + - cutoff_guesses_qe[self.parameters.element][-2] + ) converge_list.append( - cutoff_guesses_qe[self.parameters.element][ - -1] + spacing * (try_number - 1) * 2 + 1*spacing) + cutoff_guesses_qe[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 1 * spacing + ) converge_list.append( - cutoff_guesses_qe[self.parameters.element][ - -1] + spacing * (try_number - 1) * 2 + 2*spacing) - + cutoff_guesses_qe[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 2 * spacing + ) elif self.parameters.dft_calculator == "vasp": if try_number == 0: - converge_list = cutoff_guesses_vasp[self.parameters.element] + converge_list = cutoff_guesses_vasp[ + self.parameters.element + ] else: - spacing = cutoff_guesses_vasp[self.parameters.element][-1] - \ - cutoff_guesses_vasp[self.parameters.element][-2] + spacing = ( + cutoff_guesses_vasp[self.parameters.element][-1] + - cutoff_guesses_vasp[self.parameters.element][-2] + ) converge_list.append( - cutoff_guesses_vasp[self.parameters.element][ - -1] + spacing * (try_number - 1) * 2 + 1*spacing) + cutoff_guesses_vasp[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 1 * spacing + ) converge_list.append( - cutoff_guesses_vasp[self.parameters.element][ - -1] + spacing * (try_number - 1) * 2 + 2*spacing) + cutoff_guesses_vasp[self.parameters.element][-1] + + spacing * (try_number - 1) * 2 + + 2 * spacing + ) kpoints = (1, 1, 1) # Create files for submission. @@ -311,12 +410,16 @@ def __create_dft_convergence_inputs(self, parameters_to_converge, posfile, kpoints = entry else: cutoff = entry - this_folder = self.__make_convergence_folder(kpoints, cutoff, - working_directory) + this_folder = self.__make_convergence_folder( + kpoints, cutoff, working_directory + ) converge_folder_list.append(this_folder) if self.parameters.dft_calculator == "qe": - qe_pseudopotentials = {self.parameters.element : - self.parameters.pseudopotential["name"]} + qe_pseudopotentials = { + self.parameters.element: self.parameters.pseudopotential[ + "name" + ] + } # TODO: Find a better way to calculate this # With increasing temperature, this breaks of fairly quickly. @@ -326,16 +429,17 @@ def __create_dft_convergence_inputs(self, parameters_to_converge, posfile, # TODO: Find some metric for the mixing! mixing = 0.1 qe_input_data = { - "occupations": 'smearing', - "calculation": 'scf', - "restart_mode": 'from_scratch', + "occupations": "smearing", + "calculation": "scf", + "restart_mode": "from_scratch", "prefix": self.parameters.element, "pseudo_dir": self.parameters.pseudopotential["path"], "outdir": "temp", "ibrav": 0, - "smearing": 'fermi-dirac', - "degauss": round(kelvin_to_rydberg( - self.parameters.temperature), 7), + "smearing": "fermi-dirac", + "degauss": round( + kelvin_to_rydberg(self.parameters.temperature), 7 + ), "ecutrho": cutoff * 4, "ecutwfc": cutoff, "tstress": True, @@ -343,21 +447,21 @@ def __create_dft_convergence_inputs(self, parameters_to_converge, posfile, "nbnd": nbands, "mixing_mode": "plain", "mixing_beta": mixing, - "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry * self.parameters.number_of_atoms, + "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry + * self.parameters.number_of_atoms, "nosym": True, "noinv": True, - "verbosity": "high" + "verbosity": "high", } vasp_input_data = { "ISTART": 0, "ENCUT": str(cutoff) + " eV", - "EDIFF": self.parameters.dft_scf_accuracy_per_atom_Ry* - self.parameters.number_of_atoms * - ase.units.Rydberg, + "EDIFF": self.parameters.dft_scf_accuracy_per_atom_Ry + * self.parameters.number_of_atoms + * ase.units.Rydberg, "ISMEAR": -1, - "SIGMA": round(kelvin_to_eV( - self.parameters.temperature), 7), + "SIGMA": round(kelvin_to_eV(self.parameters.temperature), 7), "ISYM": 0, "NBANDS": nbands, "LREAL": "A", @@ -365,65 +469,99 @@ def __create_dft_convergence_inputs(self, parameters_to_converge, posfile, # Write the convergence files. if self.parameters.dft_calculator == "qe": - ase.io.write(this_folder + self.parameters.element - + ".pw.scf.in", - atoms_Angstrom, - "espresso-in", input_data=qe_input_data, - pseudopotentials= \ - qe_pseudopotentials, kpts=kpoints) + ase.io.write( + this_folder + self.parameters.element + ".pw.scf.in", + atoms_Angstrom, + "espresso-in", + input_data=qe_input_data, + pseudopotentials=qe_pseudopotentials, + kpts=kpoints, + ) elif self.parameters.dft_calculator == "vasp": - ase.io.write(this_folder + "POSCAR", atoms_Angstrom, - "vasp") + ase.io.write(this_folder + "POSCAR", atoms_Angstrom, "vasp") VaspUtils.write_to_incar(this_folder, vasp_input_data) VaspUtils.write_to_kpoints(this_folder, kpoints) - VaspUtils.write_to_potcar_copy(this_folder, - os.path.join(self.parameters.pseudopotential["path"], - self.parameters.pseudopotential["name"])) - + VaspUtils.write_to_potcar_copy( + this_folder, + os.path.join( + self.parameters.pseudopotential["path"], + self.parameters.pseudopotential["name"], + ), + ) return converge_folder_list def __make_convergence_folder(self, kgrid, cutoff, base_folder): if self.parameters.dft_calculator == "qe": - folder_name = str(cutoff) + "Ry_k" + str(kgrid[0]) + str( - kgrid[1]) + str(kgrid[2]) + "/" + folder_name = ( + str(cutoff) + + "Ry_k" + + str(kgrid[0]) + + str(kgrid[1]) + + str(kgrid[2]) + + "/" + ) elif self.parameters.dft_calculator == "vasp": - folder_name = str(cutoff) + "eV_k" + str(kgrid[0]) + str( - kgrid[1]) + str(kgrid[2]) + "/" + folder_name = ( + str(cutoff) + + "eV_k" + + str(kgrid[0]) + + str(kgrid[1]) + + str(kgrid[2]) + + "/" + ) this_folder = os.path.join(base_folder, folder_name) if not os.path.exists(this_folder): os.makedirs(this_folder) return this_folder - def __check_run_success(self, base_folder, parameter_to_converge, - fixed_cutoff=None, fixed_kpoints=None): + def __check_run_success( + self, + base_folder, + parameter_to_converge, + fixed_cutoff=None, + fixed_kpoints=None, + ): # First, parse the results out of the output files. if parameter_to_converge == "cutoff": if self.parameters.dft_calculator == "qe": - converge_list = glob.glob(os.path.join(base_folder, - "*Ry_k"+str(fixed_kpoints[0]) + - str(fixed_kpoints[1]) + - str(fixed_kpoints[2]))) + converge_list = glob.glob( + os.path.join( + base_folder, + "*Ry_k" + + str(fixed_kpoints[0]) + + str(fixed_kpoints[1]) + + str(fixed_kpoints[2]), + ) + ) elif self.parameters.dft_calculator == "vasp": - converge_list = glob.glob(os.path.join(base_folder, - "*eV_k"+str(fixed_kpoints[0]) + - str(fixed_kpoints[1]) + - str(fixed_kpoints[2]))) + converge_list = glob.glob( + os.path.join( + base_folder, + "*eV_k" + + str(fixed_kpoints[0]) + + str(fixed_kpoints[1]) + + str(fixed_kpoints[2]), + ) + ) elif parameter_to_converge == "kpoints": if self.parameters.dft_calculator == "qe": - converge_list = glob.glob(os.path.join(base_folder, - str(fixed_cutoff)+"Ry_k*")) + converge_list = glob.glob( + os.path.join(base_folder, str(fixed_cutoff) + "Ry_k*") + ) elif self.parameters.dft_calculator == "vasp": - converge_list = glob.glob(os.path.join(base_folder, - str(fixed_cutoff)+"eV_k*")) + converge_list = glob.glob( + os.path.join(base_folder, str(fixed_cutoff) + "eV_k*") + ) else: raise Exception("Unknown convergence parameter.") for entry in converge_list: if self.parameters.dft_calculator == "qe": - convergence_file_lists = glob.glob(os.path.join(entry, - "*.out")) + convergence_file_lists = glob.glob( + os.path.join(entry, "*.out") + ) if len(convergence_file_lists) != 1: return False elif self.parameters.dft_calculator == "vasp": @@ -433,31 +571,48 @@ def __check_run_success(self, base_folder, parameter_to_converge, raise Exception("Unknown calculator chosen.") return True - def __analyze_convergence_runs(self, base_folder, parameter_to_converge, - supercell_file, - fixed_cutoff=None, fixed_kpoints=None): + def __analyze_convergence_runs( + self, + base_folder, + parameter_to_converge, + supercell_file, + fixed_cutoff=None, + fixed_kpoints=None, + ): atoms_Angstrom = ase.io.read(supercell_file, format="vasp") # First, parse the results out of the output files. result_list = [] if parameter_to_converge == "cutoff": if self.parameters.dft_calculator == "qe": - converge_list = glob.glob(os.path.join(base_folder, - "*Ry_k"+str(fixed_kpoints[0]) + - str(fixed_kpoints[1]) + - str(fixed_kpoints[2]))) + converge_list = glob.glob( + os.path.join( + base_folder, + "*Ry_k" + + str(fixed_kpoints[0]) + + str(fixed_kpoints[1]) + + str(fixed_kpoints[2]), + ) + ) elif self.parameters.dft_calculator == "vasp": - converge_list = glob.glob(os.path.join(base_folder, - "*eV_k"+str(fixed_kpoints[0]) + - str(fixed_kpoints[1]) + - str(fixed_kpoints[2]))) + converge_list = glob.glob( + os.path.join( + base_folder, + "*eV_k" + + str(fixed_kpoints[0]) + + str(fixed_kpoints[1]) + + str(fixed_kpoints[2]), + ) + ) elif parameter_to_converge == "kpoints": if self.parameters.dft_calculator == "qe": - converge_list = glob.glob(os.path.join(base_folder, - str(fixed_cutoff)+"Ry_k*")) + converge_list = glob.glob( + os.path.join(base_folder, str(fixed_cutoff) + "Ry_k*") + ) elif self.parameters.dft_calculator == "vasp": - converge_list = glob.glob(os.path.join(base_folder, - str(fixed_cutoff)+"eV_k*")) + converge_list = glob.glob( + os.path.join(base_folder, str(fixed_cutoff) + "eV_k*") + ) else: raise Exception("Unknown convergence parameter.") @@ -475,56 +630,74 @@ def __analyze_convergence_runs(self, base_folder, parameter_to_converge, k_argument = os.path.basename(entry).split("k")[1] if len(k_argument) == 3: argument = ( - int((os.path.basename(entry).split("k")[1])[0]), - int((os.path.basename(entry).split("k")[1])[1]), - int((os.path.basename(entry).split("k")[1])[2])) + int((os.path.basename(entry).split("k")[1])[0]), + int((os.path.basename(entry).split("k")[1])[1]), + int((os.path.basename(entry).split("k")[1])[2]), + ) else: # All three dimensions are double digits. if len(k_argument) == 6: argument = ( int((os.path.basename(entry).split("k")[1])[0:2]), int((os.path.basename(entry).split("k")[1])[2:4]), - int((os.path.basename(entry).split("k")[1])[4:6])) + int((os.path.basename(entry).split("k")[1])[4:6]), + ) else: # One of the k-dimensions is a double digit. # We will attempt to recover by making an educated # guess which one it is. - scaling_x = int(np.ceil( - atoms_Angstrom.cell.cellpar()[0] / - atoms_Angstrom.cell.cellpar()[ - 2])) - scaling_y = int(np.ceil( - atoms_Angstrom.cell.cellpar()[1] / - atoms_Angstrom.cell.cellpar()[ - 2])) + scaling_x = int( + np.ceil( + atoms_Angstrom.cell.cellpar()[0] + / atoms_Angstrom.cell.cellpar()[2] + ) + ) + scaling_y = int( + np.ceil( + atoms_Angstrom.cell.cellpar()[1] + / atoms_Angstrom.cell.cellpar()[2] + ) + ) argument_x = int( - (os.path.basename(entry).split("k")[1])[0]) + (os.path.basename(entry).split("k")[1])[0] + ) argument_y = int( - (os.path.basename(entry).split("k")[1])[1]) + (os.path.basename(entry).split("k")[1])[1] + ) argument_z = int( - (os.path.basename(entry).split("k")[1])[2]) + (os.path.basename(entry).split("k")[1])[2] + ) if scaling_x != 1: argument_x = int( - (os.path.basename(entry).split("k")[1])[0:2]) + (os.path.basename(entry).split("k")[1])[0:2] + ) argument_y = int( - (os.path.basename(entry).split("k")[1])[2:3]) + (os.path.basename(entry).split("k")[1])[2:3] + ) if scaling_y != 1: if scaling_x == 1: argument_y = int( - (os.path.basename(entry).split("k")[1])[1:3]) + (os.path.basename(entry).split("k")[1])[ + 1:3 + ] + ) else: argument_y = int( - (os.path.basename(entry).split("k")[1])[2:4]) + (os.path.basename(entry).split("k")[1])[ + 2:4 + ] + ) argument = (argument_x, argument_y, argument_z) else: raise Exception("Unknown parameter chosen.") if self.parameters.dft_calculator == "qe": - convergence_file_lists = glob.glob(os.path.join(entry, - "*.out")) + convergence_file_lists = glob.glob( + os.path.join(entry, "*.out") + ) if len(convergence_file_lists) > 1: print(convergence_file_lists) raise Exception("Run folder with ambigous content.") @@ -532,8 +705,7 @@ def __analyze_convergence_runs(self, base_folder, parameter_to_converge, energy = self.__get_qe_energy(convergence_file_lists[0]) elif self.parameters.dft_calculator == "vasp": - energy = self.__get_vasp_energy(os.path.join(entry, - "OUTCAR")) + energy = self.__get_vasp_energy(os.path.join(entry, "OUTCAR")) else: raise Exception("Unknown calculator chosen.") @@ -545,10 +717,15 @@ def __analyze_convergence_runs(self, base_folder, parameter_to_converge, best_param = None print("Convergence results for: ", parameter_to_converge) for i in range(1, len(result_list)): - print(result_list[i-1][0], result_list[i][0], - np.abs(result_list[i][1] - result_list[i-1][1])) - if np.abs(result_list[i][1] - result_list[i-1][1]) < self.\ - parameters.dft_conv_accuracy_meVperatom: + print( + result_list[i - 1][0], + result_list[i][0], + np.abs(result_list[i][1] - result_list[i - 1][1]), + ) + if ( + np.abs(result_list[i][1] - result_list[i - 1][1]) + < self.parameters.dft_conv_accuracy_meVperatom + ): if best_param is not None: break else: @@ -563,16 +740,25 @@ def __get_qe_energy(self, file): with open(file, "r") as f: ll = f.readlines() for l in ll: - if "total energy" in l and "is F=E-TS" not in l and \ - "is the sum of" not in l: - energy = float((l.split('=')[1]).split('Ry')[0]) - if "convergence NOT achieved" in l or "oom-kill" in l or\ - "CANCELLED" in l or "BAD TERMINATION" in l: + if ( + "total energy" in l + and "is F=E-TS" not in l + and "is the sum of" not in l + ): + energy = float((l.split("=")[1]).split("Ry")[0]) + if ( + "convergence NOT achieved" in l + or "oom-kill" in l + or "CANCELLED" in l + or "BAD TERMINATION" in l + ): convergence_achieved = False if convergence_achieved is False: raise Exception("Convergence was not achieved at", file) - return float((energy * Rydberg * 1000)/self.parameters.number_of_atoms) + return float( + (energy * Rydberg * 1000) / self.parameters.number_of_atoms + ) def __get_vasp_energy(self, file): energy = np.nan @@ -584,18 +770,24 @@ def __get_vasp_energy(self, file): if "FREE ENERGIE" in l: found_end = True if "TOTEN" in l and found_end: - energy = float((l.split('=')[1]).split('eV')[0]) + energy = float((l.split("=")[1]).split("eV")[0]) if convergence_achieved is False: raise Exception("Convergence was not achieved at", file) - return float((energy * 1000)/self.parameters.number_of_atoms) + return float((energy * 1000) / self.parameters.number_of_atoms) def __k_edge_length_to_grid(self, edge_length, atoms_Angstrom): # TODO: Reflect geometry here. - scaling_x = int(np.ceil( - atoms_Angstrom.cell.cellpar()[0] / atoms_Angstrom.cell.cellpar()[ - 2])) - scaling_y= int(np.ceil( - atoms_Angstrom.cell.cellpar()[1] / atoms_Angstrom.cell.cellpar()[ - 2])) - return (edge_length*scaling_x, edge_length*scaling_y, edge_length) + scaling_x = int( + np.ceil( + atoms_Angstrom.cell.cellpar()[0] + / atoms_Angstrom.cell.cellpar()[2] + ) + ) + scaling_y = int( + np.ceil( + atoms_Angstrom.cell.cellpar()[1] + / atoms_Angstrom.cell.cellpar()[2] + ) + ) + return (edge_length * scaling_x, edge_length * scaling_y, edge_length) diff --git a/malada/providers/ldosconvergence.py b/malada/providers/ldosconvergence.py index 344eac4..e13648a 100644 --- a/malada/providers/ldosconvergence.py +++ b/malada/providers/ldosconvergence.py @@ -1,4 +1,5 @@ """Provider for optimal LDOS calculation parameters.""" + import os from shutil import copyfile from .provider import Provider @@ -46,19 +47,26 @@ def provide(self, provider_path, snapshot_file, dft_convergence_file): Path to a file containing an ASE trajectory containing atomic snapshots for DFT/LDOS calculation. """ - file_name = self.parameters.element + \ - str(self.parameters.number_of_atoms) + \ - "_" + self.parameters.crystal_structure +\ - "_" + str(self.parameters.temperature) +\ - "_" + self.parameters.dft_calculator+".ldosconf.xml" + file_name = ( + self.parameters.element + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + "_" + + str(self.parameters.temperature) + + "_" + + self.parameters.dft_calculator + + ".ldosconf.xml" + ) self.ldos_configuration_file = os.path.join(provider_path, file_name) # Check if there exist results or if we have to work from scratch. if self.external_ldos_configuration is None: - raise Exception("Currently there is no way to determine LDOS" - "configuration automatically.") + raise Exception( + "Currently there is no way to determine LDOS" + "configuration automatically." + ) else: - copyfile(self.external_ldos_configuration, - self.ldos_configuration_file) + copyfile( + self.external_ldos_configuration, self.ldos_configuration_file + ) print("Getting <>.xml file from disc.") - - diff --git a/malada/providers/md.py b/malada/providers/md.py index 6ca478c..8f15483 100644 --- a/malada/providers/md.py +++ b/malada/providers/md.py @@ -42,8 +42,13 @@ class MDProvider(Provider): file. """ - def __init__(self, parameters, external_trajectory=None, - external_temperatures=None, external_run_folder=None): + def __init__( + self, + parameters, + external_trajectory=None, + external_temperatures=None, + external_run_folder=None, + ): super(MDProvider, self).__init__(parameters) self.external_trajectory = external_trajectory self.external_temperatures = external_temperatures @@ -51,8 +56,13 @@ def __init__(self, parameters, external_trajectory=None, self.trajectory_file = None self.temperature_file = None - def provide(self, provider_path, supercell_file, dft_convergence_file, - md_performance_file): + def provide( + self, + provider_path, + supercell_file, + dft_convergence_file, + md_performance_file, + ): """ Provide a MD trajectory and temperatures from previous steps. @@ -75,30 +85,39 @@ def provide(self, provider_path, supercell_file, dft_convergence_file, MD. """ # Creating file for output. - file_name = self.parameters.element + \ - str(self.parameters.number_of_atoms) + \ - "_" + self.parameters.crystal_structure +\ - "_" + str(self.parameters.temperature) - self.trajectory_file = os.path.join(provider_path, file_name+".traj") - self.temperature_file = os.path.join(provider_path, file_name + - ".temp.npy") + file_name = ( + self.parameters.element + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + "_" + + str(self.parameters.temperature) + ) + self.trajectory_file = os.path.join(provider_path, file_name + ".traj") + self.temperature_file = os.path.join( + provider_path, file_name + ".temp.npy" + ) # MD performance has to be loaded if SLURM is used. # TODO: Fix this, the MD parameters should also contain the thermostat # controller if self.parameters.run_system == "slurm_creator": - self.parameters.md_slurm = malada.SlurmParameters.\ - from_xml(md_performance_file) - if self.external_trajectory is None or self.external_temperatures is None: + self.parameters.md_slurm = malada.SlurmParameters.from_xml( + md_performance_file + ) + if ( + self.external_trajectory is None + or self.external_temperatures is None + ): if self.external_run_folder is None: # Here, we have to perform the MD first. # First, create MD inputs. - self.__create_md_run(dft_convergence_file, - supercell_file, provider_path) + self.__create_md_run( + dft_convergence_file, supercell_file, provider_path + ) # Run the MD calculation. - mdrunner = malada.RunnerInterface( - self.parameters) + mdrunner = malada.RunnerInterface(self.parameters) mdrunner.run_folder(provider_path, "md") folder_to_parse = provider_path @@ -113,40 +132,51 @@ def provide(self, provider_path, supercell_file, dft_convergence_file, # Now we parse the results. if self.parameters.md_calculator == "qe": - self._qe_out_to_trajectory(folder_to_parse, self.trajectory_file) - self._qe_out_to_temperature(folder_to_parse, self.temperature_file) + self._qe_out_to_trajectory( + folder_to_parse, self.trajectory_file + ) + self._qe_out_to_temperature( + folder_to_parse, self.temperature_file + ) else: - self._vasp_out_to_trajectory(folder_to_parse, self.trajectory_file) - self._vasp_out_to_temperature(folder_to_parse, self.temperature_file) + self._vasp_out_to_trajectory( + folder_to_parse, self.trajectory_file + ) + self._vasp_out_to_temperature( + folder_to_parse, self.temperature_file + ) else: copyfile(self.external_trajectory, self.trajectory_file) copyfile(self.external_temperatures, self.temperature_file) - print("Getting <>.traj and <>.temp.npy" - " files from disc.") + print( + "Getting <>.traj and <>.temp.npy" + " files from disc." + ) - def __create_md_run(self, dft_convergence_file, - posfile, base_path): + def __create_md_run(self, dft_convergence_file, posfile, base_path): cutoff, kgrid = self._read_convergence(dft_convergence_file) if self.parameters.md_at_gamma_point: kgrid = (1, 1, 1) if self.parameters.dft_calculator == "qe": kgrid = None - qe_pseudopotentials = {self.parameters.element: - self.parameters.pseudopotential["name"]} + qe_pseudopotentials = { + self.parameters.element: self.parameters.pseudopotential["name"] + } nbands = self._get_number_of_bands() qe_input_data = { - "occupations": 'smearing', - "calculation": 'md', - "restart_mode": 'from_scratch', + "occupations": "smearing", + "calculation": "md", + "restart_mode": "from_scratch", "prefix": self.parameters.element, "pseudo_dir": self.parameters.pseudopotential["path"], "outdir": "temp", "ibrav": 0, - "smearing": 'fermi-dirac', - "degauss": round(kelvin_to_rydberg( - self.parameters.temperature), 7), + "smearing": "fermi-dirac", + "degauss": round( + kelvin_to_rydberg(self.parameters.temperature), 7 + ), "ecutrho": cutoff * 4, "ecutwfc": cutoff, "tstress": True, @@ -154,8 +184,8 @@ def __create_md_run(self, dft_convergence_file, "nbnd": nbands, "mixing_mode": "plain", "mixing_beta": 0.1, - "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry * - self.parameters.number_of_atoms, + "conv_thr": self.parameters.dft_scf_accuracy_per_atom_Ry + * self.parameters.number_of_atoms, # "verbosity" : "high", # This is maybe a bit high "nosym": True, # MD variables - these are not final in any way. @@ -164,11 +194,11 @@ def __create_md_run(self, dft_convergence_file, # https://www2.mpip-mainz.mpg.de/~andrienk/journal_club/thermostats.pdf # it is apparent that we would need Nose-Hover, but the next # best thing QE gives us is berendsen. - "ion_temperature": 'berendsen', + "ion_temperature": "berendsen", "tempw": self.parameters.temperature, # Time step: In Ryberg atomic units. # For now: 1 fs - "dt": second_to_rydberg_time(1e-15*self.parameters.time_step_fs), + "dt": second_to_rydberg_time(1e-15 * self.parameters.time_step_fs), # Number of steps. # For now: 500. "nstep": self.parameters.maximum_number_of_timesteps, @@ -176,7 +206,7 @@ def __create_md_run(self, dft_convergence_file, # I think we want verlet, which is also the default. "ion_dynamics": "verlet", # I think this helps with performance. - "wfc_extrapolation": 'second order', + "wfc_extrapolation": "second order", # This controls the velocity rescaling performed by the Berendsen # thermostat. It corresponds to tau/dt in e.g. eq. 1.12 # of https://link.springer.com/content/pdf/10.1007%2F978-3-540-74686-7_1.pdf @@ -186,11 +216,11 @@ def __create_md_run(self, dft_convergence_file, vasp_input_data = { "ISTART": 0, "ENCUT": str(cutoff) + " eV", - "EDIFF": 1e-6 * self.parameters.number_of_atoms * - ase.units.Rydberg, + "EDIFF": 1e-6 + * self.parameters.number_of_atoms + * ase.units.Rydberg, "ISMEAR": -1, - "SIGMA": round(kelvin_to_eV( - self.parameters.temperature), 7), + "SIGMA": round(kelvin_to_eV(self.parameters.temperature), 7), "ISYM": 0, "NBANDS": nbands, "LREAL": "A", @@ -218,41 +248,42 @@ def __create_md_run(self, dft_convergence_file, # This enables the Nose Hoover thermostat, it is controlled by this # parameter "SMASS": self.parameters.md_thermostat_controller, - # Time step in fs, we want 1 fs. "POTIM": self.parameters.time_step_fs, - # TEBEG: Initial temperature. "TEBEG": self.parameters.temperature, - # This surpresses large disk operations at the end of the run "LWAVE": ".FALSE.", "LCHARG": ".FALSE.", "LPLANE": ".FALSE.", - } if self.parameters.dft_calculator == "qe": atoms_Angstrom = ase.io.read(posfile, format="vasp") - ase.io.write(os.path.join(base_path, self.parameters.element - + ".pw.md.in"), - atoms_Angstrom, - "espresso-in", input_data=qe_input_data, - pseudopotentials= \ - qe_pseudopotentials, kpts=kgrid) + ase.io.write( + os.path.join(base_path, self.parameters.element + ".pw.md.in"), + atoms_Angstrom, + "espresso-in", + input_data=qe_input_data, + pseudopotentials=qe_pseudopotentials, + kpts=kgrid, + ) elif self.parameters.dft_calculator == "vasp": atoms_Angstrom = ase.io.read(posfile, format="vasp") - ase.io.write(os.path.join(base_path, "POSCAR"), atoms_Angstrom, - "vasp") - ase.io.write(os.path.join(base_path, "POSCAR_original"), atoms_Angstrom, - "vasp") + ase.io.write( + os.path.join(base_path, "POSCAR"), atoms_Angstrom, "vasp" + ) + ase.io.write( + os.path.join(base_path, "POSCAR_original"), + atoms_Angstrom, + "vasp", + ) VaspUtils.write_to_incar(base_path, vasp_input_data) VaspUtils.write_to_kpoints(base_path, kgrid) - VaspUtils.write_to_potcar_copy(base_path, - os.path.join( - self.parameters.pseudopotential[ - "path"], - self.parameters.pseudopotential[ - "name"])) - - + VaspUtils.write_to_potcar_copy( + base_path, + os.path.join( + self.parameters.pseudopotential["path"], + self.parameters.pseudopotential["name"], + ), + ) diff --git a/malada/providers/mdperformance.py b/malada/providers/mdperformance.py index 5e3dd04..c901990 100644 --- a/malada/providers/mdperformance.py +++ b/malada/providers/mdperformance.py @@ -1,4 +1,5 @@ """Provider for optimal MD performance parameters.""" + import os from shutil import copyfile from xml.etree.ElementTree import Element, SubElement, tostring @@ -39,27 +40,37 @@ def provide(self, provider_path, dft_convergence_file): dft_convergence_file : string Path to xml file containing the DFT convergence parameter. """ - file_name = self.parameters.element + \ - str(self.parameters.number_of_atoms) + \ - "_" + self.parameters.crystal_structure +\ - "_" + str(self.parameters.temperature) +\ - "_" + self.parameters.dft_calculator+".mdperformance.xml" + file_name = ( + self.parameters.element + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + "_" + + str(self.parameters.temperature) + + "_" + + self.parameters.dft_calculator + + ".mdperformance.xml" + ) self.md_performance_xml = os.path.join(provider_path, file_name) if self.external_performance_file is None: if self.parameters.run_system == "bash": # In this case, we can simply write an empty xml file - print("Using bash based run system, no MD performance " - "optimization possible or necessary.") - top = Element('mdperformanceparameters') - dummy = SubElement(top, "nraise", {'type': "float"}) + print( + "Using bash based run system, no MD performance " + "optimization possible or necessary." + ) + top = Element("mdperformanceparameters") + dummy = SubElement(top, "nraise", {"type": "float"}) dummy.text = "0.001" - rough_string = tostring(top, 'utf-8') + rough_string = tostring(top, "utf-8") reparsed = minidom.parseString(rough_string) with open(self.md_performance_xml, "w") as f: f.write(reparsed.toprettyxml(indent=" ")) else: - raise Exception("Currently there is no way to evaluate MD " - "performance on the fly.") + raise Exception( + "Currently there is no way to evaluate MD " + "performance on the fly." + ) else: copyfile(self.external_performance_file, self.md_performance_xml) print("Getting <>.xml file from disc.") diff --git a/malada/providers/provider.py b/malada/providers/provider.py index 8f221c2..736d335 100644 --- a/malada/providers/provider.py +++ b/malada/providers/provider.py @@ -1,4 +1,5 @@ """Base class for all pipeline providers.""" + from abc import ABC, abstractmethod import xml.etree.ElementTree as ET import glob @@ -35,21 +36,34 @@ def _read_convergence(self, filename, ignore_atom_number=False): # Parse the XML file and first check for consistency. filecontents = ET.parse(filename).getroot() dftparams = filecontents.find("calculationparameters") - number_of_atoms_check = (int(dftparams.find("number_of_atoms").text) - != self.parameters.number_of_atoms) if ( - ignore_atom_number is False) else False - - if dftparams.find("element").text != self.parameters.element or \ - dftparams.find("crystal_structure").text != self.parameters.crystal_structure or \ - dftparams.find("dft_calculator").text != self.parameters.dft_calculator or \ - float(dftparams.find("temperature").text) != self.parameters.temperature or \ - number_of_atoms_check: + number_of_atoms_check = ( + ( + int(dftparams.find("number_of_atoms").text) + != self.parameters.number_of_atoms + ) + if (ignore_atom_number is False) + else False + ) + + if ( + dftparams.find("element").text != self.parameters.element + or dftparams.find("crystal_structure").text + != self.parameters.crystal_structure + or dftparams.find("dft_calculator").text + != self.parameters.dft_calculator + or float(dftparams.find("temperature").text) + != self.parameters.temperature + or number_of_atoms_check + ): raise Exception("Incompatible convergence parameters provided.") cutoff_energy = int(filecontents.find("cutoff_energy").text) kpoints = filecontents.find("kpoints") - kgrid = (int(kpoints.find("kx").text),int(kpoints.find("kx").text), - int(kpoints.find("kx").text)) + kgrid = ( + int(kpoints.find("kx").text), + int(kpoints.find("kx").text), + int(kpoints.find("kx").text), + ) return cutoff_energy, kgrid @@ -60,16 +74,22 @@ def _qe_out_to_trajectory(self, out_folder, file_name): # I know this breaks down if one of the out files is for any reason incorrect. i_actual = 0 for posfile in ordered_file_list: - current_atoms = ase.io.read(posfile,index = ':', format="espresso-out") + current_atoms = ase.io.read( + posfile, index=":", format="espresso-out" + ) for i in range(0, len(current_atoms)): if i_actual < self.parameters.maximum_number_of_timesteps: atoms_to_write = self.enforce_pbc(current_atoms[i]) if i_actual == 0: - traj_writer = ase.io.trajectory.TrajectoryWriter(file_name, mode='w') + traj_writer = ase.io.trajectory.TrajectoryWriter( + file_name, mode="w" + ) traj_writer.write(atoms=atoms_to_write) i_actual += 1 else: - traj_writer = ase.io.trajectory.TrajectoryWriter(file_name, mode='a') + traj_writer = ase.io.trajectory.TrajectoryWriter( + file_name, mode="a" + ) if i > 0: traj_writer.write(atoms=atoms_to_write) i_actual += 1 @@ -87,7 +107,11 @@ def _qe_out_to_temperature(self, out_folder, file_name): posfile = open(file_to_open) for line in posfile.readlines(): if i < self.parameters.maximum_number_of_timesteps: - if "temperature" in line and "=" in line and "Starting" not in line: + if ( + "temperature" in line + and "=" in line + and "Starting" not in line + ): temp = float((line.split("=")[1]).split("K")[0]) temps.append(temp) i += 1 @@ -112,7 +136,7 @@ def _qe_out_to_timing(self, out_folder, file_name): time_step_found = True if time_step_found and "cpu time" in line: current_time = float(line.split()[-2]) - times.append(current_time-last_time) + times.append(current_time - last_time) last_time = current_time time_step_found = False i += 1 @@ -131,8 +155,7 @@ def _vasp_out_to_temperature(self, out_folder, file_name): for line in posfile.readlines(): if i < self.parameters.maximum_number_of_timesteps: if "tmprt" in line: - temp = float( - line.split()[2]) + temp = float(line.split()[2]) temps.append(temp) i += 1 else: @@ -164,18 +187,24 @@ def _vasp_out_to_trajectory(self, out_folder, file_name): # I know this breaks down if one of the out files is for any reason incorrect. i_actual = 0 for file_to_open in ordered_file_list: - current_atoms = ase.io.read(os.path.join(file_to_open, "OUTCAR") - , index=':', - format="vasp-out") + current_atoms = ase.io.read( + os.path.join(file_to_open, "OUTCAR"), + index=":", + format="vasp-out", + ) for i in range(0, len(current_atoms)): if i_actual < self.parameters.maximum_number_of_timesteps: atoms_to_write = self.enforce_pbc(current_atoms[i]) if i_actual == 0: - traj_writer = ase.io.trajectory.TrajectoryWriter(file_name, mode='w') + traj_writer = ase.io.trajectory.TrajectoryWriter( + file_name, mode="w" + ) traj_writer.write(atoms=atoms_to_write) i_actual += 1 else: - traj_writer = ase.io.trajectory.TrajectoryWriter(file_name, mode='a') + traj_writer = ase.io.trajectory.TrajectoryWriter( + file_name, mode="a" + ) if i > 0: traj_writer.write(atoms=atoms_to_write) i_actual += 1 @@ -183,9 +212,11 @@ def _vasp_out_to_trajectory(self, out_folder, file_name): break def _get_number_of_bands(self): - number_of_bands = int(self.parameters.number_of_atoms * - self.parameters.pseudopotential["valence_electrons"] - * (1.0 + self.parameters.number_of_bands_factor)) + number_of_bands = int( + self.parameters.number_of_atoms + * self.parameters.pseudopotential["valence_electrons"] + * (1.0 + self.parameters.number_of_bands_factor) + ) return number_of_bands @staticmethod @@ -218,7 +249,10 @@ def enforce_pbc(atoms): # metric here. rescaled_atoms = 0 for i in range(0, len(atoms)): - if False in (np.isclose(new_atoms[i].position, - atoms[i].position, atol=0.001)): + if False in ( + np.isclose( + new_atoms[i].position, atoms[i].position, atol=0.001 + ) + ): rescaled_atoms += 1 return new_atoms diff --git a/malada/providers/snapshots.py b/malada/providers/snapshots.py index 127fe91..e902949 100644 --- a/malada/providers/snapshots.py +++ b/malada/providers/snapshots.py @@ -1,4 +1,5 @@ """Provider for a set of snapshots from a MD trajectory.""" + from .provider import Provider import os from shutil import copyfile @@ -10,6 +11,7 @@ from asap3.analysis.rdf import RadialDistributionFunction import pickle + class SnapshotsProvider(Provider): """ Filters snapshots from a given MD trajectory, with a user specified metric. @@ -53,13 +55,19 @@ def provide(self, provider_path, trajectoryfile, temperaturefile): temperaturefile : string File containing the temperatures from the MD run as numpy array. """ - file_name = self.parameters.element + \ - str(self.parameters.number_of_atoms) + \ - "_" + self.parameters.crystal_structure +\ - "_" + str(self.parameters.temperature) +\ - "_possible_snapshots" - self.snapshot_file = os.path.join(provider_path, file_name+".traj") - iteration_numbers = os.path.join(provider_path, file_name+".md_iterations.npy") + file_name = ( + self.parameters.element + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + "_" + + str(self.parameters.temperature) + + "_possible_snapshots" + ) + self.snapshot_file = os.path.join(provider_path, file_name + ".traj") + iteration_numbers = os.path.join( + provider_path, file_name + ".md_iterations.npy" + ) md_trajectory = ase.io.trajectory.Trajectory(trajectoryfile) temperatures = np.load(temperaturefile) if self.external_snapshots is None: @@ -67,17 +75,20 @@ def provide(self, provider_path, trajectoryfile, temperaturefile): self.first_snapshot = self.__get_first_snapshot(md_trajectory) # Now determine the value for the distance metric. - self.distance_metric_cutoff = self.__determine_distance_metric(md_trajectory) + self.distance_metric_cutoff = self.__determine_distance_metric( + md_trajectory + ) # Now parse the entire trajectory. - self.__parse_trajectory(md_trajectory, - temperatures, - self.snapshot_file, - iteration_numbers) + self.__parse_trajectory( + md_trajectory, + temperatures, + self.snapshot_file, + iteration_numbers, + ) else: copyfile(self.external_snapshots, self.snapshot_file) - print("Getting <>.npy" - " files from disc.") + print("Getting <>.npy" " files from disc.") def __get_first_snapshot(self, trajectory): if self.parameters.snapshot_parsing_beginning < 0: @@ -94,94 +105,122 @@ def __determine_distance_metric(self, trajectory): else: return self.parameters.distance_metric_snapshots_cutoff - def __parse_trajectory(self, trajectory, temperatures, - filename_traj, - filename_numbers): - allowed_temp_diff_K = (self.parameters. - snapshot_parsing_temperature_tolerance_percent - / 100) * self.parameters.temperature + def __parse_trajectory( + self, trajectory, temperatures, filename_traj, filename_numbers + ): + allowed_temp_diff_K = ( + self.parameters.snapshot_parsing_temperature_tolerance_percent + / 100 + ) * self.parameters.temperature current_snapshot = self.first_snapshot - begin_snapshot = self.first_snapshot+1 + begin_snapshot = self.first_snapshot + 1 end_snapshot = len(trajectory) j = 0 md_iteration = [] for i in range(begin_snapshot, end_snapshot): - if self.__check_if_snapshot_is_valid(trajectory[i], - temperatures[i], - trajectory[current_snapshot], - temperatures[current_snapshot], - self.distance_metric_cutoff, - allowed_temp_diff_K): + if self.__check_if_snapshot_is_valid( + trajectory[i], + temperatures[i], + trajectory[current_snapshot], + temperatures[current_snapshot], + self.distance_metric_cutoff, + allowed_temp_diff_K, + ): current_snapshot = i md_iteration.append(current_snapshot) - j+=1 + j += 1 np.random.shuffle(md_iteration) for i in range(0, len(md_iteration)): if i == 0: - traj_writer = ase.io.trajectory.TrajectoryWriter(filename_traj, mode='w') + traj_writer = ase.io.trajectory.TrajectoryWriter( + filename_traj, mode="w" + ) else: - traj_writer = ase.io.trajectory.TrajectoryWriter(filename_traj, mode='a') + traj_writer = ase.io.trajectory.TrajectoryWriter( + filename_traj, mode="a" + ) atoms_to_write = self.enforce_pbc(trajectory[md_iteration[i]]) traj_writer.write(atoms=atoms_to_write) np.save(filename_numbers, md_iteration) print(j, "possible snapshots found in MD trajectory.") if j < self.parameters.number_of_snapshots: - raise Exception("Not enough snapshots found in MD trajectory. " - "Please run a longer MD calculation.") - - def __check_if_snapshot_is_valid(self, snapshot_to_test, temp_to_test, - reference_snapshot, reference_temp, - distance_metric, - allowed_temp_diff): - distance = self.\ - _calculate_distance_between_snapshots(snapshot_to_test, - reference_snapshot, - "realspace", - "minimal_distance") - temp_diff = np.abs(temp_to_test-reference_temp) + raise Exception( + "Not enough snapshots found in MD trajectory. " + "Please run a longer MD calculation." + ) + + def __check_if_snapshot_is_valid( + self, + snapshot_to_test, + temp_to_test, + reference_snapshot, + reference_temp, + distance_metric, + allowed_temp_diff, + ): + distance = self._calculate_distance_between_snapshots( + snapshot_to_test, + reference_snapshot, + "realspace", + "minimal_distance", + ) + temp_diff = np.abs(temp_to_test - reference_temp) if distance > distance_metric and temp_diff < allowed_temp_diff: return True else: return False - def _calculate_distance_between_snapshots(self, snapshot1, snapshot2, - distance_metric, reduction, - save_rdf1=False): + def _calculate_distance_between_snapshots( + self, snapshot1, snapshot2, distance_metric, reduction, save_rdf1=False + ): if distance_metric == "realspace": positions1 = snapshot1.get_positions() positions2 = snapshot2.get_positions() if reduction == "minimal_distance": - result = np.amin(distance.cdist(positions1, positions2), axis=0) + result = np.amin( + distance.cdist(positions1, positions2), axis=0 + ) result = np.mean(result) elif reduction == "cosine_distance": number_of_atoms = snapshot1.get_number_of_atoms() - result = distance.cosine(np.reshape(positions1, [number_of_atoms*3]), - np.reshape(positions2, [number_of_atoms*3])) + result = distance.cosine( + np.reshape(positions1, [number_of_atoms * 3]), + np.reshape(positions2, [number_of_atoms * 3]), + ) else: raise Exception("Unknown distance metric reduction.") elif distance_metric == "rdf": - rng = np.min( - np.linalg.norm(snapshot1.get_cell(), axis=0)) - self.\ - parameters.distance_metric_snapshots_rdf_tolerance + rng = ( + np.min(np.linalg.norm(snapshot1.get_cell(), axis=0)) + - self.parameters.distance_metric_snapshots_rdf_tolerance + ) if save_rdf1 is True: if self.__saved_rdf is None: - self.__saved_rdf = RadialDistributionFunction(snapshot1, - rng, - self.parameters.distance_metric_snapshots_rdf_bins).get_rdf() + self.__saved_rdf = RadialDistributionFunction( + snapshot1, + rng, + self.parameters.distance_metric_snapshots_rdf_bins, + ).get_rdf() rdf1 = self.__saved_rdf else: - rdf1 = RadialDistributionFunction(snapshot1, - rng, - self.parameters.distance_metric_snapshots_rdf_bins).get_rdf() - rdf2 = RadialDistributionFunction(snapshot2, - rng, - self.parameters.distance_metric_snapshots_rdf_bins).get_rdf() + rdf1 = RadialDistributionFunction( + snapshot1, + rng, + self.parameters.distance_metric_snapshots_rdf_bins, + ).get_rdf() + rdf2 = RadialDistributionFunction( + snapshot2, + rng, + self.parameters.distance_metric_snapshots_rdf_bins, + ).get_rdf() if reduction == "minimal_distance": - raise Exception("Combination of distance metric and reduction " - "not supported.") + raise Exception( + "Combination of distance metric and reduction " + "not supported." + ) elif reduction == "cosine_distance": result = distance.cosine(rdf1, rdf2) @@ -194,12 +233,17 @@ def _calculate_distance_between_snapshots(self, snapshot1, snapshot2, return result def __denoise(self, signal): - denoised_signal = np.convolve(signal, np.ones( - self.parameters.distance_metrics_denoising_width) / self.parameters.distance_metrics_denoising_width, mode='same') + denoised_signal = np.convolve( + signal, + np.ones(self.parameters.distance_metrics_denoising_width) + / self.parameters.distance_metrics_denoising_width, + mode="same", + ) return denoised_signal - def analyze_trajectory(self, trajectory, equilibrated_snapshot=None, - distance_threshold=None): + def analyze_trajectory( + self, trajectory, equilibrated_snapshot=None, distance_threshold=None + ): """ Calculate distance metrics/first equilibrated timestep on a trajectory. @@ -234,17 +278,31 @@ def analyze_trajectory(self, trajectory, equilibrated_snapshot=None, if equilibrated_snapshot is None: equilibrated_snapshot = trajectory[-1] for idx, step in enumerate(trajectory): - self.distance_metrics.append(self._calculate_distance_between_snapshots(equilibrated_snapshot, step, "rdf", "cosine_distance", - save_rdf1=True)) + self.distance_metrics.append( + self._calculate_distance_between_snapshots( + equilibrated_snapshot, + step, + "rdf", + "cosine_distance", + save_rdf1=True, + ) + ) # Now, we denoise the distance metrics. self.distance_metrics_denoised = self.__denoise(self.distance_metrics) # Which snapshots are considered depends on how we denoise the # distance metrics. - self.first_considered_snapshot = self.parameters.distance_metrics_denoising_width - self.last_considered_snapshot = np.shape(self.distance_metrics_denoised)[0]-self.parameters.distance_metrics_denoising_width - considered_length = self.last_considered_snapshot-self.first_considered_snapshot + self.first_considered_snapshot = ( + self.parameters.distance_metrics_denoising_width + ) + self.last_considered_snapshot = ( + np.shape(self.distance_metrics_denoised)[0] + - self.parameters.distance_metrics_denoising_width + ) + considered_length = ( + self.last_considered_snapshot - self.first_considered_snapshot + ) # Next, the average of the presumed equilibrated part is calculated, # and then the first N number of times teps which are below this @@ -252,14 +310,22 @@ def analyze_trajectory(self, trajectory, equilibrated_snapshot=None, self.average_distance_equilibrated = distance_threshold if self.average_distance_equilibrated is None: self.average_distance_equilibrated = np.mean( - self.distance_metrics_denoised[considered_length - - int(self.parameters.distance_metrics_estimated_equilibrium * considered_length):self.last_considered_snapshot]) + self.distance_metrics_denoised[ + considered_length + - int( + self.parameters.distance_metrics_estimated_equilibrium + * considered_length + ) : self.last_considered_snapshot + ] + ) is_below = True counter = 0 first_snapshot = None for idx, dist in enumerate(self.distance_metrics_denoised): - if idx >= self.first_considered_snapshot \ - and idx <= self.last_considered_snapshot: + if ( + idx >= self.first_considered_snapshot + and idx <= self.last_considered_snapshot + ): if is_below: counter += 1 if dist < self.average_distance_equilibrated: @@ -267,7 +333,10 @@ def analyze_trajectory(self, trajectory, equilibrated_snapshot=None, if dist >= self.average_distance_equilibrated: counter = 0 is_below = False - if counter == self.parameters.distance_metrics_below_average_counter: + if ( + counter + == self.parameters.distance_metrics_below_average_counter + ): first_snapshot = idx break @@ -300,18 +369,38 @@ def analyze_distance_metric(self, trajectory): # distance metric usef for the snapshot parsing (realspace similarity # of the snapshot), we first find the center of the equilibrated part # of the trajectory and calculate the differences w.r.t to to it. - center = int((np.shape(self.distance_metrics_denoised)[0]-self.first_snapshot)/2) + self.first_snapshot - width = int(self.parameters.distance_metrics_estimated_equilibrium * - np.shape(self.distance_metrics_denoised)[0]) + center = ( + int( + ( + np.shape(self.distance_metrics_denoised)[0] + - self.first_snapshot + ) + / 2 + ) + + self.first_snapshot + ) + width = int( + self.parameters.distance_metrics_estimated_equilibrium + * np.shape(self.distance_metrics_denoised)[0] + ) self.distances_realspace = [] self.__saved_rdf = None - for i in range(center-width, center+width): - self.distances_realspace.append(self._calculate_distance_between_snapshots(trajectory[center], trajectory[i], - "realspace", "minimal_distance", save_rdf1=True)) + for i in range(center - width, center + width): + self.distances_realspace.append( + self._calculate_distance_between_snapshots( + trajectory[center], + trajectory[i], + "realspace", + "minimal_distance", + save_rdf1=True, + ) + ) # From these metrics, we assume mean - 2.576 std as limit. # This translates to a confidence interval of ~99%, which should # make any coincidental similarites unlikely. - cutoff = np.mean(self.distances_realspace)-2.576*np.std(self.distances_realspace) + cutoff = np.mean(self.distances_realspace) - 2.576 * np.std( + self.distances_realspace + ) print("Distance metric cutoff is", cutoff) return cutoff diff --git a/malada/providers/supercell.py b/malada/providers/supercell.py index e1e0b95..887bdae 100644 --- a/malada/providers/supercell.py +++ b/malada/providers/supercell.py @@ -1,4 +1,5 @@ """Provider for creation of supercell from crystal structure.""" + from .provider import Provider import os import ase.io @@ -6,6 +7,7 @@ from ase.units import m, kg, Bohr from shutil import copyfile import numpy as np + try: from mp_api.client import MPRester except: @@ -39,8 +41,14 @@ def provide(self, provider_path, cif_file): cif_file : string Path to cif file used for supercell creation. """ - file_name = self.parameters.element + "_" + str(self.parameters.number_of_atoms) \ - + "_" + self.parameters.crystal_structure + ".vasp" + file_name = ( + self.parameters.element + + "_" + + str(self.parameters.number_of_atoms) + + "_" + + self.parameters.crystal_structure + + ".vasp" + ) self.supercell_file = os.path.join(provider_path, file_name) if self.external_supercell_file is None: try: @@ -51,7 +59,9 @@ def provide(self, provider_path, cif_file): transformation_matrix = self.get_transformation_matrix(cif_file) - super_cell = ase.build.make_supercell(primitive_cell, transformation_matrix) + super_cell = ase.build.make_supercell( + primitive_cell, transformation_matrix + ) super_cell = self.get_compressed_cell( super_cell, stretch_factor=self.parameters.stretch_factor, @@ -59,7 +69,10 @@ def provide(self, provider_path, cif_file): radius=self.parameters.WS_radius, ) ase.io.write( - self.supercell_file, super_cell, format="vasp", long_format=True + self.supercell_file, + super_cell, + format="vasp", + long_format=True, ) else: copyfile(self.external_supercell_file, self.supercell_file) @@ -78,7 +91,9 @@ def get_compressed_cell( raise ValueError( "At least one of stretch_factor, density and radius must be speficied" ) - elif sum([stretch_factor is None, density is None, radius is None]) < 2: + elif ( + sum([stretch_factor is None, density is None, radius is None]) < 2 + ): print( "Warning: More than one of stretch factor, density, " "and radius is specified.\nRadius takes first priority, " @@ -90,10 +105,14 @@ def get_compressed_cell( ) stretch_factor = (density_ambient / density) ** (1.0 / 3.0) if radius is not None: - radius_ambient = SuperCellProvider.get_wigner_seitz_radius(supercell) + radius_ambient = SuperCellProvider.get_wigner_seitz_radius( + supercell + ) stretch_factor = radius / radius_ambient - supercell.set_cell(supercell.get_cell() * stretch_factor, scale_atoms=True) + supercell.set_cell( + supercell.get_cell() * stretch_factor, scale_atoms=True + ) return supercell @@ -104,20 +123,21 @@ def get_number_density(supercell, unit="Angstrom^3"): nr_electrons = 0 for i in range(0, nr_atoms): nr_electrons += supercell[i].number - number_density = nr_electrons/volume_atoms + number_density = nr_electrons / volume_atoms angstrom3_in_m3 = 1 / (m * m * m) angstrom3_in_cm3 = angstrom3_in_m3 * 1000000 if unit == "Angstrom^3": return number_density elif unit == "cm^3": - return number_density/angstrom3_in_cm3 + return number_density / angstrom3_in_cm3 else: raise Exception("Unit not implemented") @staticmethod def get_wigner_seitz_radius(supercell): - number_density = SuperCellProvider.\ - get_number_density(supercell, unit="Angstrom^3") + number_density = SuperCellProvider.get_number_density( + supercell, unit="Angstrom^3" + ) rs_angstrom = (3 / (4 * np.pi * number_density)) ** (1 / 3) return rs_angstrom / Bohr @@ -131,9 +151,9 @@ def get_mass_density(supercell, unit="g/(cm^3)"): u_angstrom3_in_kg_m3 = kg / (m * m * m) u_angstrom3_in_g_cm3 = u_angstrom3_in_kg_m3 * 1000 if unit == "kg_m^3": - return mass_density/u_angstrom3_in_kg_m3 + return mass_density / u_angstrom3_in_kg_m3 elif unit == "g/(cm^3)": - return mass_density/u_angstrom3_in_g_cm3 + return mass_density / u_angstrom3_in_g_cm3 elif unit == "u_Angstrom^3": return mass_density else: @@ -160,7 +180,12 @@ def generate_cif(self, cif_file): # search materials project database for the desired element structures = mpr.summary.search( chemsys=self.parameters.element, - fields=["symmetry", "material_id", "theoretical", "energy_above_hull"], + fields=[ + "symmetry", + "material_id", + "theoretical", + "energy_above_hull", + ], ) # narrow down by correct structure structures = list( @@ -180,14 +205,18 @@ def generate_cif(self, cif_file): + "Please reconsider parameters or provide your own input files." ) # narrow down by structures which are experimentally verified - structures_expt = list(filter(lambda x: x.theoretical == False, structures)) + structures_expt = list( + filter(lambda x: x.theoretical == False, structures) + ) # choose minimal energy structure if len(structures_expt) == 0: best_structure = min(structures, key=lambda x: x.energy_above_hull) elif len(structures_expt) == 1: best_structure = structures_expt[0] elif len(structures_expt) > 1: - best_structure = min(structures_expt, key=lambda x: x.energy_above_hull) + best_structure = min( + structures_expt, key=lambda x: x.energy_above_hull + ) # get the ID of the best structure best_structure_id = best_structure.material_id structure = mpr.get_structure_by_material_id( diff --git a/malada/runners/__init__.py b/malada/runners/__init__.py index 23503f8..4945cb1 100644 --- a/malada/runners/__init__.py +++ b/malada/runners/__init__.py @@ -1,4 +1,5 @@ """Runners to run simulations from pipeline.""" + from .runner import Runner from .bashrunner import BashRunner from .runner_interface import RunnerInterface diff --git a/malada/runners/bashrunner.py b/malada/runners/bashrunner.py index fbc67f5..db43459 100644 --- a/malada/runners/bashrunner.py +++ b/malada/runners/bashrunner.py @@ -1,4 +1,5 @@ """Bash based runner. This will be replaced by an ASE based system.""" + import glob import os import ase.io @@ -44,7 +45,11 @@ def run_folder(self, folder, calculation_type): print(filelist, folder) raise Exception("Run folder with ambigous content.") filename = os.path.basename(filelist[0]) - run_process = subprocess.Popen("pw.x -in "+filename+" > "+job_name+".out", cwd=folder, shell=True) + run_process = subprocess.Popen( + "pw.x -in " + filename + " > " + job_name + ".out", + cwd=folder, + shell=True, + ) run_process.wait() if calculation_type == "md": filelist = glob.glob(os.path.join(folder, "*.pw.md.in")) @@ -52,30 +57,46 @@ def run_folder(self, folder, calculation_type): print(filelist, folder) raise Exception("Run folder with ambigous content.") filename = os.path.basename(filelist[0]) - run_process = subprocess.Popen("pw.x -in "+filename+" > "+job_name+".out", cwd=folder, shell=True) + run_process = subprocess.Popen( + "pw.x -in " + filename + " > " + job_name + ".out", + cwd=folder, + shell=True, + ) run_process.wait() elif calculation_type == "dft+pp": scf_file = glob.glob(os.path.join(folder, "*.pw.scf.in")) ldos_file = glob.glob(os.path.join(folder, "*.pp.ldos.in")) dens_file = glob.glob(os.path.join(folder, "*.pp.dens.in")) dos_file = glob.glob(os.path.join(folder, "*.dos.in")) - if len(scf_file) != 1 or \ - len(ldos_file) != 1 or \ - len(dens_file) != 1 or \ - len(dos_file) != 1: + if ( + len(scf_file) != 1 + or len(ldos_file) != 1 + or len(dens_file) != 1 + or len(dos_file) != 1 + ): print(scf_file, ldos_file, dens_file, dos_file, folder) raise Exception("Run folder with ambigous content.") - run_process = subprocess.Popen("pw.x -in "+os.path.basename(scf_file[0])+" > "+job_name+".out;" + - "pp.x -in "+os.path.basename(dens_file[0])+";" + - "dos.x -in "+os.path.basename(dos_file[0])+";" + - "pp.x -in "+os.path.basename(ldos_file[0])+";" - , cwd=folder, shell=True) + run_process = subprocess.Popen( + "pw.x -in " + + os.path.basename(scf_file[0]) + + " > " + + job_name + + ".out;" + + "pp.x -in " + + os.path.basename(dens_file[0]) + + ";" + + "dos.x -in " + + os.path.basename(dos_file[0]) + + ";" + + "pp.x -in " + + os.path.basename(ldos_file[0]) + + ";", + cwd=folder, + shell=True, + ) run_process.wait() - elif calculator_type == "vasp": raise Exception("VASP currently not implemented.") else: raise Exception("Calculator type unknown.") - - diff --git a/malada/runners/runner.py b/malada/runners/runner.py index 2cbe7ce..d146b25 100644 --- a/malada/runners/runner.py +++ b/malada/runners/runner.py @@ -1,4 +1,5 @@ """Base class for all runners.""" + from abc import ABC, abstractmethod diff --git a/malada/runners/runner_interface.py b/malada/runners/runner_interface.py index 5d9a4f6..bf9661a 100644 --- a/malada/runners/runner_interface.py +++ b/malada/runners/runner_interface.py @@ -1,4 +1,5 @@ """Interface to automate creation of Runners.""" + from .bashrunner import BashRunner from .slurm_creator import SlurmCreatorRunner @@ -27,4 +28,3 @@ def RunnerInterface(parameters): return runner else: raise Exception("Unknown runner type.") - diff --git a/malada/runners/slurm_creator.py b/malada/runners/slurm_creator.py index f055a9b..b8caa8e 100644 --- a/malada/runners/slurm_creator.py +++ b/malada/runners/slurm_creator.py @@ -1,4 +1,5 @@ """Runner that only creates SLURM input files.""" + from .runner import Runner from malada import SlurmParameters import os @@ -42,14 +43,20 @@ def run_folder(self, folder, calculation_type): raise Exception("Unknown calculation type encountered.") job_name = os.path.basename(os.path.normpath(folder)) - submit_file = open(os.path.join(folder,"submit.slurm"), mode='w') + submit_file = open(os.path.join(folder, "submit.slurm"), mode="w") submit_file.write("#!/bin/bash\n") - submit_file.write("#SBATCH --nodes="+str(slurm_params.nodes)+"\n") - submit_file.write("#SBATCH --ntasks-per-node="+str(slurm_params.tasks_per_node)+"\n") - submit_file.write("#SBATCH --job-name="+job_name+"\n") + submit_file.write("#SBATCH --nodes=" + str(slurm_params.nodes) + "\n") + submit_file.write( + "#SBATCH --ntasks-per-node=" + + str(slurm_params.tasks_per_node) + + "\n" + ) + submit_file.write("#SBATCH --job-name=" + job_name + "\n") if calculation_type != "md": - submit_file.write("#SBATCH --output="+job_name+".out\n") - submit_file.write("#SBATCH --time="+str(slurm_params.execution_time)+":00:00\n") + submit_file.write("#SBATCH --output=" + job_name + ".out\n") + submit_file.write( + "#SBATCH --time=" + str(slurm_params.execution_time) + ":00:00\n" + ) submit_file.write(slurm_params.partition_string) submit_file.write("\n") submit_file.write(slurm_params.module_loading_string) @@ -63,52 +70,110 @@ def run_folder(self, folder, calculation_type): print(filelist, folder) raise Exception("Run folder with ambigous content.") filename = os.path.basename(filelist[0]) - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.scf_executable +" -in "+filename+"\n") + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.scf_executable + + " -in " + + filename + + "\n" + ) elif calculation_type == "dft+pp": # Get the filenames. scf_file = glob.glob(os.path.join(folder, "*.pw.scf.in")) ldos_file = glob.glob(os.path.join(folder, "*.pp.ldos.in")) dens_file = glob.glob(os.path.join(folder, "*.pp.dens.in")) dos_file = glob.glob(os.path.join(folder, "*.dos.in")) - if len(scf_file) != 1 or \ - len(ldos_file) != 1 or \ - len(dens_file) != 1 or \ - len(dos_file) != 1: + if ( + len(scf_file) != 1 + or len(ldos_file) != 1 + or len(dens_file) != 1 + or len(dos_file) != 1 + ): print(scf_file, ldos_file, dens_file, dos_file, folder) raise Exception("Run folder with ambigous content.") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.scf_executable + - " -in "+os.path.basename(scf_file[0])+" \n") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.pp_executable + - " -in "+os.path.basename(dens_file[0])+" \n") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.dos_executable + - " -in "+os.path.basename(dos_file[0])+" \n") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.pp_executable + - " -in "+os.path.basename(ldos_file[0])+" \n") + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.scf_executable + + " -in " + + os.path.basename(scf_file[0]) + + " \n" + ) + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.pp_executable + + " -in " + + os.path.basename(dens_file[0]) + + " \n" + ) + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.dos_executable + + " -in " + + os.path.basename(dos_file[0]) + + " \n" + ) + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.pp_executable + + " -in " + + os.path.basename(ldos_file[0]) + + " \n" + ) elif calculation_type == "md": md_file = glob.glob(os.path.join(folder, "*.pw.md.in")) if len(md_file) != 1: print(md_file, folder) raise Exception("Run folder with ambigous content.") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" " + - str(slurm_params.nodes*slurm_params.tasks_per_node) +" "+ - slurm_params.scf_executable + - " -in "+os.path.basename(md_file[0])+" \n") + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.scf_executable + + " -in " + + os.path.basename(md_file[0]) + + " \n" + ) elif calculator_type == "vasp": if calculation_type == "dft": submit_file.write("bash potcar_copy.sh\n") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.scf_executable+" \n") + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.scf_executable + + " \n" + ) elif calculation_type == "md": submit_file.write("mkdir slurm-$SLURM_JOB_ID\n") submit_file.write("cp INCAR slurm-$SLURM_JOB_ID\n") @@ -117,11 +182,17 @@ def run_folder(self, folder, calculation_type): submit_file.write("cp potcar_copy.sh slurm-$SLURM_JOB_ID\n") submit_file.write("cd slurm-$SLURM_JOB_ID\n") submit_file.write("bash potcar_copy.sh\n") - submit_file.write(slurm_params.mpi_runner+" "+slurm_params.get_mpirunner_process_params()+" "+ - str(slurm_params.nodes*slurm_params.tasks_per_node)+" "+ - slurm_params.scf_executable+" \n") + submit_file.write( + slurm_params.mpi_runner + + " " + + slurm_params.get_mpirunner_process_params() + + " " + + str(slurm_params.nodes * slurm_params.tasks_per_node) + + " " + + slurm_params.scf_executable + + " \n" + ) submit_file.write(slurm_params.cleanup_string) submit_file.write("\n") submit_file.close() - diff --git a/malada/utils/convergence_guesses.py b/malada/utils/convergence_guesses.py index 59df740..187f83f 100644 --- a/malada/utils/convergence_guesses.py +++ b/malada/utils/convergence_guesses.py @@ -1,26 +1,32 @@ """Initial guesses for convergence calculations.""" -# TODO: Find a smarter way to do this. -cutoff_guesses_qe = {"Fe": [40, 50, 60, 70, 80, 90, 100], - "Be": [40, 50, 60, 70], - "Al": [20, 30, 40, 50, 60, 70], - "H": [26, 36, 46, 56, 66, 76], - "C": [70, 80, 90, 100, 110, 120], - "Li": [50, 60, 70, 80, 90, 100], - "B": [50, 60, 70, 80, 90, 100],} +# TODO: Find a smarter way to do this. -cutoff_guesses_vasp = {"Fe": [268, 368, 468, 568, 668, 768], - "Al": [240, 340, 440, 540], - "Be": [248, 258, 268, 278], - "H": [400, 410, 420, 430, 440, 450], - "Li": [140, 150, 160, 170], - "B": [310, 320, 330, 340]} +cutoff_guesses_qe = { + "Fe": [40, 50, 60, 70, 80, 90, 100], + "Be": [40, 50, 60, 70], + "Al": [20, 30, 40, 50, 60, 70], + "H": [26, 36, 46, 56, 66, 76], + "C": [70, 80, 90, 100, 110, 120], + "Li": [50, 60, 70, 80, 90, 100], + "B": [50, 60, 70, 80, 90, 100], +} -kpoints_guesses = {"Fe": [2, 3, 4], - "Be": [2, 3, 4, 5, 6], - "Al": [2, 3, 4], - "H": [2, 3, 4, 5], - "C": [1, 2, 3, 4], - "Li": [1, 2, 3, 4], - "B": [1, 2, 3, 4]} +cutoff_guesses_vasp = { + "Fe": [268, 368, 468, 568, 668, 768], + "Al": [240, 340, 440, 540], + "Be": [248, 258, 268, 278], + "H": [400, 410, 420, 430, 440, 450], + "Li": [140, 150, 160, 170], + "B": [310, 320, 330, 340], +} +kpoints_guesses = { + "Fe": [2, 3, 4], + "Be": [2, 3, 4, 5, 6], + "Al": [2, 3, 4], + "H": [2, 3, 4, 5], + "C": [1, 2, 3, 4], + "Li": [1, 2, 3, 4], + "B": [1, 2, 3, 4], +} diff --git a/malada/utils/custom_converter.py b/malada/utils/custom_converter.py index a6d33f9..7f64541 100644 --- a/malada/utils/custom_converter.py +++ b/malada/utils/custom_converter.py @@ -1,4 +1,5 @@ """Collection of custom converters.""" + import scipy.constants import ase.units @@ -19,8 +20,10 @@ def kelvin_to_rydberg(temperature_K): """ k_B = scipy.constants.Boltzmann - Ry_in_Joule = scipy.constants.Rydberg*scipy.constants.h*scipy.constants.c - return (k_B*temperature_K)/Ry_in_Joule + Ry_in_Joule = ( + scipy.constants.Rydberg * scipy.constants.h * scipy.constants.c + ) + return (k_B * temperature_K) / Ry_in_Joule def kelvin_to_eV(temperature_K): @@ -58,7 +61,8 @@ def second_to_rydberg_time(time_s): Time in Rydberg unit time. """ - Ry_in_Joule = scipy.constants.Rydberg*scipy.constants.h*scipy.constants.c - t_0 = scipy.constants.hbar/Ry_in_Joule + Ry_in_Joule = ( + scipy.constants.Rydberg * scipy.constants.h * scipy.constants.c + ) + t_0 = scipy.constants.hbar / Ry_in_Joule return time_s / t_0 - diff --git a/malada/utils/parameters.py b/malada/utils/parameters.py index d4a9d14..5c81924 100644 --- a/malada/utils/parameters.py +++ b/malada/utils/parameters.py @@ -127,8 +127,11 @@ def __init__(self): self.dft_calculator = "qe" self.md_calculator = "qe" # TODO: Get number of electrons directly from file. - self.pseudopotential = {"path": None, "valence_electrons": 0, - "name": None} + self.pseudopotential = { + "path": None, + "valence_electrons": 0, + "name": None, + } self.run_system = "bash" self.mp_api_file = os.path.expanduser("~") + "/malada/.mp_api/.api_key" self.dft_slurm = SlurmParameters() diff --git a/malada/utils/slurmparams.py b/malada/utils/slurmparams.py index d5b91f1..a4a68da 100644 --- a/malada/utils/slurmparams.py +++ b/malada/utils/slurmparams.py @@ -1,4 +1,5 @@ """Parameters to create a slurm run script.""" + from xml.etree.ElementTree import Element, SubElement, tostring, parse from xml.dom import minidom @@ -58,32 +59,24 @@ def save(self, filename): filename : string Path to file to save parameters to. """ - top = Element('slurmparameters') - node = SubElement(top, "scf_executable", - {"type": "string"}) + top = Element("slurmparameters") + node = SubElement(top, "scf_executable", {"type": "string"}) node.text = self.scf_executable - node = SubElement(top, "module_loading_string", - {"type": "string"}) + node = SubElement(top, "module_loading_string", {"type": "string"}) node.text = self.module_loading_string - node = SubElement(top, "execution_time", - {"type": "int"}) + node = SubElement(top, "execution_time", {"type": "int"}) node.text = str(self.execution_time) - node = SubElement(top, "partition_string", - {"type": "string"}) + node = SubElement(top, "partition_string", {"type": "string"}) node.text = self.partition_string - node = SubElement(top, "mpi_runner", - {"type": "string"}) + node = SubElement(top, "mpi_runner", {"type": "string"}) node.text = self.mpi_runner - node = SubElement(top, "tasks_per_node", - {"type": "int"}) + node = SubElement(top, "tasks_per_node", {"type": "int"}) node.text = str(self.tasks_per_node) - node = SubElement(top, "nodes", - {"type": "int"}) + node = SubElement(top, "nodes", {"type": "int"}) node.text = str(self.nodes) - node = SubElement(top, "cleanup_string", - {"type": "string"}) + node = SubElement(top, "cleanup_string", {"type": "string"}) node.text = self.cleanup_string - rough_string = tostring(top, 'utf-8') + rough_string = tostring(top, "utf-8") reparsed = minidom.parseString(rough_string) with open(filename, "w") as f: f.write(reparsed.toprettyxml(indent=" ")) @@ -109,17 +102,29 @@ def from_xml(cls, filename): new_object.scf_executable = filecontents.find("scf_executable").text try: new_object.pp_executable = filecontents.find("pp_executable").text - new_object.dos_executable = filecontents.find("dos_executable").text + new_object.dos_executable = filecontents.find( + "dos_executable" + ).text except: pass - new_object.module_loading_string = filecontents.find("module_loading_string").text + new_object.module_loading_string = filecontents.find( + "module_loading_string" + ).text new_object.mpi_runner = filecontents.find("mpi_runner").text - new_object.execution_time = int(filecontents.find("execution_time").text) - new_object.partition_string = filecontents.find("partition_string").text - new_object.tasks_per_node = int(filecontents.find("tasks_per_node").text) + new_object.execution_time = int( + filecontents.find("execution_time").text + ) + new_object.partition_string = filecontents.find( + "partition_string" + ).text + new_object.tasks_per_node = int( + filecontents.find("tasks_per_node").text + ) new_object.nodes = int(filecontents.find("nodes").text) try: - new_object.cleanup_string = filecontents.find("cleanup_string").text + new_object.cleanup_string = filecontents.find( + "cleanup_string" + ).text except: pass diff --git a/malada/utils/vasp_utils.py b/malada/utils/vasp_utils.py index 33193fc..a77bedc 100644 --- a/malada/utils/vasp_utils.py +++ b/malada/utils/vasp_utils.py @@ -1,4 +1,5 @@ """Utilities for setting up VASP calculations.""" + import os @@ -44,7 +45,8 @@ def write_to_kpoints(folder, kgrid): file_handle.write("0\n") file_handle.write("Gamma\n") file_handle.write( - str(kgrid[0]) + " " + str(kgrid[1]) + " " + str(kgrid[2]) + "\n") + str(kgrid[0]) + " " + str(kgrid[1]) + " " + str(kgrid[2]) + "\n" + ) file_handle.write("0. 0. 0.\n") file_handle.close() @@ -67,5 +69,5 @@ def write_to_potcar_copy(folder, pspstring): """ file_handle = open(os.path.join(folder, "potcar_copy.sh"), "w") file_handle.write("#!/bin/bash\n") - file_handle.write("cp "+ pspstring+" POTCAR\n") + file_handle.write("cp " + pspstring + " POTCAR\n") file_handle.close() diff --git a/malada/version.py b/malada/version.py index 078673c..ba63a13 100644 --- a/malada/version.py +++ b/malada/version.py @@ -1,3 +1,3 @@ """Version number of MALA.""" -__version__: str = '0.0.1' +__version__: str = "0.0.1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f210e8a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 79 + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" From e1e97e1c9def62b888011d173a4a3cd6ce98c8a7 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 24 Oct 2024 19:36:56 +0200 Subject: [PATCH 12/13] Added missing docstring --- malada/utils/parameters.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/malada/utils/parameters.py b/malada/utils/parameters.py index 5c81924..e11f3b7 100644 --- a/malada/utils/parameters.py +++ b/malada/utils/parameters.py @@ -110,6 +110,10 @@ class Parameters: be used. E.g. if =0.05, the overall number of bands will be the number of electrons times 1.05. Has to be scaled with temperature. Default is 0.05, which should be ok up to ~2500K. + + dft_assume_two_dimensional : bool + If True, two dimensional DFT calculations will be performed. + To that end, the relevant QE parameters will be selected. """ def __init__(self): @@ -152,6 +156,7 @@ def __init__(self): self.dft_use_inversion_symmetry = False self.dft_mixing_beta = 0.1 self.dft_assume_two_dimensional = False + self.twodimensional_cutting_tolerance = 1e-3 # Information about MD parsing. self.snapshot_parsing_beginning = -1 From d995a36406edded8758d1cf468c67b6426270ab8 Mon Sep 17 00:00:00 2001 From: Lenz Fiedler Date: Thu, 24 Oct 2024 19:43:17 +0200 Subject: [PATCH 13/13] Added note on 2D accuracy --- malada/providers/dft.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/malada/providers/dft.py b/malada/providers/dft.py index 7f1b08f..cff05b0 100644 --- a/malada/providers/dft.py +++ b/malada/providers/dft.py @@ -152,6 +152,17 @@ def __create_dft_run( } nbands = self._get_number_of_bands() outdir = "temp" + if ( + self.parameters.dft_scf_accuracy_per_atom_Ry >= 1e-6 + and self.parameters.dft_assume_two_dimensional + ): + print( + "Large DFT accuracy threshold detected. When running " + "two-dimensional calculations, which include areas of low " + "electronic density, smaller accuracy thresholds are " + "recommended. Consider setting dft_scf_accuracy_per_atom_Ry " + "to, e.g., 1-e9." + ) qe_input_data = { "occupations": "smearing", "calculation": "scf",