def show_path(directory, filename): """ Show the final migration for a NEB calculation. Returns: """ initial_structure = Cathode.from_file( os.path.join(directory, "initial", "final_cathode.json")).as_ordered_structure() final_structure = Cathode.from_file( os.path.join(directory, "final", "final_cathode.json")).as_ordered_structure() image_dirs = [d for d in os.listdir(directory) if "0" in d and os.path.isdir(d)] image_dirs.sort() image_structures = [Structure.from_file(os.path.join(directory, idir, "CONTCAR")) for idir in image_dirs[1:-1]] image_structures.append(final_structure) transition_structure = initial_structure.copy() for structure in image_structures: for site in structure: transition_structure.append(site.specie, site.frac_coords) transition_structure.to("cif", filename)
def find_configuration_dict(path): path = os.path.abspath(path) return { Cathode.from_file(file).__hash__(): { "structure": Cathode.from_file(file).as_dict(), "directory": file.replace(path, "").replace("configuration.json", "") } for file in find_all("configuration.json", path) }
def find_transition_cathodes(directory, initial_contains="init.json", final_contains="final.json"): """ Find the initial and final Cathodes for a transition from the files in a directory. The function demands .json type files, in order to include the magnetic moments, as well as the vacant Sites in the Cathode. Args: directory (str): Path to the directory in which the initial and final cathode structure files should be present. initial_contains (str): String that is present in the initial Cathode structure file. final_contains (str): String that is present in the final Cathode structure file. Returns: tuple: Tuple of the initial and final pybat.core.Cathode's """ directory = os.path.abspath(directory) initial_structure_file = None final_structure_file = None for item in os.listdir(directory): if initial_contains in item \ and os.path.isfile(os.path.join(directory, item)): initial_structure_file = os.path.join(directory, item) if final_contains in item \ and os.path.isfile(os.path.join(directory, item)): final_structure_file = os.path.join(directory, item) if initial_structure_file: initial_cathode = Cathode.from_file(initial_structure_file) else: raise FileNotFoundError("No suitably named initial structure file in " "directory.") if final_structure_file: final_cathode = Cathode.from_file(final_structure_file) else: raise FileNotFoundError("No suitably named final structure file in " "directory.") return initial_cathode, final_cathode
def find_hash_dict(path): path = os.path.abspath(path) return { Cathode.from_file(file).__hash__(): file.replace(path, "").replace("cathode.json", "") for file in find_all("cathode.json", path) }
def static(structure_file, functional, directory, write_chgcar, in_custodian, number_nodes, launchpad_file, lpad_name): """ Set up an static calculation workflow. """ from pybat.workflow.workflows import get_wf_static # Process the input options if number_nodes == 0: number_nodes = None functional = string_to_functional(functional) # Set up the calculation directory directory = set_up_directory(directory, functional, "static") cat = Cathode.from_file(structure_file) if launchpad_file: lpad = LaunchPad.from_file(launchpad_file) else: lpad = _load_launchpad(lpad_name) lpad.add_wf( get_wf_static(structure=cat, functional=functional, directory=directory, write_chgcar=write_chgcar, in_custodian=in_custodian, number_nodes=number_nodes))
def make_supercell(structure_file, supercell, fmt="json"): """ Make a supercell of the Structure in the structure file. #TODO Assumes the new structure must be of the same class as the original, i.e. a Cathode is transformed into another cathode. Args: structure_file: supercell: fmt: Returns: """ # Turn into list TODO add checks supercell_list = [int(number) for number in supercell] # Load the structure as a Cathode cathode = Cathode.from_file(structure_file) cathode.make_supercell(supercell_list) super_structure_file = structure_file.split(".")[0] + "_" + supercell \ + "." + fmt cathode.to(fmt, super_structure_file)
def optimize(structure_file, functional, directory, is_metal, in_custodian, number_nodes, launchpad_file, lpad_name): """ Set up a geometry optimization workflow. """ from pybat.workflow.workflows import get_wf_optimize cat = Cathode.from_file(structure_file) # Process the input options if number_nodes == 0: number_nodes = None functional = string_to_functional(functional) # Set up the calculation directory directory = set_up_directory(directory, functional, "optimize") if launchpad_file: lpad = LaunchPad.from_file(launchpad_file) else: lpad = _load_launchpad(lpad_name) lpad.add_wf( get_wf_optimize(structure=cat, functional=functional, directory=directory, is_metal=is_metal, in_custodian=in_custodian, number_nodes=number_nodes))
def get_wf_neb(directory, nimages=7, functional=("pbe", {}), is_metal=False, in_custodian=False, number_nodes=None): """ Set up a workflow that calculates the kinetic barrier between two geometries. # TODO TEMPORARY? Should NEB be integrated in other workflows? If so, should we still have a separate NEB workflow? Args: directory (str): Directory in which the NEB calculation should be performed. nimages (int): Number of images to use for the NEB calculation. functional (tuple): Tuple with the functional details. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. is_metal (bool): Flag that indicates the material being studied is a metal, which changes the smearing from Gaussian to second order Methfessel-Paxton of 0.2 eV. Defaults to False. in_custodian (bool): Flag that indicates that the calculations should be run within a Custodian. Defaults to False. number_nodes (int): Number of nodes that should be used for the calculations. Is required to add the proper `_category` to the Firework generated, so it is picked up by the right Fireworker. Defaults to the number of images. """ directory = os.path.abspath(directory) # If no number of nodes is specified, take the number of images if number_nodes is None: number_nodes = nimages # Create the Firework that sets up and runs the NEB neb_firework = NebFirework(directory=directory, nimages=nimages, functional=functional, is_metal=is_metal, in_custodian=in_custodian, number_nodes=number_nodes) # Add number of nodes to spec, or "none" firework_spec = {} if number_nodes is None: firework_spec.update({"_category": "none"}) else: firework_spec.update({"_category": str(number_nodes) + "nodes"}) cathode = Cathode.from_file( os.path.join(directory, "final", "initial_cathode.json")) dir_name = os.path.abspath(directory).split("/")[-1] workflow_name = str(cathode.composition).replace(" ", "") + " " + dir_name return Workflow(fireworks=[ neb_firework, ], name=workflow_name)
def static(structure_file, functional, directory, write_chgcar): """ Set up a static calculation for a structure. """ from pybat.cli.commands.setup import static cat = Cathode.from_file(structure_file) static(structure=cat, functional=string_to_functional(functional), directory=directory, write_chgcar=write_chgcar)
def run_task(self, fw_spec): directory = os.path.abspath(self["directory"]) cathode = Cathode.from_file( os.path.join(directory, "initial_cathode.json")) cathode.update_sites(directory, ignore_magmom=self.get("ignore_magmom", False)) cathode.to("json", os.path.join(directory, "final_cathode.json")) FWAction() return FWAction(update_spec={"structure": cathode.as_dict()})
def optimize(structure_file, functional, directory, is_metal): """ Set up a geometry optimization for a structure. """ from pybat.cli.commands.setup import optimize cat = Cathode.from_file(structure_file) optimize(structure=cat, functional=string_to_functional(functional), directory=directory, is_metal=is_metal)
def primitive_structure(structure_file, fmt="json"): """ Args: structure_file: fmt: Returns: """ cathode = Cathode.from_file(structure_file) spg = SpacegroupAnalyzer(cathode) prim_structure_file = structure_file.split(".")[0] + "_conv" + "." + fmt spg.get_primitive_standard_structure().to(fmt, prim_structure_file)
def migration(structure_file, migration_indices, write_cif): """ Define a migration of an ion in a structure. """ from pybat.cli.commands.define import define_migration cat = Cathode.from_file(structure_file) if migration_indices == (0, 0): raise NotImplementedError( "Please provide the migration indices using the '-i' " "option.") define_migration(structure=cat, site=migration_indices[0], final_site=migration_indices[1], write_cif=write_cif)
def get_cathode(directory, to_current_dir=False, write_cif=False, ignore_magmom=False): """ Construct a .json file of the updated Cathode from a geometry optimization, based on the initial_cathode.json file and the output of a VASP calculation, i.e. the CONTCAR and OUTCAR files. All these files must be present in the directory. Args: directory (str): Directory in which the geometry optimization calculation was performed. Must contain the initial_cathode.json, OUTCAR and CONTCAR file. to_current_dir (bool): Write the output final_cathode files to the current working directory. write_cif (bool): Flag that determines whether a .cif file of the cathode structure is written to the directory. ignore_magmom (bool): Flag that indicates that the final magnetic moments of the optimized structure should be ignored. This means that the magnetic moments of the initial structure will be used. Returns: None """ directory = os.path.abspath(directory) cathode = Cathode.from_file(os.path.join(directory, "initial_cathode.json")) cathode.update_sites(directory, ignore_magmom=ignore_magmom) if to_current_dir: filename = os.path.join(os.getcwd(), "final_cathode") else: filename = os.path.join(directory, "final_cathode") cathode.to("json", filename + ".json") if write_cif: cathode.to("cif", filename + ".cif")
def neb(directory, nimages=7, functional=("pbe", {}), is_metal=False, is_migration=False): """ Set up the NEB calculation from the initial and final structures. Args: directory (str): Directory in which the transition calculations should be set up. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. nimages (int): Number of images to use in the NEB calculation. is_metal (bool): Flag that indicates the material being studied is a metal, which changes the smearing from Gaussian to second order Methfessel-Paxton of 0.2 eV. is_migration (bool): Flag that indicates that the transition is a migration of an atom in the structure. Returns: None """ directory = os.path.abspath(directory) # Extract the optimized initial and final geometries initial_dir = os.path.join(directory, "initial") final_dir = os.path.join(directory, "final") try: # Check to see if the initial final_cathode structure is present initial_structure = Cathode.from_file( os.path.join(initial_dir, "final_cathode.json")).as_ordered_structure() except FileNotFoundError: # In case the required json file is not present, check to see if # there is VASP output which can be used initial_structure = Structure.from_file( os.path.join(initial_dir, "CONTCAR")) # Add the magnetic configuration to the initial structure initial_out = Outcar(os.path.join(initial_dir, "OUTCAR")) initial_magmom = [site["tot"] for site in initial_out.magnetization] try: initial_structure.add_site_property("magmom", initial_magmom) except ValueError: if len(initial_magmom) == 0: print("No magnetic moments found in OUTCAR file. Setting " "magnetic moments to zero.") initial_magmom = [0] * len(initial_structure) initial_structure.add_site_property("magmom", initial_magmom) else: raise ValueError("Number of magnetic moments in OUTCAR file " "do not match the number of sites!") except BaseException: raise FileNotFoundError("Could not find required structure " "information in " + initial_dir + ".") try: final_structure = Structure.from_file( os.path.join(final_dir, "CONTCAR")) except FileNotFoundError: final_structure = Cathode.from_file( os.path.join(final_dir, "final_cathode.json")).as_ordered_structure() # In case the transition is a migration if is_migration: # Set up the static potential for the Pathfinder from the host charge # density host_charge_density = Chgcar.from_file(os.path.join(directory, "host")) host_potential = ChgcarPotential(host_charge_density) migration_site_index = find_migrating_ion(initial_structure, final_structure) neb_path = NEBPathfinder(start_struct=initial_structure, end_struct=final_structure, relax_sites=migration_site_index, v=host_potential) images = neb_path.images neb_path.plot_images("neb.vasp") # In case an "middle image" has been provided via which to interpolate elif os.path.exists(os.path.join(directory, "middle")): print("Found a 'middle' directory in the NEB directory. Interpolating " "via middle geometry.") # Load the middle image middle_structure = Structure.from_file( os.path.join(directory, "middle", "CONTCAR")) # Perform an interpolation via this image images_1 = initial_structure.interpolate( end_structure=middle_structure, nimages=int((nimages + 1) / 2), interpolate_lattices=True) images_2 = middle_structure.interpolate(end_structure=final_structure, nimages=int((nimages) / 2 + 1), interpolate_lattices=True) images = images_1[:-1] + images_2 else: # Linearly interpolate the initial and final structures images = initial_structure.interpolate(end_structure=final_structure, nimages=nimages + 1, interpolate_lattices=True) # TODO Add functionality for NEB calculations with changing lattices user_incar_settings = {} # Set up the functional if functional[0] != "pbe": functional_config = _load_yaml_config(functional[0] + "Set") functional_config["INCAR"].update(functional[1]) user_incar_settings.update(functional_config["INCAR"]) # Add the standard Methfessel-Paxton smearing for metals if is_metal: user_incar_settings.update({"ISMEAR": 2, "SIGMA": 0.2}) neb_calculation = PybatNEBSet(images, potcar_functional=DFT_FUNCTIONAL, user_incar_settings=user_incar_settings) # Set up the NEB calculation neb_calculation.write_input(directory) # Make a file to visualize the transition neb_calculation.visualize_transition( os.path.join(directory, "transition.cif"))
def relax(structure_file, functional=("pbe", {}), calculation_dir="", is_metal=False): """ Set up a standard geometry optimization calculation of a Cathode structure. Optimizes both the atomic positions as well as the unit cell. Args: structure_file (str): Path to the Cathode structure file, either relative or absolute. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. calculation_dir (str): Path to the directory in which to set up the VASP calculation. is_metal (bool): Flag that indicates the material being studied is a metal, which changes the smearing from Gaussian to second order Methfessel-Paxton of 0.2 eV. Returns: str: Path to the directory in which the calculation is set up. """ # Import the structure as a Cathode instance from the structure file structure_file = os.path.abspath(structure_file) structure = Cathode.from_file(structure_file).as_ordered_structure() # Check if a magnetic moment was not provided for the sites. If not, make # sure it is zero for the calculations._ if "magmom" not in structure.site_properties.keys(): structure.add_site_property("magmom", [0] * len(structure.sites)) # Set up the calculation user_incar_settings = {} # Set up the functional if functional[0] != "pbe": functional_config = _load_yaml_config(functional[0] + "Set") functional_config["INCAR"].update(functional[1]) user_incar_settings.update(functional_config["INCAR"]) # Set up the calculation directory if calculation_dir == "": calculation_dir = os.path.join(os.getcwd(), functional[0]) if functional[0] == "pbeu": calculation_dir += "_" + "".join( k + str(functional[1]["LDAUU"][k]) for k in functional[1]["LDAUU"].keys()) calculation_dir += "_relax" # For metals, add some Methfessel Paxton smearing if is_metal: user_incar_settings.update({"ISMEAR": 2, "SIGMA": 0.2}) # Set up the geometry optimization geo_optimization = BulkRelaxSet(structure=structure, user_incar_settings=user_incar_settings, potcar_functional=DFT_FUNCTIONAL) # Write the input files to the geometry optimization directory geo_optimization.write_input(calculation_dir) shutil.copy(structure_file, os.path.join(calculation_dir, "initial_cathode.json")) return calculation_dir
def find_all_cathode_hashes(path): return [Cathode.from_file(file).__hash__() for file in find_all("cathode.json", path)]
def configuration_workflow(structure_file, substitution_sites=None, element_list=None, sizes=None, concentration_restrictions=None, max_configurations=None, functional=("pbe", {}), directory=None, in_custodian=False, number_nodes=None): """ Set up a workflow for a set of atomic configurations, which includes a geometric optimization as well as a SCF calculation based on the final geometry. Args: structure_file (str): Structure file of the cathode material. Note that the structure file should be a json format file that is derived from the Cathode class, i.e. it should contain the cation configuration of the structure. substitution_sites (list): List of site indices or pymatgen.Sites to be substituted. element_list (list): List of string representations of the cation elements which have to be substituted on the substitution sites. Can also include "Vac" to introduce vacancy sites. E.g. ["Li", "Vac"]; ["Mn", "Co", "Ni"]; ... sizes (list): List of unit supercell sizes to be considered for the enumeration of the configurations. E.g. [1, 2]; range(1, 4); ... concentration_restrictions (dict): Dictionary of allowed concentration ranges for each element. Note that the concentration is defined versus the total amount of atoms in the unit cell. E.g. {"Li": (0.2, 0.3)}; {"Ni": (0.1, 0.2, "Mn": (0.05, 0.1)}; ... max_configurations (int): Maximum number of new configurations to generate. Note that the function detects all the cathode.json files present in the directory tree and ignores the corresponding configurations. max_configurations is the maximum number of new configurations that need to be generated, i.e. on top of the configurations already present in the directory tree in the form of cathode.json files. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. directory (str): Path to the directory in which the configurations and calculations should be set up. in_custodian (bool): Flag that indicates that the calculations should be run within a Custodian. Defaults to False. number_nodes (int): Number of nodes that should be used for the calculations. Is required to add the proper `_category` to the Firework generated, so it is picked up by the right Fireworker. Returns: None """ # Load the cathode from the structure file cathode = Cathode.from_file(structure_file) # Check for the required input, and request if necessary if not substitution_sites or not element_list or not sizes: print(cathode) print() if not substitution_sites: substitution_sites = [int(i) for i in input( "Please provide the substitution site indices, separated by a space: " ).split(" ")] if not element_list: element_list = [i for i in input( "Please provide the substitution elements, separated by a space: " ).split(" ")] if not sizes: sizes = [int(i) for i in input( "Please provide the possible unit cell sizes, separated by a space: " ).split(" ")] # Set up the directory if directory == "": directory = os.getcwd() directory = os.path.abspath(directory) configuration_task = ConfigurationTask( structure=cathode, directory=directory, substitution_sites=list(substitution_sites), element_list=element_list, sizes=list(sizes), concentration_restrictions=concentration_restrictions, max_configurations=max_configurations ) energy_task = EnergyConfTask( functional=functional, in_custodian=in_custodian, number_nodes=number_nodes ) # Set up a (sort of) clear name for the workflow workflow_name = str(cathode.composition.reduced_formula).replace(" ", "") workflow_name += " " + str(element_list) workflow_name += " " + str(functional) configuration_fw = Firework(tasks=[configuration_task, energy_task], name="Configuration Setup", spec={"_category": "none"}) # Create the workflow workflow = Workflow( fireworks=[configuration_fw], name=workflow_name ) LAUNCHPAD.add_wf(workflow)
def configuration(structure_file, functional, sub_sites, element_list, sizes, directory, include_existing, conc_restrict, max_conf, in_custodian, number_nodes, launchpad_file, lpad_name): """ Set up a workflow for a set of configurations. Calculate the geometry and total energy of a set of configurations. The configurations can be specified using the various options. In case some required input is not specified during command execution, it will be requested from the user. """ from pybat.workflow.workflows import get_wf_configurations cat = Cathode.from_file(structure_file) # Set up the directory if directory == "": directory = os.getcwd() directory = os.path.abspath(directory) # Process the option input try: substitution_sites = eval(sub_sites) except SyntaxError: substitution_sites = [int(site) for site in sub_sites.split(" ")] try: element_list = eval(element_list) except SyntaxError: element_list = [el for el in element_list.split(" ")] try: sizes = [int(i) for i in sizes.strip("[]").split(",")] except ValueError: sizes = eval(sizes) try: conc_restrict = eval(conc_restrict) except TypeError: conc_restrict = None # In case some required settings are missing, request them if not substitution_sites: print(cat) print() substitution_sites = [ int(i) for i in input( "Please provide the substitution site indices, separated by a space: " ).split(" ") ] if not element_list: element_list = [ i for i in input( "Please provide the substitution elements, separated by a space: " ).split(" ") ] if not sizes: sizes = [ int(i) for i in input( "Please provide the possible unit cell sizes, separated by a space: " ).split(" ") ] if launchpad_file: lpad = LaunchPad.from_file(launchpad_file) else: lpad = _load_launchpad(lpad_name) lpad.add_wf( get_wf_configurations(structure=cat, substitution_sites=substitution_sites, element_list=element_list, sizes=sizes, concentration_restrictions=conc_restrict, max_configurations=max_conf, functional=string_to_functional(functional), directory=directory, include_existing=include_existing, in_custodian=in_custodian, number_nodes=number_nodes))
def run_task(self, fw_spec): # If requested, check for existing configurations in the directory tree current_conf_dict = find_configuration_dict(self["directory"]) # Adjust max_configurations based on existing configurations if self.get("max_configurations", None): if self.get("include_existing", True): max_conf_to_generate = self.get("max_configurations") else: max_conf_to_generate = self.get("max_configurations") \ + len(current_conf_dict) else: max_conf_to_generate = None if not self.get("configuration_list", None): structure = self["structure"] # In case the input geometry is a Structure, convert it to a Cathode if isinstance(structure, Structure): structure = Cathode.from_structure(structure) configurations = structure.get_cation_configurations( substitution_sites=self["substitution_sites"], cation_list=self["element_list"], sizes=self["sizes"], concentration_restrictions=self.get( "configuration_restrictions", None), max_configurations=max_conf_to_generate) else: configurations = self.get("configuration_list", None) configuration_dict = {} conf_number = 0 for configuration in configurations: conf_hash = configuration.__hash__() # If the configuration is not found in the directory tree if conf_hash not in current_conf_dict.keys(): # Make sure the configuration directory number is new while "conf_" + str(conf_number) in [ e for l in [ v["directory"].split("/") for v in current_conf_dict.values() ] for e in l if "conf" in e ]: conf_number += 1 configuration_dir = os.path.join(self["directory"], "conf_" + str(conf_number), "prim") # Create the configuration directory and write the structure to a file os.makedirs(configuration_dir) configuration.to( "json", os.path.join(configuration_dir, "configuration.json")) configuration_dict[str(conf_hash)] = { "structure": configuration.as_dict(), "directory": configuration_dir } conf_number += 1 elif self.get("include_existing", True): configuration_dict[str(conf_hash)] = { "structure": configuration.as_dict(), "directory": os.path.join(self["directory"], current_conf_dict[conf_hash]["directory"]) } # Break out of the loop if we have enough configurations if len(configuration_dict) == self.get("max_configurations", None): break return FWAction(update_spec={"configuration_dict": configuration_dict})
def static(structure, directory="", functional=("pbe", {}), write_chgcar=False): """ Set up a standard static calculation using the tetrahedron method to obtain accurate total energies. Args: structure: pymatgen.Structure OR path to structure file for which to set up the static calculation. directory (str): Path to the directory in which to set up the static calculation. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. write_chgcar (bool): Write out the charge density for Bader charge analysis. Returns: str: Path to the directory in which the calculation is set up. """ # Set up the calculation directory directory = _set_up_directory(directory, functional, "static") try: os.makedirs(directory) except FileExistsError: pass # In case the structure is given as a string, load it from the specified path if isinstance(structure, str): structure = Cathode.from_file(structure) # If the structure is a cathode object if isinstance(structure, Cathode): structure.to("json", os.path.join(directory, "initial_cathode.json")) structure = structure.as_ordered_structure() # Set up the calculation user_incar_settings = {} # Set up the functional user_incar_settings.update(_load_functional(functional)) # Check if a magnetic moment was provided for the sites. If so, perform a # spin-polarized calculation if "magmom" in structure.site_properties.keys(): user_incar_settings.update({"ISPIN": 2, "MAGMOM": True}) # Set charge density to be written if requested if write_chgcar: user_incar_settings.update({"LCHARG": True, "LAECHG": True}) # For the HSE06 calculation, also increase the FFT grids, etc... if functional[0] == "hse": user_incar_settings.update({"PRECFOCK": "Accurate"}) # Set up the BulkSCFSet static_calculation = BulkSCFSet(structure=structure, user_incar_settings=user_incar_settings, potcar_functional=DFT_FUNCTIONAL) # Write the input files to the SCF calculation directory static_calculation.write_input(directory) return directory
def optimize(structure, directory="", functional=("pbe", {}), is_metal=False): """ Set up a standard geometry optimization calculation for a structure. Optimizes both the atomic positions as well as the unit cell (ISIF=3). Args: structure: pymatgen.Structure OR path to structure file for which to set up the geometry optimization calculation. directory (str): Path to the directory in which to set up the geometry optimization. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. E.g. ("hse", {"LAEXX": 0.2}). is_metal (bool): Flag that indicates the material being studied is a metal, which changes the smearing from Gaussian to second order Methfessel-Paxton of 0.2 eV. Returns: str: Path to the directory in which the calculation is set up. """ # Set up the calculation directory directory = _set_up_directory(directory, functional, "optimize") try: os.makedirs(directory) except FileExistsError: pass # In case the structure is given as a string, load it from the specified path if isinstance(structure, str): structure = Cathode.from_file(structure) # If the structure is a cathode object, convert it to a structure for VASP IO if isinstance(structure, Cathode): structure.to("json", os.path.join(directory, "initial_cathode.json")) structure = structure.as_ordered_structure() # Set up the calculation user_incar_settings = {} # Set up the functional user_incar_settings.update(_load_functional(functional)) # Check if a magnetic moment was provided for the sites. If so, perform a # spin-polarized calculation if "magmom" in structure.site_properties.keys(): user_incar_settings.update({"ISPIN": 2, "MAGMOM": True}) # For metals, use Methfessel Paxton smearing if is_metal: user_incar_settings.update({"ISMEAR": 2, "SIGMA": 0.2}) # Set up the geometry optimization geo_optimization = BulkRelaxSet(structure=structure, user_incar_settings=user_incar_settings, potcar_functional=DFT_FUNCTIONAL) # Write the input files to the geometry optimization directory geo_optimization.write_input(directory) return directory
def define_migration(structure_file, migration_indices=(0, 0), write_cif=False): """ This script allows the user to define a migration of a cation in a Cathode structure. The user has to identify the site that is migrating, as well as provide the fractional coordinates to which the site will migrate, or a vacant site index. Args: structure_file (str): Path to the structure file. migration_indices (tuple): Tuple of the indices which designate the migrating site and the vacant site to which the cation will migrate. write_cif (bool): Flag that determines if the initial and final structures should also be written in a cif format. Returns: migration_dir (str): The absolute path to the migration directory. """ cathode = Cathode.from_file(structure_file) final_structure = cathode.copy() if migration_indices == (0, 0): # Prompt the user for the migration site print("") print(cathode) print("") migration_site_index = int( input("Please provide the index of the " "migrating cation (Note: Not the " "VESTA index!): ")) print("") migration_site = cathode.sites[migration_site_index] migration_species = migration_site.species_and_occu # Check if the site to which the ion should migrate is actually # occupied if migration_species == Composition(): raise ValueError("Chosen site is vacant.") # Ask the user for the final coordinates of the migrating ion final_coords = input("Please provide the index of the site the cation " "is migrating to, or the final fractional " "coordinates of the migration site: ") print("") final_coords = [ float(number) for number in list(final_coords.split(" ")) ] else: migration_site_index = migration_indices[0] migration_site = cathode.sites[migration_site_index] migration_species = migration_site.species_and_occu # Check if the site to which the ion should migrate is actually # occupied if migration_species == Composition(): raise ValueError("Chosen site is vacant.") final_coords = [migration_indices[1]] # In case of a site index as input if len(final_coords) == 1: # Grab the required information about the final site final_site_index = int(final_coords[0]) final_site = cathode.sites[final_site_index] final_coords = final_site.frac_coords final_species = final_site.species_and_occu # Check if site is occupied if final_species != Composition(): raise ValueError("Chosen final site is not vacant.") # Change the coordinates of the migration site with the ones of # the final site. final_structure.replace(i=migration_site_index, species=migration_species, coords=final_coords, properties=migration_site.properties) # Do the opposite for the final site final_structure.replace(i=final_site_index, species=final_species, coords=migration_site.frac_coords, properties=final_site.properties) migration_id = str(migration_site_index) + "_" + str(final_site_index) # In case of a set of fractional coordinates as input elif len(final_coords) == 3: # Replace the site with the site of the new coordinates final_structure.replace( i=migration_site_index, species=migration_species, coords=final_coords, properties=final_structure.sites[migration_site_index].properties) migration_id = str(migration_site_index) + "_a" letter_index = 0 while "migration_" + migration_id in os.listdir(os.getcwd()) and \ letter_index < len(ascii_letters): letter_index += 1 migration_id = str(migration_site_index) + "_" + \ ascii_letters[letter_index] else: raise IOError("Provided input is incorrect.") # Replace the final_structure.remove_sites([migration_site_index]) # Add the final position of the migrating ion final_structure.insert( i=migration_site_index, species=migration_species, coords=final_coords, properties=final_structure.sites[migration_site_index].properties) # Set up the migration directory migration_dir = os.path.join(os.getcwd(), "migration_" + migration_id) try: os.mkdir(migration_dir) except FileExistsError: print("WARNING: Migration directory already exists.") # Set up the filenames initial_structure_file = ".".join( structure_file.split("/")[-1].split(".") [0:-1]) + "_m_" + migration_id + "_init" final_structure_file = ".".join( structure_file.split("/")[-1].split(".") [0:-1]) + "_m_" + migration_id + "_final" # Write out the initial and final structures cathode.to("json", migration_dir + "/" + initial_structure_file + ".json") final_structure.to("json", migration_dir + "/" + final_structure_file + ".json") # Write the structures to a cif format if requested if write_cif: cathode.to("cif", migration_dir + "/" + initial_structure_file + ".cif") final_structure.to("cif", migration_dir + "/" + final_structure_file + ".cif") return migration_dir
def print_structure(structure_file): print(Cathode.from_file(structure_file))
def scf(structure_file, functional=("pbe", {}), calculation_dir="", write_chgcar=False): """ Set up a standard scf calculation. Always uses the tetrahedron method to calculate accurate total energies. Args: structure_file (str): Path to the Cathode structure file, either relative or absolute. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. calculation_dir (str): Path to the directory in which to set up the VASP calculation. write_chgcar (bool): Write out the charge Returns: str: Path to the directory in which the calculation is set up. """ # Import the structure as a Cathode instance from the structure file structure_file = os.path.abspath(structure_file) structure = Cathode.from_file(structure_file).as_ordered_structure() # Check if a magnetic moment was not provided for the sites. If not, make # sure it is zero for the calculations. if "magmom" not in structure.site_properties.keys(): structure.add_site_property("magmom", [0] * len(structure.sites)) # Set up the calculation user_incar_settings = {} # Set up the functional if functional[0] != "pbe": functional_config = _load_yaml_config(functional[0] + "Set") functional_config["INCAR"].update(functional[1]) user_incar_settings.update(functional_config["INCAR"]) # Set up the calculation directory if calculation_dir == "": calculation_dir = os.path.join(os.getcwd(), functional[0]) if functional[0] == "pbeu": calculation_dir += "_" + "".join( k + str(functional[1]["LDAUU"][k]) for k in functional[1]["LDAUU"].keys()) calculation_dir += "_scf" # Set charge density to be written if requested if write_chgcar: user_incar_settings.update({"LCHARG": True, "LAECHG": True}) # For the HSE06 calculation, also increase the FFT grids, etc... if functional[0] == "hse": user_incar_settings.update({"PRECFOCK": "Accurate"}) # Set up the BulkSCFSet scf_calculation = BulkSCFSet(structure=structure, user_incar_settings=user_incar_settings, potcar_functional=DFT_FUNCTIONAL) # Write the input files to the SCF calculation directory scf_calculation.write_input(calculation_dir) shutil.copy(structure_file, os.path.join(calculation_dir, "initial_cathode.json")) return calculation_dir
def define_dimer(structure_file, dimer_indices=(0, 0), distance=0, remove_cations=False, write_cif=False): """ Define the formation of an oxygen dimer in a Cathode structure. The user has to provide the indices of the oxygen pair that will form the dimer, as well as the final distance between the oxygen atoms. Args: structure_file (str): Path to the Cathode structure file. dimer_indices (tuple): Indices of the oxygen sites which are to form a dimer. distance (float): Final distance between the oxygen atoms. remove_cations (bool): Flag that allows the user to remove the cations (Li, Na, ...) around the chosen oxygen pair. write_cif (bool): Flag that indicates that the initial and final structure files should also be written in a cif format. Returns: dimer_dir (str): Absolute path to the dimer directory. """ cathode = Cathode.from_file(structure_file) if dimer_indices == (0, 0): print("") print("No site indices were given for structure:") print("") print(cathode) print("") dimer_indices = input("Please provide the two indices of the elements " "that need to form a dimer, separated by a " "space (Note: Not the VESTA indices!): ") dimer_indices = tuple( [int(number) for number in list(dimer_indices.split(" "))]) if distance == 0: distance = input("Please provide the final distance between the atoms " "in the dimer: ") print("") distance = float(distance) if remove_cations: # Remove the cations around the oxygen dimer cathode.remove_dimer_cations(dimer_indices) # Change the distance between the oxygen atoms for the dimer structure dimer_structure = cathode.copy() dimer_structure.change_site_distance(dimer_indices, distance) # Create the dimer directory dimer_dir = os.path.join( os.getcwd(), "dimer_" + "_".join([str(el) for el in dimer_indices])) try: os.mkdir(dimer_dir) except FileExistsError: warnings.warn("Warning: " + dimer_dir + " already exists, " "overwriting...") # Set up the filenames initial_structure_file = ".".join( structure_file.split("/")[-1].split(".")[0:-1]) + "_d_" \ + "_".join([str(el) for el in dimer_indices]) \ + "_init" dimer_structure_file = ".".join( structure_file.split("/")[-1].split(".")[0:-1]) + "_d_" \ + "_".join([str(el) for el in dimer_indices]) \ + "_final" # Write out the initial and final structures cathode.to("json", dimer_dir + "/" + initial_structure_file + ".json") dimer_structure.to("json", dimer_dir + "/" + dimer_structure_file + ".json") # Write the structures to a cif format if requested if write_cif: cathode.to("cif", dimer_dir + "/" + initial_structure_file + ".cif") dimer_structure.to("cif", dimer_dir + "/" + dimer_structure_file + ".cif") return dimer_dir
def neb(directory, nimages=7, functional=("pbe", {}), is_metal=False): """ Set up the NEB calculation from the initial and final structures. Args: directory (str): Directory in which the transition calculations should be set up. functional (tuple): Tuple with the functional choices. The first element contains a string that indicates the functional used ("pbe", "hse", ...), whereas the second element contains a dictionary that allows the user to specify the various functional tags. nimages (int): Number of images to use in the NEB calculation. is_metal (bool): Flag that indicates the material being studied is a metal, which changes the smearing from Gaussian to second order Methfessel-Paxton of 0.2 eV. Returns: None """ directory = os.path.abspath(directory) # Extract the optimized initial and final geometries initial_dir = os.path.join(directory, "initial") final_dir = os.path.join(directory, "final") try: # Check to see if the initial final_cathode.json structure is present initial_structure = Cathode.from_file( os.path.join(initial_dir, "final_cathode.json")).as_ordered_structure() except FileNotFoundError: # In case the required json file is not present, check to see if # there is VASP output which can be used initial_structure = Structure.from_file( os.path.join(initial_dir, "CONTCAR")) # Add the magnetic configuration to the initial structure, if present in the # OUTCAR initial_out = Outcar(os.path.join(initial_dir, "OUTCAR")) initial_magmom = [site["tot"] for site in initial_out.magnetization] try: initial_structure.add_site_property("magmom", initial_magmom) except ValueError: if len(initial_magmom) == 0: print( "No magnetic moments found in OUTCAR file of initial geometry. " "Setting up non-spin-polarized calculation.") else: raise ValueError("Number of magnetic moments in OUTCAR file " "do not match the number of sites!") except BaseException: raise FileNotFoundError("Could not find required structure " "information in " + initial_dir + ".") try: final_structure = Structure.from_file( os.path.join(final_dir, "CONTCAR")) except FileNotFoundError: final_structure = Cathode.from_file( os.path.join(final_dir, "final_cathode.json")).as_ordered_structure() # In case an "middle image" has been provided via which to interpolate if os.path.exists(os.path.join(directory, "middle")): print("Found a 'middle' directory in the NEB directory. Interpolating " "via middle geometry.") # Load the middle image middle_structure = Structure.from_file( os.path.join(directory, "middle", "CONTCAR")) # Perform an interpolation via this image images_1 = initial_structure.interpolate( end_structure=middle_structure, nimages=int((nimages + 1) / 2), interpolate_lattices=True) images_2 = middle_structure.interpolate(end_structure=final_structure, nimages=int((nimages) / 2 + 1), interpolate_lattices=True) images = images_1[:-1] + images_2 else: try: # Linearly interpolate the initial and final structures images = initial_structure.interpolate( end_structure=final_structure, nimages=nimages + 1, interpolate_lattices=True) except ValueError: warnings.warn( "Found a ValueError while interpolating the initial and final " "neb structures. Attempting to sort the structures and " "interpolating again. Make sure to check if the transition is " "correct.") images = initial_structure.get_sorted_structure().interpolate( end_structure=final_structure.get_sorted_structure(), nimages=nimages + 1, interpolate_lattices=True) # TODO Add functionality for NEB calculations with changing lattices user_incar_settings = {} # Set up the functional user_incar_settings.update(_load_functional(functional)) # Check if a magnetic moment was provided for the sites of the initial structure. # If so, perform a spin-polarized calculation if "magmom" in initial_structure.site_properties.keys(): user_incar_settings.update({"ISPIN": 2, "MAGMOM": True}) # Add the standard Methfessel-Paxton smearing for metals if is_metal: user_incar_settings.update({"ISMEAR": 2, "SIGMA": 0.2}) neb_calculation = PybatNEBSet(images, potcar_functional=DFT_FUNCTIONAL, user_incar_settings=user_incar_settings) # Set up the NEB calculation neb_calculation.write_input(directory) # Make a file to visualize the transition neb_calculation.visualize_transition( os.path.join(directory, "transition.cif"))