├── mpmorph.egg-info ├── dependency_links.txt ├── top_level.txt ├── PKG-INFO └── SOURCES.txt ├── mpmorph ├── __init__.py ├── analysis │ ├── __init__.py │ ├── __pycache__ │ │ ├── md_data.cpython-35.pyc │ │ ├── md_data.cpython-36.pyc │ │ ├── __init__.cpython-35.pyc │ │ ├── __init__.cpython-36.pyc │ │ ├── diffusion.cpython-35.pyc │ │ ├── clustering_analysis.cpython-35.pyc │ │ ├── clustering_analysis.cpython-36.pyc │ │ ├── environment_tracking.cpython-35.pyc │ │ ├── environment_tracking.cpython-36.pyc │ │ ├── structural_analysis.cpython-35.pyc │ │ └── structural_analysis.cpython-36.pyc │ ├── examples │ │ ├── liquid_Na │ │ │ ├── Na_df_671.0 │ │ │ │ └── run0 │ │ │ │ │ └── XDATCAR.gz │ │ │ ├── Na_df_763.5 │ │ │ │ └── run0 │ │ │ │ │ └── XDATCAR.gz │ │ │ └── Na_df_856.0 │ │ │ │ └── run0 │ │ │ │ └── XDATCAR.gz │ │ └── Example-Get_D_and_Q.ipynb │ ├── md_data.py │ ├── diffusion.py │ └── structural_analysis.py ├── runners │ ├── __init__.py │ ├── rescale_volume.py │ └── amorphous_maker.py ├── workflows │ ├── __init__.py │ ├── examples │ │ ├── H2O.xyz │ │ └── water1.py │ ├── diffusion.py │ ├── quench.py │ └── converge.py ├── database │ └── __pycache__ │ │ ├── __init__.cpython-35.pyc │ │ ├── __init__.cpython-36.pyc │ │ ├── database.cpython-35.pyc │ │ └── database.cpython-36.pyc ├── util.py ├── firetasks │ ├── glue_tasks.py │ ├── mdtasks.py │ └── dbtasks.py ├── fireworks │ ├── powerups.py │ └── core.py ├── io.py └── database.py ├── .gitignore ├── setup.py ├── LICENSE.md └── README.md /mpmorph.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mpmorph.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | mpmorph 2 | -------------------------------------------------------------------------------- /mpmorph/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0' 2 | -------------------------------------------------------------------------------- /mpmorph/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Muratahan Aykol ' 2 | -------------------------------------------------------------------------------- /mpmorph/runners/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Muratahan Aykol ' 2 | -------------------------------------------------------------------------------- /mpmorph/workflows/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Muratahan Aykol ' 2 | -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/md_data.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/md_data.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/md_data.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/md_data.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/diffusion.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/diffusion.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/database/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/database/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/database/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/database/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/database/__pycache__/database.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/database/__pycache__/database.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/database/__pycache__/database.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/database/__pycache__/database.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/clustering_analysis.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/clustering_analysis.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/clustering_analysis.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/clustering_analysis.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/environment_tracking.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/environment_tracking.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/environment_tracking.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/environment_tracking.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/structural_analysis.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/structural_analysis.cpython-35.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/__pycache__/structural_analysis.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/__pycache__/structural_analysis.cpython-36.pyc -------------------------------------------------------------------------------- /mpmorph/analysis/examples/liquid_Na/Na_df_671.0/run0/XDATCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/examples/liquid_Na/Na_df_671.0/run0/XDATCAR.gz -------------------------------------------------------------------------------- /mpmorph/analysis/examples/liquid_Na/Na_df_763.5/run0/XDATCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/examples/liquid_Na/Na_df_763.5/run0/XDATCAR.gz -------------------------------------------------------------------------------- /mpmorph/analysis/examples/liquid_Na/Na_df_856.0/run0/XDATCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mpmorph/HEAD/mpmorph/analysis/examples/liquid_Na/Na_df_856.0/run0/XDATCAR.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | mpmorph/\.idea/dictionaries/esivonxay\.xml 3 | 4 | mpmorph/\.idea/misc\.xml 5 | 6 | mpmorph/\.idea/modules\.xml 7 | 8 | mpmorph/\.idea/mpmorph\.iml 9 | 10 | mpmorph/\.idea/workspace\.xml 11 | 12 | *.pyc 13 | -------------------------------------------------------------------------------- /mpmorph/workflows/examples/H2O.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | water-molecule-from-packmol-examples 3 | H 9.625597 6.787278 12.673000 4 | H 9.625597 8.420323 12.673000 5 | O 10.203012 7.603800 12.673000 6 | -------------------------------------------------------------------------------- /mpmorph/workflows/diffusion.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Eric Sivonxay and Muratahan Aykol' 2 | __maintainer__ = 'Eric Sivonxay' 3 | __email__ = 'esivonxay@lbl.gov' 4 | 5 | 6 | def get_diffusion(structure, temperatures=[500, 1000, 1500]): 7 | # TODO 8 | return 9 | -------------------------------------------------------------------------------- /mpmorph.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: mpmorph 3 | Version: 1.0 4 | Summary: UNKNOWN 5 | Home-page: https://github.com/aykol/mpmorph 6 | Author: Muratahan Aykol 7 | Author-email: maykol@lbl.gov 8 | License: modified BSD 9 | Description: UNKNOWN 10 | Platform: UNKNOWN 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='mpmorph', 5 | version='2.0', 6 | packages=find_packages(), 7 | url='https://github.com/materialsproject/mpmorph', 8 | license='modified BSD', 9 | author='Muratahan Aykol', 10 | author_email='maykol@lbl.gov' 11 | ) 12 | -------------------------------------------------------------------------------- /mpmorph/util.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | __author__ = 'Eric Sivonxay' 4 | 5 | 6 | def recursive_update(orig_dict, new_dict): 7 | for key, val in new_dict.items(): 8 | if isinstance(val, collections.Mapping): 9 | tmp = recursive_update(orig_dict.get(key, {}), val) 10 | orig_dict[key] = tmp 11 | elif isinstance(val, list): 12 | orig_dict[key] = (orig_dict.get(key, []) + val) 13 | else: 14 | orig_dict[key] = new_dict[key] 15 | return orig_dict 16 | -------------------------------------------------------------------------------- /mpmorph.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.py 2 | mpmorph/__init__.py 3 | mpmorph.egg-info/PKG-INFO 4 | mpmorph.egg-info/SOURCES.txt 5 | mpmorph.egg-info/dependency_links.txt 6 | mpmorph.egg-info/top_level.txt 7 | mpmorph/analysis/__init__.py 8 | mpmorph/analysis/clustering_analysis.py 9 | mpmorph/analysis/diffusion.py 10 | mpmorph/analysis/environment_tracking.py 11 | mpmorph/analysis/md_data.py 12 | mpmorph/analysis/rings_analysis.py 13 | mpmorph/analysis/structural_analysis.py 14 | mpmorph/runners/__init__.py 15 | mpmorph/runners/amorphous_maker.py 16 | mpmorph/runners/rescale_volume.py 17 | mpmorph/workflow/Test_WF.py 18 | mpmorph/workflow/__init__.py 19 | mpmorph/workflow/mdtasks.py 20 | mpmorph/workflow/workflows.py -------------------------------------------------------------------------------- /mpmorph/workflows/examples/water1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example generates a dynamic AIMD workflow for finding equilibrium density for liquid water at 320 K. 3 | """ 4 | 5 | # Parameters: 6 | box_scale = 8.9 # edge length of MD box in Angstroms, can also be a numpy array that scales the lattice 7 | packmol_path = "~/packmol/packmol/packmol" # Revise as appropriate 8 | structure = {'H2O': 20} # "structure" in this context can be a dict of number of atoms or molecules. 9 | temperature = 320 10 | 11 | # Note one can use a pymatgen Structure object also 12 | # E.g. p = Poscar.from_file("POSCAR") 13 | # structure = p.structure 14 | 15 | copy_calcs = True # MD runs can be backed up in a desired location 16 | calc_home = '~/test_H2O_wflows' # This is the location to copy the calculations if copy_calcs=True 17 | 18 | # Since we specified a molecule, we must also give the path to xyz 19 | # file of a single sample molecule. 20 | xyz_paths = ['H2O.xyz'] 21 | name = 'H2O_df_' + str(temperature) 22 | 23 | from fireworks import LaunchPad 24 | from mpmorph.workflows.old_workflows import get_wf_density 25 | 26 | amorphous_maker_params = {'box_scale': box_scale, 'packmol_path': packmol_path, 'xyz_paths': xyz_paths, 'tol': 2.0} 27 | 28 | wf = get_wf_density(structure, temperature=temperature, pressure_threshold=0.5, nsteps=1000, wall_time=19200, 29 | max_rescales=5, 30 | amorphous_maker_params=amorphous_maker_params, copy_calcs=copy_calcs, calc_home=calc_home, 31 | name=name) 32 | 33 | lp = LaunchPad.auto_load() 34 | lp.add_wf(wf) 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MPmorph Copyright (c) 2015, The Regents of the University of 2 | California, through Lawrence Berkeley National Laboratory (subject 3 | to receipt of any required approvals from the U.S. Dept. of Energy). 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | (1) Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | (2) Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided with 16 | the distribution. 17 | 18 | (3) Neither the name of the University of California, Lawrence 19 | Berkeley National Laboratory, U.S. Dept. of Energy nor the names of 20 | its contributors may be used to endorse or promote products derived 21 | from this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | POSSIBILITY OF SUCH DAMAGE. 35 | 36 | You are under no obligation whatsoever to provide any bug fixes, 37 | patches, or upgrades to the features, functionality or performance 38 | of the source code ("Enhancements") to anyone; however, if you 39 | choose to make your Enhancements available either publicly, or 40 | directly to Lawrence Berkeley National Laboratory or its 41 | contributors, without imposing a separate written license agreement 42 | for such Enhancements, then you hereby grant the following license: 43 | a non-exclusive, royalty-free perpetual license to install, use, 44 | modify, prepare derivative works, incorporate into other computer 45 | software, distribute, and sublicense such enhancements or derivative 46 | works thereof, in binary and source code form. -------------------------------------------------------------------------------- /mpmorph/firetasks/glue_tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from fireworks import explicit_serialize, FireTaskBase, FWAction 5 | from mpmorph.analysis import md_data 6 | from pymatgen.core import Structure 7 | from pymatgen.io.vasp import Poscar 8 | 9 | __author__ = 'Eric Sivonxay ' 10 | 11 | 12 | @explicit_serialize 13 | class PreviousStructureTask(FireTaskBase): 14 | required_params = [] 15 | optional_params = ["rescale_volume"] 16 | 17 | def run_task(self, fw_spec): 18 | structure_dict = fw_spec["structure"] 19 | 20 | if self.get("rescale_volume", False): 21 | spec_structure = Structure.from_dict(structure_dict) 22 | scaling_volume = spec_structure.volume * self["rescale_volume"] 23 | 24 | spec_structure.scale_lattice(scaling_volume) 25 | structure_dict = spec_structure.as_dict() 26 | 27 | _poscar = Poscar(structure_dict) 28 | _poscar.write_file("POSCAR") 29 | return FWAction() 30 | 31 | 32 | @explicit_serialize 33 | class SaveStructureTask(FireTaskBase): 34 | required_params = [] 35 | optional_params = [] 36 | 37 | def run_task(self, fw_spec): 38 | osw = list(os.walk("."))[0] 39 | files = [] 40 | 41 | for file_name in osw[2]: 42 | if "CONTCAR" in file_name: 43 | files.append(file_name) 44 | 45 | _poscar = Poscar.from_file(filename=files[-1], check_for_POTCAR=True, read_velocities=True) 46 | _structure = _poscar.structure.as_dict() 47 | 48 | return FWAction(update_spec={"structure": _structure}) 49 | 50 | 51 | @explicit_serialize 52 | class PassPVTask(FireTaskBase): 53 | required_params = [] 54 | optional_params = [] 55 | 56 | def run_task(self, fw_spec): 57 | pressure_volume = fw_spec.get('pressure_volume', []) 58 | 59 | # get volume 60 | osw = list(os.walk("."))[0] 61 | files = [] 62 | for file_name in osw[2]: 63 | if "CONTCAR" in file_name: 64 | files.append(file_name) 65 | _poscar = Poscar.from_file(filename=files[-1], check_for_POTCAR=True, read_velocities=True) 66 | volume = _poscar.structure.volume 67 | 68 | # get pressure 69 | search_keys = ['external'] 70 | outcar_data = md_data.get_MD_data("./OUTCAR.gz", search_keys=search_keys) 71 | 72 | _data = np.transpose(outcar_data)[0] 73 | pressure = np.mean(_data[int(0.5 * (len(_data) - 1)):]) 74 | 75 | pressure_volume.append((volume, pressure)) 76 | return FWAction(mod_spec={'_push_all': {'pressure_volume': pressure_volume}}) 77 | -------------------------------------------------------------------------------- /mpmorph/fireworks/powerups.py: -------------------------------------------------------------------------------- 1 | from mpmorph.firetasks.dbtasks import VaspMDToDb, TrajectoryDBTask 2 | from mpmorph.firetasks.glue_tasks import PreviousStructureTask, SaveStructureTask, \ 3 | PassPVTask 4 | from mpmorph.firetasks.mdtasks import RescaleVolumeTask, ConvergeTask, PVRescaleTask, \ 5 | DiffusionTask 6 | 7 | __author__ = 'Eric Sivonxay and Jianli Cheng' 8 | __maintainer__ = "Eric Sivonxay" 9 | __email__ = "esivonxay@lbl.gov" 10 | 11 | 12 | def add_diffusion_task(fw, **kwargs): 13 | spawner_task = DiffusionTask(**kwargs) 14 | fw.tasks.append(spawner_task) 15 | return fw 16 | 17 | 18 | def add_converge_task(fw, **kwargs): 19 | """ 20 | This powerup adds the convergence task onto a MD firework, which turns a workflow into a dynamic workflow. 21 | This firetask will check to ensure the specified parameters (Usually pressure and Energy) are converged within the 22 | specified thresholds. 23 | :param fw: 24 | :param kwargs: 25 | :return: 26 | """ 27 | spawner_task = ConvergeTask(**kwargs) 28 | fw.tasks.append(spawner_task) 29 | return fw 30 | 31 | 32 | def aggregate_trajectory(fw, **kwargs): 33 | """ 34 | This firetask will add a task which converts a series of MD runs into a trajectory object 35 | :param fw: 36 | :param kwargs: 37 | :return: 38 | """ 39 | fw.tasks.append(TrajectoryDBTask(**kwargs)) 40 | return fw 41 | 42 | 43 | def add_cont_structure(fw): 44 | prev_struct_task = PreviousStructureTask() 45 | insert_i = 2 46 | for (i, task) in enumerate(fw.tasks): 47 | if task.fw_name == "{{atomate.vasp.firetasks.run_calc.RunVaspCustodian}}": 48 | insert_i = i 49 | break 50 | fw.tasks.insert(insert_i, prev_struct_task) 51 | return fw 52 | 53 | 54 | def add_pass_structure(fw, **kwargs): 55 | save_struct_task = SaveStructureTask(**kwargs) 56 | fw.tasks.append(save_struct_task) 57 | return fw 58 | 59 | 60 | def add_pass_pv(fw, **kwargs): 61 | pass_pv_task = PassPVTask(**kwargs) 62 | fw.tasks.append(pass_pv_task) 63 | return fw 64 | 65 | 66 | def add_pv_volume_rescale(fw): 67 | insert_i = 2 68 | for (i, task) in enumerate(fw.tasks): 69 | if task.fw_name == "{{atomate.vasp.firetasks.run_calc.RunVaspCustodian}}": 70 | insert_i = i 71 | break 72 | 73 | fw.tasks.insert(insert_i, PVRescaleTask()) 74 | return fw 75 | 76 | 77 | def add_rescale_volume(fw, **kwargs): 78 | rsv_task = RescaleVolumeTask(**kwargs) 79 | insert_i = 2 80 | for (i, task) in enumerate(fw.tasks): 81 | if task.fw_name == "{{atomate.vasp.firetasks.run_calc.RunVaspCustodian}}": 82 | insert_i = i 83 | break 84 | 85 | fw.tasks.insert(insert_i, rsv_task) 86 | return fw 87 | 88 | 89 | def replace_pass_structure(fw, **kwargs): 90 | # look for rescale_volume task 91 | replaced = False 92 | fw_dict = fw.to_dict() 93 | for i in range(len(fw_dict['spec']['_tasks'])): 94 | if fw_dict['spec']['_tasks'][i]["_fw_name"] == '{{mpmorph.firetasks.glue_tasks.SaveStructureTask}}': 95 | del fw_dict['spec']['_tasks'][i]["_fw_name"] 96 | fw.tasks[i] = SaveStructureTask(**kwargs) 97 | replaced = True 98 | break 99 | # TODO: Replace with real error handling 100 | if replaced == False: 101 | print("error, no SaveStructureTask to replace") 102 | return 103 | 104 | return fw 105 | 106 | 107 | def replace_vaspmdtodb(fw): 108 | # look for vaspdb task 109 | replaced = False 110 | fw_dict = fw.to_dict() 111 | for i in range(len(fw_dict['spec']['_tasks'])): 112 | if fw_dict['spec']['_tasks'][i]["_fw_name"] == '{{atomate.vasp.firetasks.parse_outputs.VaspToDb}}': 113 | del fw_dict['spec']['_tasks'][i]["_fw_name"] 114 | fw.tasks[i] = VaspMDToDb(**fw_dict['spec']['_tasks'][i]) 115 | replaced = True 116 | break 117 | # TODO: Replace with real error handling 118 | if replaced == False: 119 | print("error, no vasptodb to replace") 120 | return 121 | 122 | return fw 123 | -------------------------------------------------------------------------------- /mpmorph/io.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from monty.io import zopen 4 | 5 | __author__ = 'Eric Sivonxay' 6 | 7 | 8 | class Xdatcar_Writer(): 9 | def write_xdatcar(self, filename, **kwargs): 10 | """ 11 | Writes Xdatcar to a file. The supported kwargs are the same as those for 12 | the Xdatcar_from_structs.get_string method and are passed through directly. 13 | """ 14 | with zopen(filename, "wt") as f: 15 | f.write(self.get_string_from_struct(**kwargs)) 16 | 17 | def get_string_from_struct(self, structures, system="unknown system", significant_figures=6): 18 | format_str = "{{:.{0}f}}".format(significant_figures) 19 | 20 | for (si, structure) in enumerate(structures): 21 | lines = [system, "1.0", str(structure.lattice)] 22 | lines.append(" ".join(self.get_site_symbols(structure))) 23 | lines.append(" ".join([str(x) for x in self.get_natoms(structure)])) 24 | 25 | lines.append("Direct configuration= " + str(si + 1)) 26 | for (i, site) in enumerate(structure): 27 | coords = site.frac_coords 28 | line = " ".join([format_str.format(c) for c in coords]) 29 | line += " " + site.species_string 30 | lines.append(line) 31 | 32 | return "\n".join(lines) + "\n" 33 | 34 | def get_site_symbols(self, structure): 35 | """ 36 | Sequence of symbols associated with the Poscar. Similar to 6th line in 37 | vasp 5+ POSCAR. 38 | """ 39 | syms = [site.specie.symbol for site in structure] 40 | return [a[0] for a in itertools.groupby(syms)] 41 | 42 | def get_natoms(self, structure): 43 | """ 44 | Sequence of number of sites of each type associated with the Poscar. 45 | Similar to 7th line in vasp 5+ POSCAR or the 6th line in vasp 4 POSCAR. 46 | """ 47 | syms = [site.specie.symbol for site in structure] 48 | return [len(tuple(a[1])) for a in itertools.groupby(syms)] 49 | 50 | 51 | class Xdatcar_Writer_Trajectory(): 52 | def __init__(self, trajectory): 53 | self.trajectory = trajectory 54 | 55 | def write_xdatcar(self, filename, **kwargs): 56 | """ 57 | Writes Xdatcar to a file. The supported kwargs are the same as those for 58 | the Xdatcar_from_structs.get_string method and are passed through directly. 59 | """ 60 | with zopen(filename, "wt") as f: 61 | f.write(self.get_string(**kwargs)) 62 | 63 | def get_string(self, system="unknown system", significant_figures=6): 64 | lines = [system, "1.0"] 65 | for direction in self.trajectory.lattice: 66 | lines.append(' '.join([str(i) for i in direction])) 67 | lines.append(" ".join(self.get_site_symbols())) 68 | lines.append(" ".join([str(x) for x in self.get_natoms()])) 69 | 70 | format_str = "{{:.{0}f}}".format(significant_figures) 71 | positions = self.trajectory.frac_coords 72 | # positions = np.add(self.trajectory[0].frac_coords, self.trajectory.displacements) 73 | atoms = [site.specie.symbol for site in self.trajectory[0]] 74 | 75 | for (si, position_array) in enumerate(positions): 76 | lines.append("Direct configuration= " + str(si + 1)) 77 | for (i, coords) in enumerate(position_array): 78 | line = " ".join([format_str.format(c) for c in coords]) 79 | line += " " + atoms[i] 80 | lines.append(line) 81 | 82 | return "\n".join(lines) + "\n" 83 | 84 | def get_site_symbols(self): 85 | """ 86 | Sequence of symbols associated with the Poscar. Similar to 6th line in 87 | vasp 5+ POSCAR. 88 | """ 89 | syms = [site.specie.symbol for site in self.trajectory[0]] 90 | return [a[0] for a in itertools.groupby(syms)] 91 | 92 | def get_natoms(self): 93 | """ 94 | Sequence of number of sites of each type associated with the Poscar. 95 | Similar to 7th line in vasp 5+ POSCAR or the 6th line in vasp 4 POSCAR. 96 | """ 97 | syms = [site.specie.symbol for site in self.trajectory[0]] 98 | return [len(tuple(a[1])) for a in itertools.groupby(syms)] 99 | -------------------------------------------------------------------------------- /mpmorph/analysis/md_data.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import os 3 | import re 4 | import shutil 5 | 6 | import numpy as np 7 | 8 | __author__ = 'Muratahan Aykol ' 9 | 10 | 11 | def get_MD_data(outcar_path, search_keys=None, search_data_column=None): 12 | ''' 13 | Extracts the pressure, kinetic energy and total energy data from 14 | VASP MD OUTCAR. 15 | 16 | Args: 17 | outcar_path: 18 | search_keys: 19 | search_keys: 20 | search_data_column: 21 | - outcar_path = path to OUTCAR to be parsed 22 | Returns: 23 | - A nested list of MD steps where each search key value is 24 | listed. 25 | ''' 26 | # Initial map of keywords to serach for and data to map out from that line in OUTCAR 27 | # search_keys = ['external', 'kinetic energy EKIN', 'ETOTAL'] 28 | # index of stripped column of data in that line, starts from 0 29 | # search_data_column = [3, 4, 4] 30 | if search_data_column is None: 31 | search_data_column = [3, 4, 4, 4] 32 | if search_keys is None: 33 | search_keys = ['external', 'kinetic energy EKIN', '% ion-electron', 'ETOTAL'] 34 | 35 | if "OUTCAR.gz" in outcar_path: 36 | with gzip.open(outcar_path, 'rb') as f_in: 37 | with open(outcar_path[:-3], 'wb') as f_out: 38 | shutil.copyfileobj(f_in, f_out) 39 | outcar_path = outcar_path[:-3] 40 | 41 | outcar = open(outcar_path) 42 | print("OUTCAR opened") 43 | data_list = [] 44 | md_step = 0 45 | print(search_keys) 46 | for line in outcar: 47 | line = line.rstrip() 48 | for key_index in range(len(search_keys)): 49 | if re.search(search_keys[key_index], line): 50 | if key_index == 0: 51 | data_list.append([[]] * len(search_keys)) 52 | data_list[md_step][0] = float(line.split()[search_data_column[key_index]]) 53 | else: 54 | try: 55 | data_list[md_step][key_index] = float(line.split()[search_data_column[key_index]]) 56 | except IndexError: 57 | break 58 | if key_index == len(search_keys) - 1: 59 | md_step += 1 60 | print("Requested information parsed.") 61 | outcar.close() 62 | return data_list 63 | 64 | 65 | def autocorrelation(data_list, search_keys=None, skip_first=0): 66 | """ 67 | TODO 68 | Args: 69 | data_list: 70 | 71 | Returns: 72 | Autocorrelation function of the external pressure 73 | 74 | """ 75 | if search_keys is None: 76 | search_keys = ['external', 'kinetic energy EKIN', '% ion-electron', 'ETOTAL'] 77 | pressures = [x[search_keys.index('external')] for x in data_list][skip_first:] 78 | pres_fluc = pressures - np.mean(pressures) 79 | correlation = np.zeros(len(pressures) - 1) 80 | for i in range(0, len(pressures) - 1): 81 | _starts = pres_fluc[np.arange(start=0, stop=len(pressures) - i - 1)] 82 | _ends = pres_fluc[np.arange(start=i, stop=len(pressures) - 1)] 83 | correlation[i] = np.mean(np.multiply(_starts, _ends)) 84 | return correlation 85 | 86 | 87 | def get_correlation_time(data_list, skip_first=0): 88 | """ 89 | Args: 90 | data_list: 91 | Returns: Correlation time in steps 92 | """ 93 | autocorr = autocorrelation(data_list, skip_first=skip_first) 94 | for i in range(len(autocorr)): 95 | if autocorr[i] <= 0: 96 | return i 97 | raise ReferenceError('Simulation too short') 98 | 99 | 100 | def get_MD_stats(data_list): 101 | """ 102 | Args: data_list is the list of MD data returned by get_MD_data 103 | Returns: means and standard deviations 104 | """ 105 | data_list = np.array(data_list) 106 | stats = [] 107 | for col in range(data_list.shape[1]): 108 | data_col = data_list[:, col] 109 | stats.append((np.mean(data_col), np.std(data_col))) 110 | return stats 111 | 112 | 113 | def parse_pressure(path, averaging_fraction=0.5): 114 | os.system("grep external " + path + "/OUTCAR | awk '{print $4}' > " + path + "/pres") 115 | os.system("grep volume/ion " + path + "/OUTCAR | awk '{print $5}' > " + path + "/vol") 116 | if os.path.isfile(path + "/OUTCAR"): 117 | with open(path + "/pres") as f: 118 | p = [float(line.rstrip()) for line in f] 119 | with open(path + "/vol") as f: 120 | vol = [float(line.rstrip()) for line in f][0] 121 | pressure = np.array(p) 122 | avg_pres = np.mean(pressure[int(averaging_fraction * (len(pressure) - 1)):]) 123 | else: 124 | raise ValueError("No OUTCAR found.") 125 | return avg_pres, vol, pressure 126 | 127 | 128 | def plot_md_data(data_list): 129 | """ 130 | 131 | :param data_list: 132 | :return: matplotlib plt object 133 | """ 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MPmorph 2 | 3 | MPmorph is a collection of tools to run and analyze ab-initio molecular dynamics (AIMD) calculations run with VASP, 4 | and is currently under development. 5 | It relies heavily on tools developed by the Materials Project ([pymatgen](http://www.pymatgen.org), 6 | [custodian](https://github.com/materialsproject/custodian), 7 | [fireworks](https://github.com/materialsproject/fireworks)) and [atomate](https://github.com/hackingmaterials/atomate). 8 | 9 | MPmorph provides: 10 | * Infrastructure for dynamic VASP MD workflows: 11 | * Tools to create dynamic MD workflows using atomate 12 | * E.g. generation of new MD runs based on the given criterion (currently pressure, for liquid/amorphous phase density estimation) 13 | * Tools for statistical analysis of: 14 | * Static observables: 15 | * Radial distribution functions 16 | * Coordination numbers 17 | * Voronoi analysis 18 | * Polyhedra connectivities 19 | * Thermodynamic quantities 20 | * Diffusion coefficients: 21 | * Robust calculation of diffusion coefficients (D) and activation energies (Q). 22 | * Rigorous error analysis for D and Q. 23 | 24 | # Installation 25 | 26 | Before installing mpmorph, install the latest version of [pymatgen](http://www.pymatgen.org), 27 | [custodian](https://github.com/materialsproject/custodian), 28 | [fireworks](https://github.com/materialsproject/fireworks)) and [atomate](https://github.com/hackingmaterials/atomate) 29 | 30 | clone the repository to your computer and install using 31 | ```bash 32 | python setup.py develop 33 | ``` 34 | 35 | If you wish to make amorphous structures, please install [packmol](http://m3g.iqm.unicamp.br/packmol/home.shtml) on your machine and add the following line to your bash profile. 36 | ```bash 37 | export PACKMOL_PATH="path_to_packmol_executable_here" 38 | ``` 39 | 40 | # Using MPmorph 41 | Before diving headfirst into mpmorph, one should get familiar with how to run fireworks before jumping into mpmorph. Relevant features of fireworks include setting up a fireworks database, adding workflows to the database, configuring a conda environment for running fireworks on a supercomputer, launching jobs to a workload manager (using qlaunch), and monitoring fireworks jobs. For a quick tutorial, check out [beginner](https://www.youtube.com/watch?v=-MalOMJt34U) and [intermediate](https://www.youtube.com/watch?v=zYA_BbKwVO4) atomate/fireworks tutorials from our 2019 Materials project workshop. 42 | 43 | A sample of using mpmorph to run an AIMD simulation at 1500K for 200ps (100k steps at 2fs/step) is shown below: 44 | 45 | ```python 46 | 47 | from mpmorph.workflows.converge import get_converge_wf 48 | from pymatgen.ext.matproj import MPRester 49 | 50 | from fireworks import LaunchPad 51 | 52 | mpr = MPRester() 53 | structure = mpr.get_structure_by_material_id('mp-1143') 54 | structure.make_supercell([3, 3, 3]) 55 | 56 | wf = get_converge_wf(structure, temperature = 750, target_steps = 100000) 57 | 58 | lp = LaunchPad.auto_load() 59 | lp.add_wf(wf) 60 | ``` 61 | 62 | To generate an amorphous structure, run the following code: 63 | 64 | ```python 65 | from mpmorph.runners.amorphous_maker import get_random_packed 66 | from mpmorph.workflows.converge import get_converge_wf 67 | from fireworks import LaunchPad 68 | 69 | structure = get_random_packed('Li', target_atoms=100) 70 | 71 | wf = get_converge_wf(structure, temperature = 5000, target_steps = 10000) 72 | 73 | lp = LaunchPad.auto_load() 74 | lp.add_wf(wf) 75 | ``` 76 | 77 | ## Customizing runs 78 | At the simplest level of customizing the workflow, one can change the temperature, total number of steps by changing the args passed to get_converge_wf(). 79 | 80 | For more advanced changes, pass a dictionary for "converge_args", "spawner_args", or "prod_args". This allows customization of vasp inputs, starting and ending temperatures, number of steps, and convergence crieteria. 81 | 82 | ### Examples: 83 | 84 | Changing the rescale parameter, to make for larger volume changes at each stage: 85 | ```python 86 | from mpmorph.runners.amorphous_maker import get_random_packed 87 | from mpmorph.workflows.converge import get_converge_wf 88 | from fireworks import LaunchPad 89 | 90 | structure = get_random_packed('Li', target_atoms=100) 91 | 92 | spawner_args = {'rescale_params': {'beta': 5e-6}} 93 | wf = get_converge_wf(structure, temperature = 5000, target_steps = 10000, spawner_args=spawner_args) 94 | 95 | lp = LaunchPad.auto_load() 96 | lp.add_wf(wf) 97 | ``` 98 | 99 | Changing precision of production runs to PREC=Low: 100 | ```python 101 | from mpmorph.runners.amorphous_maker import get_random_packed 102 | from mpmorph.workflows.converge import get_converge_wf 103 | from fireworks import LaunchPad 104 | 105 | structure = get_random_packed('Li', target_atoms=100) 106 | 107 | prod_args = {'optional_fw_params': {'override_default_vasp_params': {'user_incar_settings': {'PREC': 'Normal'}}}} 108 | wf = get_converge_wf(structure, temperature = 5000, target_steps = 10000, prod_args = prod_args) 109 | 110 | lp = LaunchPad.auto_load() 111 | lp.add_wf(wf) 112 | ``` 113 | 114 | 115 | # Citation 116 | 117 | If you use mpmorph, please cite the following papers: 118 | * Aykol, M., Dwaraknath, S.S., Sun, W. and Persson, K.A., 2018. Thermodynamic limit for synthesis of metastable inorganic materials. Science advances, 4(4), p.eaaq0148. 119 | * Aykol, M. and Persson, K.A., 2018. Oxidation Protection with Amorphous Surface Oxides: Thermodynamic Insights from Ab Initio Simulations on Aluminum. ACS applied materials & interfaces, 10(3), pp.3039-3045. 120 | -------------------------------------------------------------------------------- /mpmorph/workflows/quench.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from fireworks import Workflow 3 | from mpmorph.fireworks import powerups 4 | from mpmorph.fireworks.core import StaticFW, MDFW, OptimizeFW 5 | from mpmorph.util import recursive_update 6 | 7 | __author__ = 'Eric Sivonxay and Muratahan Aykol' 8 | __maintainer__ = 'Eric Sivonxay' 9 | __email__ = 'esivonxay@lbl.gov' 10 | 11 | 12 | def get_quench_wf(structures, priority=None, quench_type="slow_quench", 13 | descriptor="", **kwargs): 14 | """ 15 | 16 | Args: 17 | structure: Starting structure for the run 18 | priority: Priority of all fireworks in the workflows 19 | quench_type: use "slow_quench" for a gradual decrease in temperature or 20 | "mp_quench" for a instantaneous DFT relaxation 21 | target_steps: Target number of steps for production MD run 22 | descriptor: Extra description to add to the name of the firework 23 | **kwargs: Arguments such as cool_args, hold_args, quench_args, etc. Cool_args and hold args are only applicable 24 | when using "slow_quench" 25 | 26 | Returns: Workflow object 27 | 28 | """ 29 | 30 | fw_list = [] 31 | temperatures = kwargs.get('temperatures', {"start_temp": 3000, "end_temp": 500, "temp_step": 500}) 32 | cool_args = kwargs.get('cool_args', {"md_params": {"nsteps": 200}}) 33 | hold_args = kwargs.get('hold_args', {"md_params": {"nsteps": 500}}) 34 | quench_args = kwargs.get('quench_args', {}) 35 | 36 | for (i, structure) in enumerate(structures): 37 | _fw_list = [] 38 | if quench_type == "slow_quench": 39 | for temp in np.arange(temperatures["start_temp"], temperatures["end_temp"], -temperatures["temp_step"]): 40 | # get fw for cool step 41 | use_prev_structure = False 42 | if len(_fw_list) > 0: 43 | use_prev_structure = True 44 | _fw = get_MDFW(structure, temp, temp - temperatures["temp_step"], 45 | name="snap_" + str(i) + "_cool_" + str(temp - temperatures["temp_step"]), 46 | args=cool_args, parents=[_fw_list[-1]] if len(_fw_list) > 0 else [], 47 | priority=priority, previous_structure=use_prev_structure, 48 | insert_db=True, **kwargs) 49 | _fw_list.append(_fw) 50 | # get fw for hold step 51 | _fw = get_MDFW(structure, temp - temperatures["temp_step"], temp - temperatures["temp_step"], 52 | name="snap_" + str(i) + "_hold_" + str(temp - temperatures["temp_step"]), 53 | args=hold_args, parents=[_fw_list[-1]], priority=priority, 54 | previous_structure=True, insert_db=True, **kwargs) 55 | _fw_list.append(_fw) 56 | 57 | if quench_type in ["slow_quench", "mp_quench"]: 58 | # Relax OptimizeFW and StaticFW 59 | run_args = {"run_specs": {"vasp_input_set": None, "vasp_cmd": ">>vasp_cmd<<", 60 | "db_file": ">>db_file<<", 61 | "spec": {"_priority": priority} 62 | }, 63 | "optional_fw_params": {"override_default_vasp_params": {}} 64 | } 65 | run_args = recursive_update(run_args, quench_args) 66 | _name = "snap_" + str(i) 67 | 68 | use_prev_structure = True if len(_fw_list) > 0 else False 69 | fw1 = OptimizeFW(structure=structure, name=f'{_name}{descriptor}_optimize', 70 | parents=[_fw_list[-1]] if len(_fw_list) > 0 else [], 71 | previous_structure=use_prev_structure, 72 | **run_args["run_specs"], **run_args["optional_fw_params"], 73 | max_force_threshold=None) 74 | 75 | 76 | fw2 = StaticFW(structure=structure, name=f'{_name}{descriptor}_static', 77 | parents=[fw1], previous_structure=True, 78 | **run_args["run_specs"], 79 | **run_args["optional_fw_params"]) 80 | 81 | _fw_list.extend([fw1, fw2]) 82 | 83 | fw_list.extend(_fw_list) 84 | 85 | name = structure.composition.reduced_formula + descriptor + "_quench" 86 | wf = Workflow(fw_list, name=name) 87 | return wf 88 | 89 | 90 | def get_MDFW(structure, start_temp, end_temp, name="molecular dynamics", priority=None, args={}, 91 | **kwargs): 92 | """ 93 | 94 | Helper function to get molecular dynamics firework for quench workflow 95 | 96 | Args: 97 | structure: Initial structure for molecular dynamics run 98 | start_temp: Starting Temperature 99 | end_temp: Ending Temperature 100 | name: name of firework 101 | priority: priority of job in database 102 | args: custom arguments dictionary for molecular dynamics run 103 | kwargs: kwargs for MDFW 104 | 105 | Returns: Molecular Dynamics Firework 106 | 107 | """ 108 | # Get customized firework 109 | run_args = {"md_params": {"nsteps": 500, "start_temp": start_temp, "end_temp": end_temp}, 110 | "run_specs": {"vasp_input_set": None, "vasp_cmd": ">>vasp_cmd<<", "db_file": ">>db_file<<", 111 | "wall_time": 40000}, 112 | "optional_fw_params": {"override_default_vasp_params": {}, 113 | "spec": {'_priority': priority}}} 114 | 115 | run_args["optional_fw_params"]["override_default_vasp_params"].update( 116 | {'user_incar_settings': {'ISIF': 1, 'LWAVE': False, 'PREC': 'Low'}}) 117 | run_args = recursive_update(run_args, args) 118 | _mdfw = MDFW(structure=structure, name=name, **run_args["md_params"], 119 | **run_args["run_specs"], **run_args["optional_fw_params"], **kwargs) 120 | return _mdfw 121 | -------------------------------------------------------------------------------- /mpmorph/database.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, unicode_literals, absolute_import 2 | 3 | import json 4 | import zlib 5 | 6 | import gridfs 7 | from atomate.utils.utils import get_logger 8 | from atomate.vasp.database import VaspCalcDb 9 | from monty.json import MontyEncoder 10 | from pymatgen.core import Structure 11 | from pymatgen.core.trajectory import Trajectory 12 | from collections import defaultdict 13 | import numpy as np 14 | 15 | logger = get_logger(__name__) 16 | 17 | __author__ = 'Eric Sivonxay ' 18 | 19 | 20 | class VaspMDCalcDb(VaspCalcDb): 21 | """ 22 | Adapted from atomate.vasp.database 23 | 24 | Class to help manage database insertions of Vasp drones 25 | """ 26 | 27 | def __init__(self, host="localhost", port=27017, database="vasp", collection="tasks", user=None, 28 | password=None, **kwargs): 29 | super(VaspMDCalcDb, self).__init__(host, port, database, collection, user, password, **kwargs) 30 | 31 | def insert_task(self, task_doc, parse_dos=False, parse_bs=False, parse_ionic_steps=False): 32 | """ 33 | Inserts a task document (e.g., as returned by Drone.assimilate()) into the database. 34 | Handles putting DOS, band structure, and ionic_steps into GridFS as needed. 35 | Args: 36 | task_doc: (dict) the task document 37 | parse_dos: (bool) attempt to parse dos in task_doc and insert into Gridfs 38 | parse_bs: (bool) attempt to parse bandstructure in task_doc and insert into Gridfs 39 | parse_ionic_steps: (bool) attempt to parse ionic steps in task_doc and insert into Gridfs 40 | Returns: 41 | (int) - task_id of inserted document 42 | """ 43 | 44 | # insert dos into GridFS 45 | if parse_dos and "calcs_reversed" in task_doc: 46 | if "dos" in task_doc["calcs_reversed"][0]: # only store idx=0 DOS 47 | dos = json.dumps(task_doc["calcs_reversed"][0]["dos"], cls=MontyEncoder) 48 | gfs_id, compression_type = self.insert_gridfs(dos, "dos_fs") 49 | task_doc["calcs_reversed"][0]["dos_compression"] = compression_type 50 | task_doc["calcs_reversed"][0]["dos_fs_id"] = gfs_id 51 | del task_doc["calcs_reversed"][0]["dos"] 52 | 53 | # insert band structure into GridFS 54 | if parse_bs and "calcs_reversed" in task_doc: 55 | if "bandstructure" in task_doc["calcs_reversed"][0]: # only store idx=0 BS 56 | bs = json.dumps(task_doc["calcs_reversed"][0]["bandstructure"], cls=MontyEncoder) 57 | gfs_id, compression_type = self.insert_gridfs(bs, "bandstructure_fs") 58 | task_doc["calcs_reversed"][0]["bandstructure_compression"] = compression_type 59 | task_doc["calcs_reversed"][0]["bandstructure_fs_id"] = gfs_id 60 | del task_doc["calcs_reversed"][0]["bandstructure"] 61 | 62 | # insert structures at each ionic step into GridFS 63 | if parse_ionic_steps and "calcs_reversed" in task_doc: 64 | 65 | # Convert from ionic steps dictionary to pymatgen.core.trajectory.Trajectory object 66 | ionic_steps_dict = task_doc["calcs_reversed"][0]['output']['ionic_steps'] 67 | time_step = task_doc['input']['incar']['POTIM'] 68 | trajectory = convert_ionic_steps_to_trajectory((ionic_steps_dict), time_step) 69 | del task_doc["calcs_reversed"][0]['output']['ionic_steps'] 70 | 71 | traj_dict = json.dumps(trajectory, cls=MontyEncoder) 72 | gfs_id, compression_type = self.insert_gridfs(traj_dict, "trajectories_fs") 73 | 74 | task_doc['trajectory'] = { 75 | 'formula_pretty': trajectory[0].composition.reduced_formula, 76 | 'formula': trajectory[0].composition.formula.replace(' ', ''), 77 | 'temperature': int(task_doc["input"]["incar"]["TEBEG"]), 78 | 'compression': compression_type, 79 | 'fs_id': gfs_id, 80 | 'fs': 'trajectories_fs', 81 | 'dimension': list(np.shape(trajectory.frac_coords)), 82 | 'time_step': task_doc["input"]["incar"]["POTIM"], 83 | 'frame_properties': list(trajectory.frame_properties.keys()) 84 | } 85 | 86 | # insert the task document and return task_id 87 | return self.insert(task_doc) 88 | 89 | def convert_ionic_steps_to_trajectory(ionic_steps_dict, time_step): 90 | ## Convert from a list of dictionaries to a dictionary of lists 91 | ionic_steps_defaultdict = defaultdict(list) 92 | for d in ionic_steps_dict: 93 | for key, val in d.items(): 94 | ionic_steps_defaultdict[key].append(val) 95 | ionic_steps = dict(ionic_steps_defaultdict.items()) 96 | 97 | frac_coords = [] 98 | site_properties = [] 99 | read_site_props = False 100 | if 'properties' in ionic_steps_dict[0]['structure']['sites'][0].keys(): 101 | read_site_props = True 102 | 103 | for ionic_step in ionic_steps_dict: 104 | _frac_coords = [site['abc'] for site in ionic_step['structure']['sites']] 105 | frac_coords.append(_frac_coords) 106 | 107 | if read_site_props: 108 | _site_properties = {} 109 | for key in ionic_step['structure']['sites'][0]['properties']: 110 | _prop = [site['properties'][key] for site in ionic_step['structure']['sites']] 111 | _site_properties[key] = _prop 112 | site_properties.append(_site_properties) 113 | else: 114 | site_properties.append(None) 115 | lattice = ionic_steps_dict[0]['structure']['lattice']['matrix'] 116 | species = [site['species'][0]['element'] for site in ionic_step['structure']['sites']] 117 | 118 | frame_properties = {} 119 | keys = set(ionic_steps_dict[0].keys()) - set(['structure']) 120 | for key in keys: 121 | if key in ['forces', 'stress']: 122 | frame_properties[key] = np.array(ionic_steps[key]) 123 | else: 124 | frame_properties[key] = ionic_steps[key] 125 | 126 | return Trajectory(lattice, species, frac_coords, site_properties=site_properties, 127 | constant_lattice=True, frame_properties=frame_properties, 128 | time_step=time_step) 129 | -------------------------------------------------------------------------------- /mpmorph/runners/rescale_volume.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pymatgen.io.vasp import Poscar 3 | 4 | __author__ = 'Eric Sivonxay and Muratahan Aykol' 5 | __maintainer__ = 'Eric Sivonxay' 6 | __email__ = 'esivonxay@lbl.gov' 7 | 8 | 9 | class RescaleVolume(object): 10 | """ 11 | Class for adjusting the volume of an input simulation box based on conditions. 12 | """ 13 | 14 | def __init__(self, structure, initial_pressure=0.0, initial_temperature=1, 15 | target_pressure=0.0, target_temperature=1, 16 | alpha=10e-6, beta=10e-7, poscar=None): 17 | """ 18 | Args: 19 | structure: 20 | initial_pressure: in bars 21 | initial_temperature: in Kelvins 22 | target_pressure: in bars 23 | target_temperature: n Kelvins 24 | alpha: 25 | beta: 26 | poscar: 27 | 28 | Returns: 29 | 30 | """ 31 | self.structure = structure 32 | self.initial_pressure = initial_pressure # in bars 33 | self.initial_temperature = initial_temperature # in K 34 | self.target_pressure = target_pressure # in bars 35 | self.target_temperature = target_temperature # in K 36 | self.alpha = alpha # /K 37 | self.beta = beta # /bar 38 | self.poscar = poscar 39 | 40 | def rescale_structure_volume(self, v2_v1, tol=1): 41 | """ 42 | Scales the volume of a structure by the given factor v2_v1. 43 | Args: 44 | v2_v1: ratio of final volume to initial volume 45 | tol: tolerance for largest allowed volume change (fractional) 46 | Returns the same structure with the new volume. 47 | """ 48 | if np.fabs(1 - v2_v1) > tol: 49 | raise "Attempted volume change too large!" 50 | else: 51 | return self.structure.scale_lattice(self.structure.volume * v2_v1) 52 | 53 | def by_thermo(self, scale='pressure'): 54 | """ 55 | Scales the volume of structure using thermodynamic functions, which basically give linear 56 | Equations of State. For more advanced EOS, one should use the by_EOS method. 57 | Args: 58 | scale (str): thermodynamic function used to scale; 'temperature' or 'pressure'. 59 | Returns: 60 | rescaled structure 61 | 62 | """ 63 | if scale == 'pressure': 64 | v2_v1 = np.exp(-self.beta * (self.target_pressure - self.initial_pressure)) 65 | self.rescale_structure_volume(v2_v1) 66 | self.initial_pressure = self.target_pressure 67 | elif scale == 'temperature': 68 | v2_v1 = np.exp(self.alpha * (self.target_temperature - self.initial_temperature)) 69 | self.rescale_structure_volume(v2_v1) 70 | self.initial_temperature = self.target_temperature 71 | else: 72 | raise ValueError("scale function must be specified as temperature or pressure.") 73 | 74 | return self.structure 75 | 76 | def by_EOS(self, p_v, eos='polynomial'): 77 | """ 78 | Args: 79 | p_v (numpy array): an array of pressure-volume pairs; e.g. p_v = [[p1,v1],[p2,v2],...] 80 | eos (str): type of equation of state to fit to. Currently only polynomial is supported. 81 | - 'polynomial': if p_v has two elements, fits line, otherwise; quadratic polynomial 82 | - 'Murnaghan': Murnaghan EOS. Not implemented yet. 83 | - 'BirchMurnaghan': Birch-Murnaghan EOS. 84 | Returns: 85 | self.structure 86 | """ 87 | v1 = self.structure.volume 88 | if eos == 'polynomial': 89 | v2_v1 = poly_rescale(p_v, target_pressure=self.target_pressure) / v1 90 | self.rescale_structure_volume(v2_v1) 91 | self.initial_pressure = self.target_pressure 92 | elif eos == 'Murnaghan': 93 | raise ValueError("not implemented yet") 94 | elif eos == 'BirchMurnaghan': 95 | v2_v1 = BirchMurnaghan_rescale(p_v, target_pressure=self.target_pressure) / v1 96 | self.rescale_structure_volume(v2_v1) 97 | self.initial_pressure = self.target_pressure 98 | else: 99 | raise ValueError("Unknown EOS. Volume not rescaled.") 100 | return self.structure 101 | 102 | @classmethod 103 | def of_poscar(cls, poscar_path, initial_pressure=0.0, initial_temperature=1000.0, 104 | target_pressure=0.0, target_temperature=1000.0, 105 | alpha=10e-5, beta=10e-7): 106 | """ 107 | Convenience constructor that accepts a poscar file as input 108 | 109 | """ 110 | poscar = Poscar.from_file(poscar_path) 111 | return cls(poscar.structure, initial_pressure=initial_pressure, initial_temperature=initial_temperature, 112 | target_pressure=target_pressure, target_temperature=target_temperature, 113 | alpha=alpha, beta=beta, poscar=poscar) 114 | 115 | 116 | def poly_rescale(p_v, target_pressure=0.0): 117 | """ 118 | :param p_v: a numpy array of pressure-volume pairs p_v = [[p1,v1],[p2,v2],...] 119 | if p_v has 2 elements, fit a line and return volume for zero pressure 120 | if p_v has 3 or more elements, fit a second order polynomial and return 121 | :return: the volume at target_pressure 122 | """ 123 | if len(p_v) < 2: 124 | raise ValueError("At least 2 p-v points required to estimate") 125 | if len(p_v) == 2: 126 | eqs = np.poly1d(np.polyfit(p_v[:, 0], p_v[:, 1], 1)) 127 | else: 128 | eqs = np.poly1d(np.polyfit(p_v[:, 0], p_v[:, 1], 2)) 129 | return eqs(target_pressure) 130 | 131 | 132 | def BirchMurnaghanPV_EOS(V, params): 133 | """ 134 | Args: 135 | V: volume 136 | params: tuple of B0,V0,B0p 137 | Returns: 138 | Pressure of Birch-Murnaghan EOS at V with given parameters E0, B0, V0 and B0p 139 | """ 140 | V0, B0, B0p = params[0], params[1], params[2] 141 | n = (V0 / V) ** (1. / 3) # Note this definition is different from the Energy EOS 142 | p = 3.0 / 2.0 * B0 * (n ** 7 - n ** 5) * (1. + 3. / 4 * (B0p - 4) * (n ** 2 - 1.0)) 143 | return p 144 | 145 | 146 | def fit_BirchMurnaghanPV_EOS(p_v): 147 | # Borrows somewhat from pymatgen/io/abinitio/EOS 148 | # Initial guesses for the parameters 149 | from scipy.optimize import leastsq 150 | eqs = np.polyfit(p_v[:, 1], p_v[:, 0], 2) 151 | V0 = np.mean(p_v[:, 1]) # still use mean to ensure we are at reasonable volumes 152 | B0 = -1 * (2 * eqs[0] * V0 ** 2 + eqs[1] * V0) 153 | B0p = 4.0 154 | initial_params = (V0, B0, B0p) 155 | Error = lambda params, x, y: BirchMurnaghanPV_EOS(x, params) - y 156 | found_params, check = leastsq(Error, initial_params, args=(p_v[:, 1], p_v[:, 0])) 157 | if check not in [1, 2, 3, 4]: 158 | raise ValueError("fitting not converged") 159 | else: 160 | return found_params 161 | 162 | 163 | def BirchMurnaghan_rescale(p_v, target_pressure=0): 164 | """ 165 | Calls fit_BirchMurnaghanPV_EOS to find params of EOS and returns V corresponding to target_pressure 166 | Args: 167 | p_v: 168 | target_pressure: 169 | 170 | Returns: 171 | 172 | """ 173 | params = fit_BirchMurnaghanPV_EOS(p_v) 174 | if target_pressure == 0: 175 | return params[0] # This is V0 176 | else: 177 | # TODO: find volume corresponding to this target_pressure 178 | pass 179 | -------------------------------------------------------------------------------- /mpmorph/workflows/converge.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from copy import deepcopy 3 | 4 | from fireworks import Workflow 5 | from mpmorph.fireworks import powerups 6 | from mpmorph.fireworks.core import MDFW 7 | from mpmorph.util import recursive_update 8 | 9 | __author__ = 'Eric Sivonxay, Jianli Cheng, and Muratahan Aykol' 10 | __maintainer__ = 'Eric Sivonxay' 11 | __email__ = 'esivonxay@lbl.gov' 12 | 13 | 14 | def get_converge_wf(structure, temperature, converge_scheme='EOS', priority=None, 15 | max_steps=5000, target_steps=10000, preconverged=False, 16 | notes=None, save_data="all", **kwargs): 17 | """ 18 | 19 | Args: 20 | structure: Starting structure for the run 21 | temperature: Temperature for the MD runs 22 | converge_scheme: Equation of state is normally faster and preferred 23 | priority: Priority of all fireworks in the workflows 24 | max_steps: Maximum number of steps per chunk of production run MD simulation 25 | target_steps: Target number of steps for production MD run 26 | preconverged: Whether the structure already converged (i.e. Pressure 0bar) 27 | or volume rescaling not desired 28 | notes: Any additional comments to propagate with this run 29 | save_data: Level to save job outputs. Options are "all", 'production', and None 30 | **kwargs: Arguments such as spawner_args, converge_args, convergence_criteria, 31 | tag_id, prod_count, etc. 32 | 33 | Returns: Workflow object 34 | 35 | """ 36 | # Generate a unique identifier for the fireworks belonging to this workflows 37 | tag_id = kwargs.get('tag_id', uuid.uuid4()) 38 | prod_count = kwargs.get('prod_count', 0) 39 | wf_name = kwargs.get('wf_name', f'{structure.composition.reduced_formula}_{temperature}_diffusion') 40 | 41 | fw_list = [] 42 | 43 | # Setup initial Run and convergence of structure 44 | run_args = {"md_params": {"start_temp": temperature, "end_temp": temperature, "nsteps": 2000}, 45 | "run_specs": {"vasp_input_set": None, "vasp_cmd": ">>vasp_cmd<<", "db_file": ">>db_file<<"}, 46 | "optional_fw_params": { 47 | "override_default_vasp_params": {'user_incar_settings': {'ISIF': 1, 'LWAVE': False, 48 | 'PREC': 'Normal'}}, 49 | "spec": {'_priority': priority} 50 | } 51 | } 52 | run_args = recursive_update(run_args, kwargs.get('converge_args', {})) 53 | 54 | # Setup Dictionary specifying parameters of the spawner for convergence tasks 55 | _spawner_args = { 56 | "converge_params": {"max_rescales": 15, "density_spawn_count": 1, "energy_spawn_count": 0, 57 | 'converge_type': kwargs.get('convergence_criteria', 58 | [("density", 5), ('ionic', 0.001)])}, 59 | "rescale_params": {"beta": 5e-7}, 60 | "run_specs": run_args["run_specs"], 61 | "md_params": run_args["md_params"], 62 | "optional_fw_params": run_args["optional_fw_params"], 63 | "tag_id": tag_id 64 | } 65 | _spawner_args["md_params"].update({"start_temp": run_args["md_params"]["end_temp"]}) 66 | _spawner_args = recursive_update(_spawner_args, kwargs.get('spawner_args', {})) 67 | 68 | # Converge the pressure (volume) of the system 69 | if not preconverged: 70 | insert_converge_data = True if save_data == "all" else False 71 | 72 | if converge_scheme == 'EOS': 73 | # Create structures for varying volumes 74 | images = kwargs.get('image_scale', [0.8, 1, 1.2]) 75 | structures = [structure.copy() for i in images] 76 | for i, factor in enumerate(images): 77 | structures[i].scale_lattice(structure.volume * factor) 78 | 79 | # Create firework for each structure 80 | EOS_run_args = deepcopy(run_args) 81 | EOS_run_args = recursive_update(EOS_run_args, kwargs.get('converge_args', {})) 82 | volume_fws = [] 83 | for n, (i, vol_structure) in enumerate(zip(images, structures)): 84 | save_structure = True if n == len(images) - 1 else False 85 | _fw = MDFW(structure=vol_structure, name=f'volume_{i}-{tag_id}', 86 | previous_structure=False, insert_db=insert_converge_data, save_structure=save_structure, 87 | **EOS_run_args["md_params"], **EOS_run_args["run_specs"], 88 | **EOS_run_args["optional_fw_params"]) 89 | 90 | _fw = powerups.add_pass_pv(_fw) 91 | volume_fws.append(_fw) 92 | fw_list.extend(volume_fws) 93 | 94 | # Create firework to converge pressure/volume 95 | spawner_fw = MDFW(structure=structure, name=f'run1-{tag_id}', 96 | previous_structure=True, insert_db=insert_converge_data, 97 | parents=volume_fws, **run_args["md_params"], 98 | **run_args["run_specs"], **run_args["optional_fw_params"]) 99 | 100 | spawner_fw = powerups.add_pv_volume_rescale(spawner_fw) 101 | spawner_fw = powerups.add_pass_pv(spawner_fw) 102 | _spawner_args['run_specs']['insert_db'] = insert_converge_data 103 | spawner_fw = powerups.add_converge_task(spawner_fw, **_spawner_args) 104 | fw_list.append(spawner_fw) 105 | else: 106 | fw1 = MDFW(structure=structure, name="run0" + "-" + str(tag_id), 107 | previous_structure=False, insert_db=insert_converge_data, 108 | **run_args["md_params"], **run_args["run_specs"], **run_args["optional_fw_params"]) 109 | fw1 = powerups.add_converge_task(fw1, **_spawner_args) 110 | fw_list.append(fw1) 111 | 112 | # Production length MD runs 113 | insert_prod_data = True if save_data == "all" or save_data == "production" else False 114 | prod_steps = 0 115 | while prod_steps <= target_steps - max_steps: 116 | # Create Dictionary with production run parameters 117 | run_args = {"md_params": {"start_temp": run_args["md_params"]["end_temp"], 118 | "end_temp": run_args["md_params"]["end_temp"], 119 | "nsteps": max_steps}, 120 | "run_specs": {"vasp_input_set": None, "vasp_cmd": ">>vasp_cmd<<", "db_file": ">>db_file<<"}, 121 | "optional_fw_params": {"override_default_vasp_params": 122 | {'user_incar_settings': {'ISIF': 1, 'LWAVE': False, 123 | 'PREC': 'Normal'}}, 124 | "spec": {'_priority': priority}}} 125 | run_args = recursive_update(run_args, kwargs.get('prod_args', {})) 126 | 127 | parents = fw_list[-1] if len(fw_list) > 0 else [] 128 | previous_structure = False if preconverged and prod_steps == 0 else True 129 | fw = MDFW(structure=structure, name=f'{temperature}_prod_run_{prod_count}-{tag_id}', 130 | previous_structure=previous_structure, insert_db=insert_prod_data, **run_args["md_params"], 131 | **run_args["run_specs"], **run_args["optional_fw_params"], parents=parents) 132 | fw_list.append(fw) 133 | 134 | prod_steps += max_steps 135 | prod_count += 1 136 | 137 | wf = Workflow(fireworks=fw_list, name=wf_name) 138 | return wf 139 | -------------------------------------------------------------------------------- /mpmorph/fireworks/core.py: -------------------------------------------------------------------------------- 1 | from atomate.common.firetasks.glue_tasks import PassCalcLocs 2 | from atomate.vasp.firetasks.glue_tasks import CopyVaspOutputs 3 | from atomate.vasp.firetasks.parse_outputs import VaspToDb 4 | from atomate.vasp.firetasks.run_calc import RunVaspCustodian 5 | from atomate.vasp.firetasks.write_inputs import WriteVaspFromIOSet 6 | from atomate.vasp.fireworks.core import StaticFW 7 | from fireworks import Firework 8 | from mpmorph.firetasks.dbtasks import VaspMDToDb 9 | from mpmorph.firetasks.glue_tasks import PreviousStructureTask, SaveStructureTask 10 | from pymatgen.io.vasp.sets import MPMDSet, MPStaticSet, MPRelaxSet 11 | 12 | """ 13 | These fireworks were adapted from atomate.vasp.fireworks.core specifically for this package. 14 | Slight modifications were made to each. 15 | """ 16 | 17 | __maintainer__ = "Eric Sivonxay" 18 | __email__ = "esivonxay@lbl.gov" 19 | 20 | 21 | class MDFW(Firework): 22 | def __init__(self, structure, start_temp, end_temp, nsteps, name="molecular dynamics", 23 | vasp_input_set=None, vasp_cmd="vasp", override_default_vasp_params=None, 24 | wall_time=None, db_file=None, parents=None, copy_vasp_outputs=False, 25 | previous_structure=False, insert_db=False, save_structure=True, **kwargs): 26 | """ 27 | This Firework is modified from atomate.vasp.fireworks.core.MDFW to fit the needs of mpmorph 28 | Standard firework for a single MD run. 29 | Args: 30 | structure (Structure): Input structure. 31 | start_temp (float): Start temperature of MD run. 32 | end_temp (float): End temperature of MD run. 33 | nsteps (int): Number of MD steps 34 | name (string): Name for the Firework. 35 | vasp_input_set (string): string name for the VASP input set (e.g., 36 | "MITMDVaspInputSet"). 37 | vasp_cmd (string): Command to run vasp. 38 | override_default_vasp_params (dict): If this is not None, 39 | these params are passed to the default vasp_input_set, i.e., 40 | MITMDSet. This allows one to easily override some 41 | settings, e.g., user_incar_settings, etc. Particular to MD, 42 | one can control time_step and all other settings of the input set. 43 | wall_time (int): Total wall time in seconds before writing STOPCAR. 44 | copy_vasp_outputs (bool): Whether to copy outputs from previous run. Defaults to True. 45 | db_file (string): Path to file specifying db credentials. 46 | parents (Firework): Parents of this particular Firework. FW or list of FWS. 47 | \*\*kwargs: Other kwargs that are passed to Firework.__init__. 48 | """ 49 | override_default_vasp_params = override_default_vasp_params or {} 50 | vasp_input_set = vasp_input_set or MPMDSet(structure, start_temp=start_temp, 51 | end_temp=end_temp, nsteps=nsteps, 52 | **override_default_vasp_params) 53 | 54 | t = [] 55 | 56 | t.append(WriteVaspFromIOSet(structure=structure, vasp_input_set=vasp_input_set)) 57 | if previous_structure: 58 | t.append(PreviousStructureTask()) 59 | t.append(RunVaspCustodian(vasp_cmd=vasp_cmd, gamma_vasp_cmd=">>gamma_vasp_cmd<<", 60 | handler_group="md", wall_time=wall_time)) 61 | t.append(PassCalcLocs(name=name)) 62 | if save_structure: 63 | t.append(SaveStructureTask()) 64 | name = f'{structure.composition.reduced_formula}-{name}' 65 | if insert_db: 66 | t.append(VaspMDToDb(db_file=db_file, additional_fields={"task_label": name}, 67 | defuse_unsuccessful=False, md_structures=True)) 68 | super(MDFW, self).__init__(t, parents=parents, name=name, **kwargs) 69 | 70 | 71 | class OptimizeFW(Firework): 72 | def __init__(self, structure, name="structure optimization", vasp_input_set=None, 73 | insert_db=True, vasp_cmd="vasp", override_default_vasp_params=None, 74 | ediffg=None, db_file=None, force_gamma=True, 75 | job_type="double_relaxation_run", max_force_threshold=None, 76 | previous_structure=False, auto_npar=">>auto_npar<<", 77 | half_kpts_first_relax=False, parents=None, 78 | handler_group="default", 79 | prev_calc_loc=False, **kwargs): 80 | """ 81 | Optimize the given structure. 82 | Args: 83 | structure (Structure): Input structure. 84 | name (str): Name for the Firework. 85 | vasp_input_set (VaspInputSet): input set to use. Defaults to MPRelaxSet() if None. 86 | override_default_vasp_params (dict): If this is not None, these params are passed to 87 | the default vasp_input_set, i.e., MPRelaxSet. This allows one to easily override 88 | some settings, e.g., user_incar_settings, etc. 89 | vasp_cmd (str): Command to run vasp. 90 | ediffg (float): Shortcut to set ediffg in certain jobs 91 | db_file (str): Path to file specifying db credentials to place output parsing. 92 | force_gamma (bool): Force gamma centered kpoint generation 93 | job_type (str): custodian job type (default "double_relaxation_run") 94 | max_force_threshold (float): max force on a site allowed at end; otherwise, reject job 95 | auto_npar (bool or str): whether to set auto_npar. defaults to env_chk: ">>auto_npar<<" 96 | half_kpts_first_relax (bool): whether to use half the kpoints for the first relaxation 97 | parents ([Firework]): Parents of this particular Firework. 98 | \*\*kwargs: Other kwargs that are passed to Firework.__init__. 99 | """ 100 | override_default_vasp_params = override_default_vasp_params or {} 101 | vasp_input_set = vasp_input_set or MPRelaxSet( 102 | structure, force_gamma=force_gamma, **override_default_vasp_params) 103 | 104 | t = [] 105 | if prev_calc_loc: 106 | additional_files = kwargs.get("additional_files", []) 107 | if "additional_files" in kwargs.keys(): 108 | del kwargs["additional_files"] 109 | t.append(CopyVaspOutputs(calc_loc=prev_calc_loc, contcar_to_poscar=True, additional_files=additional_files)) 110 | t.append(WriteVaspFromIOSet(structure=structure, 111 | vasp_input_set=vasp_input_set)) 112 | if previous_structure: 113 | t.append(PreviousStructureTask()) 114 | t.append(RunVaspCustodian(vasp_cmd=vasp_cmd, job_type=job_type, 115 | max_force_threshold=max_force_threshold, 116 | ediffg=ediffg, 117 | auto_npar=auto_npar, 118 | half_kpts_first_relax=half_kpts_first_relax, 119 | handler_group=handler_group)) 120 | t.append(PassCalcLocs(name=name)) 121 | t.append(SaveStructureTask()) 122 | 123 | if insert_db: 124 | t.append(VaspToDb(db_file=db_file, additional_fields={"task_label": name})) 125 | name = f'{structure.composition.reduced_formula}-{name}' 126 | super(OptimizeFW, self).__init__(t, parents=parents, name=name, **kwargs) 127 | 128 | 129 | class StaticFW(Firework): 130 | def __init__(self, structure, name="static", 131 | previous_structure=False, vasp_input_set=None, 132 | vasp_cmd="vasp", db_file=None, parents=None, 133 | override_default_vasp_params=None, 134 | pass_structure=True, 135 | prev_calc_loc=False, **kwargs): 136 | """ 137 | This Firework is modified from atomate.vasp.fireworks.core.StaticFW to fit the needs of mpmorph 138 | Standard static calculation Firework - either from a previous location or from a structure. 139 | Args: 140 | structure (Structure): Input structure. Note that for prev_calc_loc jobs, the structure 141 | is only used to set the name of the FW and any structure with the same composition 142 | can be used. 143 | name (str): Name for the Firework. 144 | vasp_input_set (VaspInputSet): input set to use (for jobs w/no parents) 145 | Defaults to MPStaticSet() if None. 146 | vasp_cmd (str): Command to run vasp. 147 | prev_calc_loc (bool or str): If true (default), copies outputs from previous calc. If 148 | a str value, grabs a previous calculation output by name. If False/None, will create 149 | new static calculation using the provided structure. 150 | db_file (str): Path to file specifying db credentials. 151 | parents (Firework): Parents of this particular Firework. FW or list of FWS. 152 | \*\*kwargs: Other kwargs that are passed to Firework.__init__. 153 | """ 154 | 155 | t = [] 156 | override_default_vasp_params = override_default_vasp_params or {} 157 | vasp_input_set = vasp_input_set or MPStaticSet(structure, **override_default_vasp_params) 158 | if prev_calc_loc: 159 | t.append(CopyVaspOutputs(calc_loc=prev_calc_loc, contcar_to_poscar=True)) 160 | t.append(WriteVaspFromIOSet(structure=structure, vasp_input_set=vasp_input_set)) 161 | if previous_structure: 162 | t.append(PreviousStructureTask()) 163 | t.append(RunVaspCustodian(vasp_cmd=vasp_cmd, auto_npar=">>auto_npar<<")) 164 | t.append(SaveStructureTask()) 165 | t.append(PassCalcLocs(name=name)) 166 | t.append(VaspToDb(db_file=db_file, additional_fields={"task_label": name})) 167 | name = f'{structure.composition.reduced_formula}-{name}' 168 | super(StaticFW, self).__init__(t, parents=parents, name=name, **kwargs) 169 | -------------------------------------------------------------------------------- /mpmorph/analysis/diffusion.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains new classes for obtaining 3 | Diffusion and Activation Barrier calculations 4 | from MD calculations. 5 | """ 6 | 7 | __author__ = 'Muratahan Aykol ' 8 | 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | import scipy.integrate as integrate 12 | from pymatgen.core import Element 13 | from pymatgen.io.vasp import Xdatcar 14 | from scipy import stats 15 | 16 | 17 | class Diffusion(object): 18 | """ 19 | Robust calculation of diffusion coefficients with different statistical analysis techniques: 20 | - Block averaging (default) 21 | - Jackknife (to be implemented) 22 | - Bootstrap (to be implemented) 23 | 24 | Args: 25 | structures: (list) list of Structures 26 | corr_t: (float) correlation time (in terms of # of steps). 27 | Each time origin will be this many steps apart. 28 | block_l: (int) defines length of a block in terms of corr_t. (block_t = block_l * corr_t) 29 | t_step: (float) time-step in MD simulation. Defaults to 2.0 fs. 30 | l_lim: (int) this many time-steps are skipped in MSD while fitting D. I.e. approximate length of 31 | ballistic and cage regions. Defaults to 50. 32 | skip_first: (int) this many initial time-steps are skipped. Defaults to 0. 33 | ci: (float) confidence interval desired estimating the mean D of population. 34 | """ 35 | 36 | def __init__(self, structures, corr_t, block_l, t_step=2.0, l_lim=50, skip_first=0, ci=0.95): 37 | self.structures = structures 38 | self.abc = self.structures[0].lattice.abc 39 | self.natoms = len(self.structures[0]) 40 | self.skip_first = skip_first 41 | self.total_t = len(self.structures) 42 | self.corr_t = corr_t 43 | self.l_lim = l_lim 44 | self.t_step = t_step 45 | self.block_l = block_l 46 | self.ci = ci 47 | self.msds = None 48 | self.vel_matrix = None 49 | self.vacfs = None 50 | self.scaling_factor = 0.1 / self.t_step # conv. to cm2/s 51 | 52 | @property 53 | def n_origins(self): 54 | n = int((self.total_t - self.block_t - self.skip_first) / self.corr_t + 1) 55 | if n <= 0: 56 | raise ValueError("Too many blocks for the correlation time") 57 | return n 58 | 59 | @property 60 | def block_t(self): 61 | return self.block_l * self.corr_t 62 | 63 | def _getd(self, el): 64 | md = [np.zeros(self.structures[0].frac_coords.shape)] 65 | for i in range(self.skip_first + 1, self.total_t): 66 | dx = self.structures[i].frac_coords - self.structures[i - 1].frac_coords 67 | dx -= np.round(dx) 68 | md.append(dx) 69 | 70 | self.md = np.array(md) * self.abc 71 | 72 | # remove other elements from the rest of the calculations 73 | s = set(self.structures[0].indices_from_symbol(el)) 74 | self.md = np.delete(self.md, [x for x in list(range(self.natoms)) if x not in s], 1) 75 | 76 | msds = [] 77 | for i in range(self.n_origins): 78 | su = np.square(np.cumsum(self.md[i * self.corr_t: i * self.corr_t + self.block_t], axis=0)) 79 | msds.append(np.mean(su, axis=1)) 80 | self.msds = msds 81 | 82 | def plot_block_msds(self): 83 | for i in self.msds: 84 | plt.plot(i) 85 | 86 | def getD(self, el): 87 | """ 88 | Method to calculate diffusion coefficient(s) of the given element (el). 89 | """ 90 | 91 | if type(el) == type(Element("Li")): 92 | el = el.name 93 | 94 | self._getd(el) 95 | D = [[], [], []] 96 | for i in self.msds: 97 | for j in range(3): 98 | slope, intercept, r_value, p_value, std_err = \ 99 | stats.linregress(np.arange(self.l_lim, self.block_t), i[:, j][self.l_lim:]) 100 | D[j].append(slope / 2.0) 101 | D = np.array(D) * self.scaling_factor 102 | self.D_blocks = D 103 | 104 | alpha = 1.0 - self.ci 105 | tn = stats.t.ppf(1.0 - alpha / 2.0, len(self.D_blocks) - 1) / np.sqrt(len(self.D_blocks)) 106 | 107 | if tn == "nan": 108 | tn = 1 109 | self.D_i = np.mean(D, axis=1) 110 | self.D_i_std = np.std(D, axis=1) * tn 111 | self.D_avg = np.sum(self.D_i) / 3.0 112 | self.D_avg_std = np.std(np.sum(D, axis=0) / 3.0) * tn 113 | return self.D_dict 114 | 115 | @property 116 | def D_dict(self): 117 | D_dict = {} 118 | dirs = ["Dx", "Dy", "Dz"] 119 | D_dict.update(dict(zip(dirs, self.D_i))) 120 | D_dict.update(dict(zip([s + "_std" for s in dirs], self.D_i_std))) 121 | D_dict.update({"D": self.D_avg, "D_std": self.D_avg_std}) 122 | return D_dict 123 | 124 | @property 125 | def tao(self): 126 | tao_dict = {} 127 | for k, v in self.D_dict.items(): 128 | if "_std" not in k: 129 | tao_dict[k] = 1.0 / v * self.scaling_factor 130 | return tao_dict 131 | 132 | def autocorrelation(self): 133 | "to be implemented" 134 | pass 135 | 136 | def get_v(self, el): 137 | # Make copy of structures 138 | _structures = [structure.copy() for structure in self.structures] 139 | 140 | # Find unneccessary elements and delete 141 | prune_els = [] 142 | for specie in _structures[0].species: 143 | if specie != el: 144 | prune_els.append(specie) 145 | for structure in _structures: 146 | structure.remove_species(prune_els) 147 | _structures_sites = [structure.sites for structure in _structures] 148 | 149 | # Iterate through each site through each timestep and find velocity 150 | vel_matrix = [[0 for y in range(len(_structures) - 1)] for x in range(len(_structures[0].sites))] 151 | for i in range(len(vel_matrix)): 152 | for j in range(len(vel_matrix[0])): 153 | vel_matrix[i][j] = _structures_sites[j][i].distance(_structures_sites[j + 1][i]) / self.t_step 154 | self.vel_matrix = vel_matrix 155 | return 156 | 157 | def get_v_vector(self, el): 158 | # Make copy of structures 159 | _structures = [structure.copy() for structure in self.structures] 160 | 161 | # Find unneccessary elements and delete 162 | prune_els = [] 163 | for specie in _structures[0].species: 164 | if specie != el: 165 | prune_els.append(specie) 166 | for structure in _structures: 167 | structure.remove_species(prune_els) 168 | _structures_sites = [structure.sites for structure in _structures] 169 | 170 | # Iterate through each site through each timestep and find velocity 171 | vel_matrix = [[[0, 0, 0] for y in range(len(_structures) - 1)] for x in range(len(_structures[0].sites))] 172 | for i in range(len(vel_matrix)): 173 | for j in range(len(vel_matrix[0])): 174 | dist_x = _structures_sites[j][i].x - _structures_sites[j + 1][i].x 175 | if dist_x > _structures[i].lattice.a / 2: 176 | dist_x = (_structures[i].lattice.a - np.abs(dist_x)) * (-1 * np.sign(dist_x)) 177 | dist_y = _structures_sites[j][i].y - _structures_sites[j + 1][i].y 178 | # if dist_y > _structures[i].lattice.b/2: 179 | # dist_y = (_structures[i].lattice.b-np.abs(dist_y))*(-1*np.sign(dist_y)) 180 | dist_z = _structures_sites[j][i].z - _structures_sites[j + 1][i].z 181 | # if dist_z > _structures[i].lattice.c/2: 182 | # dist_z = (_structures[i].lattice.c-np.abs(dist_z))*(-1*np.sign(dist_z)) 183 | 184 | vel_matrix[i][j][0] = dist_x / self.t_step 185 | vel_matrix[i][j][1] = dist_y / self.t_step 186 | vel_matrix[i][j][2] = dist_z / self.t_step 187 | 188 | self.vel_matrix = vel_matrix 189 | return 190 | 191 | def green_kubo_D(self, el): 192 | self.get_v(el) 193 | # Get velocity autocorrelation function for each site 194 | vacfs = [] 195 | for site_vel in self.vel_matrix: 196 | _vacf = np.correlate(site_vel, site_vel, "full") 197 | vacfs.append(_vacf) 198 | self.vacfs = vacfs 199 | D = [] 200 | for vacf in vacfs: 201 | D.append(integrate.simps(vacf)) 202 | return D 203 | 204 | 205 | class Activation(object): 206 | def __init__(self, D_t): 207 | self.D_t = D_t 208 | self.Q = None 209 | self.intercept = None 210 | self.Q_std = None 211 | 212 | def LS(self): 213 | self.x = np.array([1 / float(t[0]) for t in self.D_t]) 214 | self.y = np.array([np.log(t[1]["D"]) for t in self.D_t]) 215 | self.yerr = np.array([[-np.log((t[1]["D"] - t[1]["D_std"]) / t[1]["D"]), 216 | np.log((t[1]["D"] + t[1]["D_std"]) / t[1]["D"]) 217 | ] for t in self.D_t]) 218 | self.Q, self.intercept, self.r_value, self.p_value, self.std_err = \ 219 | stats.linregress(self.x, self.y) 220 | self.Q *= -1 221 | return self.Q 222 | 223 | def ODR(self): 224 | if not self.Q: 225 | self.LS() 226 | import scipy.odr 227 | 228 | def fit_func(p, t): 229 | return p[0] * t + p[1] 230 | 231 | Model = scipy.odr.Model(fit_func) 232 | Data = scipy.odr.RealData(self.x, self.y, sy=np.mean(self.yerr, axis=1)) 233 | Odr = scipy.odr.ODR(Data, Model, [-self.Q, self.intercept]) 234 | Odr.set_job(fit_type=2) 235 | self.output = Odr.run() 236 | self.Q, self.intercept = -self.output.beta[0], self.output.beta[1] 237 | self.Q_std = self.output.sd_beta[0] 238 | self.intercept_std = self.output.sd_beta[1] 239 | return self.Q, self.Q_std 240 | 241 | def plot(self, title=None, annotate=True, el='', **kwargs): 242 | # fig = plt.figure() 243 | 244 | line = np.polyval([-self.Q, self.intercept], self.x) 245 | tx = str(int(np.rint(self.Q))) 246 | if self.Q_std: 247 | tx += "$\pm${}".format(str(int(np.rint(self.Q_std)))) 248 | c = kwargs.get('color', '') 249 | plt.plot(self.x * 1000, line, c + '-', ) 250 | plt.errorbar(self.x * 1000, self.y, yerr=self.yerr.T, label="Q[{}]: ".format(el) + tx + " K", **kwargs) 251 | plt.ylabel("ln(D cm$^2$/s)", fontsize=15) 252 | plt.xlabel("1000/T K$^{-1}$", fontsize=15) 253 | 254 | if annotate: 255 | plt.annotate("Q: " + tx + " K", xy=(0.98, 0.95), xycoords='axes fraction', fontsize=14, 256 | horizontalalignment='right', verticalalignment='top') 257 | if title: 258 | plt.title = title 259 | # return fig 260 | 261 | @classmethod 262 | def from_run_paths(cls, p, T, el, corr_t, block_l, t_step=2.0, l_lim=50, skip_first=0): 263 | D_t = [] 264 | for t in range(len(p)): 265 | xdatcar = Xdatcar(p[t]) 266 | d = Diffusion(xdatcar.structures, corr_t=corr_t, block_l=block_l, 267 | t_step=t_step, l_lim=l_lim, skip_first=skip_first) 268 | D_t.append([T[t], d.getD(el)]) 269 | return cls(D_t) 270 | -------------------------------------------------------------------------------- /mpmorph/firetasks/mdtasks.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import numpy as np 4 | from fireworks import explicit_serialize, Workflow, FireTaskBase, FWAction 5 | from mpmorph.analysis import md_data 6 | from mpmorph.runners.rescale_volume import RescaleVolume, fit_BirchMurnaghanPV_EOS 7 | from mpmorph.util import recursive_update 8 | from pymatgen.core import Structure 9 | from pymatgen.io.vasp import Poscar 10 | from pymatgen.io.vasp.outputs import Vasprun 11 | from scipy import stats 12 | 13 | __author__ = 'Eric Sivonxay and Muratahan Aykol' 14 | __maintainer__ = "Eric Sivonxay" 15 | __email__ = "esivonxay@lbl.gov" 16 | 17 | 18 | @explicit_serialize 19 | class DiffusionTask(FireTaskBase): 20 | required_params = ['temperatures', 'max_steps', 'target_steps', 21 | 'num_samples' 'trajectory_to_db', 'notes'] 22 | optional_params = [] 23 | 24 | def run_task(self, fw_spec): 25 | from mpmorph.workflows.converge import get_converge_wf 26 | 27 | vr = Vasprun('vasprun.xml.gz') 28 | 29 | fws = [] 30 | for t in self['temperatures']: 31 | fws.extend(get_converge_wf(s, int(t), max_steps=self['max_steps'], 32 | target_steps=self['target_steps'], 33 | trajectory_to_db=self['trajectory_to_db'], 34 | notes=self['notes'])) 35 | wf = Workflow(fws) 36 | return FWAction(detours=wf) 37 | 38 | 39 | @explicit_serialize 40 | class ConvergeTask(FireTaskBase): 41 | """ 42 | 43 | Ensures a structure is converged before production MD run 44 | 45 | """ 46 | 47 | required_params = ["converge_params", "run_specs", "md_params"] 48 | optional_params = ["rescale_params", 'tag_id', "optional_fw_params"] 49 | 50 | def run_task(self, fw_spec): 51 | from mpmorph.fireworks import powerups 52 | from mpmorph.fireworks.core import MDFW 53 | 54 | # Load Structure from Poscar 55 | _poscar = Poscar.from_file("CONTCAR.gz") 56 | structure = _poscar.structure 57 | 58 | # Get convergence parameters from spec 59 | converge_params = self["converge_params"] 60 | avg_fraction = converge_params.get("avg_fraction", 0.5) 61 | convergence_vars = dict(converge_params["converge_type"]) 62 | if "ionic" not in convergence_vars.keys(): 63 | convergence_vars["ionic"] = 0.0005 64 | rescale_params = self.get("rescale_params", {}) 65 | 66 | # Load Data from OUTCAR 67 | search_keys = ['external', 'kinetic energy EKIN', '% ion-electron', 'ETOTAL'] 68 | key_map = {'density': 'external', 'kinetic energy': 'kinetic energy EKIN', 69 | 'ionic': '% ion-electron', 'total energy': 'ETOTAL'} 70 | outcar_data = md_data.get_MD_data("./OUTCAR.gz", search_keys=search_keys) 71 | 72 | # Check for convergence 73 | converged = {} 74 | _index = search_keys.index(key_map["density"]) 75 | _data = np.transpose(outcar_data)[_index].copy() 76 | pressure = np.mean(_data[int(avg_fraction * (len(_data) - 1)):]) 77 | if "density" in convergence_vars.keys(): 78 | if np.abs(pressure) >= convergence_vars["density"]: 79 | converged["density"] = False 80 | else: 81 | converged["density"] = True 82 | 83 | if "kinetic energy" in convergence_vars.keys(): 84 | _index = search_keys.index(key_map["kinetic energy"]) 85 | energy = np.transpose(outcar_data)[_index].copy() 86 | norm_energy = (energy / structure.num_sites) / np.mean(energy / structure.num_sites) - 1 87 | if np.abs(np.mean(norm_energy[-500:]) - np.mean(norm_energy)) > convergence_vars["kinetic energy"]: 88 | converged["kinetic energy"] = False 89 | else: 90 | converged["kinetic energy"] = True 91 | 92 | _index = search_keys.index(key_map["ionic"]) 93 | energy = np.transpose(outcar_data)[_index].copy() 94 | norm_energies = energy / structure.num_sites 95 | mu, std = stats.norm.fit(norm_energies) 96 | mu1, std1 = stats.norm.fit(norm_energies[0:int(len(norm_energies) / 2)]) 97 | mu2, std2 = stats.norm.fit(norm_energies[int(len(norm_energies) / 2):]) 98 | if np.abs((mu2 - mu1) / mu) < convergence_vars["ionic"]: 99 | converged["ionic"] = True 100 | else: 101 | converged["ionic"] = False 102 | 103 | # Spawn Additional Fireworks 104 | if not all([item[1] for item in converged.items()]): 105 | density_spawn_count = converge_params["density_spawn_count"] 106 | energy_spawn_count = converge_params["energy_spawn_count"] 107 | max_rescales = converge_params["max_rescales"] 108 | max_energy_runs = 3 # Set max energy convergence runs to default of 3 109 | 110 | run_specs = self["run_specs"] 111 | md_params = self["md_params"] 112 | optional_params = self.get("optional_fw_params", {}) 113 | 114 | tag_id = self.get("tag_id", "") 115 | 116 | if density_spawn_count >= max_rescales: 117 | return FWAction(defuse_children=True) 118 | elif energy_spawn_count >= max_energy_runs: 119 | # Too many energy rescales... Just continue with the production runs 120 | return FWAction(stored_data={'pressure': pressure, 121 | 'energy': mu, 122 | 'density_calculated': True}) 123 | elif not converged.get("density", True): 124 | rescale_args = {"initial_pressure": pressure * 1000, "initial_temperature": 1, "beta": 0.0000005} 125 | rescale_args = recursive_update(rescale_args, rescale_params) 126 | 127 | # Spawn fw 128 | fw = MDFW(structure, name=f'density_run_{density_spawn_count + 1}-{tag_id}', 129 | previous_structure=False, 130 | **run_specs, **md_params, **optional_params) 131 | converge_params["density_spawn_count"] += 1 132 | _spawner_args = {"converge_params": converge_params, "rescale_params": rescale_params, 133 | "run_specs": run_specs, "md_params": md_params, 134 | "optional_fw_params": optional_params, "tag_id": tag_id} 135 | fw = powerups.add_rescale_volume(fw, **rescale_args) 136 | fw = powerups.add_pass_pv(fw) 137 | fw = powerups.add_converge_task(fw, **_spawner_args) 138 | wf = Workflow([fw]) 139 | return FWAction(detours=wf, stored_data={'pressure': pressure, 'energy': mu}) 140 | else: 141 | fw = MDFW(structure, name=f'energy_run_{energy_spawn_count + 1}-{tag_id}', previous_structure=False, 142 | **run_specs, **md_params, **optional_params) 143 | converge_params["energy_spawn_count"] += 1 144 | _spawner_args = {"converge_params": converge_params, "rescale_params": rescale_params, 145 | "run_specs": run_specs, "md_params": md_params, 146 | "optional_fw_params": optional_params, "tag_id": tag_id} 147 | fw = powerups.add_pass_pv(fw) 148 | fw = powerups.add_converge_task(fw, **_spawner_args) 149 | wf = Workflow([fw]) 150 | return FWAction(detours=wf, stored_data={'pressure': pressure, 'energy': mu}) 151 | else: 152 | return FWAction(stored_data={'pressure': pressure, 153 | 'energy': mu, 154 | 'density_calculated': True}) 155 | 156 | 157 | @explicit_serialize 158 | class RescaleVolumeTask(FireTaskBase): 159 | """ 160 | Volume rescaling 161 | """ 162 | required_params = ["initial_temperature", "initial_pressure"] 163 | optional_params = ["target_pressure", "target_temperature", "target_pressure", "alpha", "beta"] 164 | 165 | def run_task(self, fw_spec): 166 | # Initialize volume correction object with last structure from last_run 167 | initial_temperature = self["initial_temperature"] 168 | initial_pressure = self["initial_pressure"] 169 | target_temperature = self.get("target_temperature", initial_temperature) 170 | target_pressure = self.get("target_pressure", 0.0) 171 | alpha = self.get("alpha", 10e-6) 172 | beta = self.get("beta", 10e-7) 173 | corr_vol = RescaleVolume.of_poscar(poscar_path="./POSCAR", initial_temperature=initial_temperature, 174 | initial_pressure=initial_pressure, 175 | target_pressure=target_pressure, 176 | target_temperature=target_temperature, alpha=alpha, beta=beta) 177 | # Rescale volume based on temperature difference first. Const T will return no volume change: 178 | corr_vol.by_thermo(scale='temperature') 179 | # TO DB ("Rescaled volume due to delta T: ", corr_vol.structure.volume) 180 | # Rescale volume based on pressure difference: 181 | corr_vol.by_thermo(scale='pressure') 182 | # TO DB ("Rescaled volume due to delta P: ", corr_vol.structure.volume) 183 | corr_vol.poscar.write_file("./POSCAR") 184 | # Pass the rescaled volume to Poscar 185 | return FWAction(stored_data=corr_vol.structure.as_dict()) 186 | 187 | 188 | @explicit_serialize 189 | class PVRescaleTask(FireTaskBase): 190 | """ 191 | Rescale based on fitting pressure vs volume to Birch-Murnaghan EOS 192 | """ 193 | 194 | required_params = [] 195 | optional_params = ['rescale_type'] 196 | 197 | def run_task(self, fw_spec): 198 | rescale_type = self.get('rescale_type', 'BirchMurnaghan_EOS') 199 | 200 | if rescale_type == 'BirchMurnaghan_EOS': 201 | pv_pairs = np.array(fw_spec["pressure_volume"]) 202 | pv_pairs = np.flip(pv_pairs, axis=1) 203 | pv_pairs = np.flip(pv_pairs[pv_pairs[:, 1].argsort()], axis=0) 204 | 205 | try: 206 | params = fit_BirchMurnaghanPV_EOS(pv_pairs) 207 | equil_volume = params[0] 208 | except: 209 | warnings.warn("Could not converge Birch-Murnaghan EOS fit, trying linear regression") 210 | rescale_type = 'linear_regression' 211 | 212 | pvs = fw_spec["pressure_volume"] 213 | p = [item[1] for item in pvs] 214 | v = [item[0] for item in pvs] 215 | if rescale_type == 'linear_regression': 216 | slope, intercept, r_value, p_value, std_err = stats.linregress(v, p) 217 | if slope >= 0: 218 | ## In future try building a hull with composition and volume. then getting composition volume 219 | raise ValueError("P and V should be inversely related. Try using larger NSW in the volume variation") 220 | equil_volume = -intercept / slope 221 | 222 | frac_change = equil_volume / sorted(v)[int(np.floor(len(v) / 2))] 223 | if frac_change > 2 or frac_change < 0.5: 224 | # If volume is greater than 2x or 0.5x, use the lowest pressure volume. 225 | equil_volume = v[np.argmin(p)] 226 | 227 | poscar = Poscar.from_file("./POSCAR") 228 | poscar.structure.scale_lattice(equil_volume) 229 | poscar.write_file("./POSCAR") 230 | 231 | return FWAction() 232 | -------------------------------------------------------------------------------- /mpmorph/firetasks/dbtasks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import zlib 5 | 6 | import gridfs 7 | import numpy as np 8 | from atomate.common.firetasks.glue_tasks import get_calc_loc 9 | from atomate.utils.utils import env_chk, get_logger 10 | from atomate.vasp.drones import VaspDrone 11 | from bson import ObjectId 12 | from mpmorph.database import VaspMDCalcDb 13 | from fireworks import explicit_serialize, FiretaskBase, FWAction 14 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 15 | from monty.json import MontyEncoder 16 | from pymatgen.core import Structure 17 | from pymatgen.core.trajectory import Trajectory 18 | from collections import defaultdict 19 | from mpmorph.database import convert_ionic_steps_to_trajectory 20 | 21 | __author__ = 'Eric Sivonxay and Jianli Cheng' 22 | 23 | logger = get_logger(__name__) 24 | 25 | 26 | @explicit_serialize 27 | class VaspMDToDb(FiretaskBase): 28 | """ 29 | Enter a VASP run into the database. Uses current directory unless you 30 | specify calc_dir or calc_loc. 31 | Optional params: 32 | calc_dir (str): path to dir (on current filesystem) that contains VASP 33 | output files. Default: use current working directory. 34 | calc_loc (str OR bool): if True will set most recent calc_loc. If str 35 | search for the most recent calc_loc with the matching name 36 | parse_dos (bool): whether to parse the DOS and store in GridFS. 37 | Defaults to False. 38 | bandstructure_mode (str): Set to "uniform" for uniform band structure. 39 | Set to "line" for line mode. If not set, band structure will not 40 | be parsed. 41 | additional_fields (dict): dict of additional fields to add 42 | db_file (str): path to file containing the database credentials. 43 | Supports env_chk. Default: write data to JSON file. 44 | fw_spec_field (str): if set, will update the task doc with the contents 45 | of this key in the fw_spec. 46 | defuse_unsuccessful (bool): Defuses children fireworks if VASP run state 47 | is not "successful"; i.e. both electronic and ionic convergence are reached. 48 | Defaults to True. 49 | """ 50 | optional_params = ["calc_dir", "calc_loc", "parse_dos", "bandstructure_mode", 51 | "additional_fields", "db_file", "fw_spec_field", 52 | "md_structures", "defuse_unsuccessful"] 53 | 54 | def run_task(self, fw_spec): 55 | # get the directory that contains the VASP dir to parse 56 | calc_dir = os.getcwd() 57 | if "calc_dir" in self: 58 | calc_dir = self["calc_dir"] 59 | elif self.get("calc_loc"): 60 | calc_dir = get_calc_loc(self["calc_loc"], fw_spec["calc_locs"])["path"] 61 | 62 | # parse the VASP directory 63 | logger.info("PARSING DIRECTORY: {}".format(calc_dir)) 64 | 65 | drone = VaspDrone(additional_fields=self.get("additional_fields"), 66 | parse_dos=self.get("parse_dos", False), 67 | parse_bader=False, 68 | bandstructure_mode=self.get("bandstructure_mode", False), 69 | store_volumetric_data=[]) 70 | 71 | # assimilate (i.e., parse) 72 | task_doc = drone.assimilate(calc_dir) 73 | 74 | # Check for additional keys to set based on the fw_spec 75 | if self.get("fw_spec_field"): 76 | task_doc.update(fw_spec[self.get("fw_spec_field")]) 77 | 78 | # get the database connection 79 | db_file = env_chk(self.get('db_file'), fw_spec) 80 | 81 | # db insertion or taskdoc dump 82 | if not db_file: 83 | with open("task.json", "w") as f: 84 | f.write(json.dumps(task_doc, default=DATETIME_HANDLER)) 85 | else: 86 | mmdb = VaspMDCalcDb.from_db_file(db_file, admin=True) 87 | 88 | # prevent duplicate insertion 89 | mmdb.db.tasks.find_one_and_delete({'formula_pretty': task_doc['formula_pretty'], 90 | 'task_label': task_doc['task_label']}) 91 | 92 | t_id = mmdb.insert_task(task_doc, 93 | parse_dos=self.get("parse_dos", False), 94 | parse_bs=bool(self.get("bandstructure_mode", False)), 95 | parse_ionic_steps=self.get("md_structures", True)) 96 | 97 | logger.info("Finished parsing with task_id: {}".format(t_id)) 98 | 99 | if self.get("defuse_unsuccessful", True): 100 | defuse_children = (task_doc["state"] != "successful") 101 | else: 102 | defuse_children = False 103 | 104 | return FWAction(stored_data={"task_id": task_doc.get("task_id", None)}, 105 | defuse_children=defuse_children) 106 | 107 | 108 | @explicit_serialize 109 | class TrajectoryDBTask(FiretaskBase): 110 | """ 111 | Obtain all production runs with the same uuid, stitch together the trajectory, and then insert them into the db. 112 | This is done by searching for the unique tag 113 | """ 114 | required_params = ["tag_id", "db_file"] 115 | optional_params = ['notes'] 116 | 117 | def run_task(self, fw_spec): 118 | notes = self.get('notes', None) 119 | tag_id = self['tag_id'] 120 | 121 | # get the database connection 122 | db_file = env_chk(self.get('db_file'), fw_spec) 123 | mmdb = VaspMDCalcDb.from_db_file(db_file, admin=True) 124 | # mmdb.db.trajectories.find_one_and_delete({"runs_label": tag_id}) 125 | runs = mmdb.db['tasks'].find( 126 | {"task_label": re.compile(f'.*prod_run.*{tag_id}.*')}) 127 | 128 | runs_sorted = sorted(runs, key=lambda x: int(re.findall('run[_-](\d+)', x['task_label'])[0])) 129 | 130 | # Remove duplicates of the same run (if they exist) 131 | labels = [result['task_label'] for result in runs_sorted] 132 | nums = [int(re.findall('run[_-](\d+)', label)[0]) for label in labels] 133 | duplicates = np.where((nums - np.roll(nums, 1)) == 0)[0] 134 | runs_sorted = [runs_sorted[i] for i in range(len(runs_sorted)) if i not in duplicates] 135 | 136 | trajectory_doc = runs_to_trajectory_doc(runs_sorted, db_file, tag_id, notes) 137 | 138 | mmdb.db.trajectories.insert_one(trajectory_doc) 139 | 140 | 141 | def runs_to_trajectory_doc(runs, db_file, runs_label, notes=None): 142 | """ 143 | Takes a list of task_documents, aggregates the trajectories from the ionics_steps gridfs storage, then dumps 144 | the pymatgen.core.Trajectory object into the 'trajectories_fs' collection and makes a dictionary doc to track 145 | the entry. 146 | 147 | :param runs: list of MD runs 148 | :param db_file: 149 | :param runs_label: unique identifier to the runs 150 | :param notes: (optional) any notes or comments on the specific run 151 | :return: 152 | """ 153 | mmdb = VaspMDCalcDb.from_db_file(db_file, admin=True) 154 | 155 | trajectory = load_trajectories_from_gfs(runs, mmdb) 156 | 157 | traj_dict = json.dumps(trajectory, cls=MontyEncoder) 158 | gfs_id, compression_type = insert_gridfs(traj_dict, mmdb.db, "trajectories_fs") 159 | 160 | traj_doc = { 161 | 'formula_pretty': trajectory[0].composition.reduced_formula, 162 | 'formula': trajectory[0].composition.formula.replace(' ', ''), 163 | 'temperature': int(runs[0]["input"]["incar"]["TEBEG"]), 164 | 'runs_label': runs_label, 165 | 'compression': compression_type, 166 | 'fs_id': gfs_id, 167 | 'fs': 'trajectories_fs', 168 | 'step_fs_ids': [i["calcs_reversed"][0]["output"]['ionic_steps_fs_id'] 169 | for i in runs], 170 | 'structure': trajectory[0].as_dict(), 171 | 'dimension': list(np.shape(trajectory.frac_coords)), 172 | 'time_step': runs[0]["input"]["incar"]["POTIM"] * 1e-3, 173 | 'frame_properties': list(trajectory[0].frame_properties.keys()), 174 | 'notes': notes 175 | } 176 | return traj_doc 177 | 178 | def load_trajectories_from_gfs(runs, mmdb, gfs_keys=None): 179 | if gfs_keys is None: 180 | # Attempt to automatically determine where the trajectory is stored (for compatibility with older mpmorph) 181 | gfs_keys = [] 182 | for run in runs: 183 | # 3 cases to deal with: 1) Trajectory 2) previous_runs (old mpmorph) 3) structures_fs 184 | if 'trajectory' in run.keys(): 185 | gfs_keys.append((run['trajectory']['fs_id'], 'trajectories_fs')) 186 | elif "INCAR" in run.keys(): 187 | # for backwards compatibility with older version of mpmorph 188 | gfs_keys.append((run["ionic_steps_fs_id"], 'previous_runs_gfs')) 189 | elif "input" in run.keys(): 190 | gfs_keys.append((run["calcs_reversed"][0]["output"]["ionic_steps_fs_id"], 'structures_fs')) 191 | 192 | trajectory = None 193 | for i, (fs_id, fs) in enumerate(gfs_keys): 194 | 195 | if fs == 'trajectories_fs' or fs == 'rebuild_trajectories_fs': 196 | # Load stored Trajectory 197 | print(fs_id, 'is stored in trajectories_fs') 198 | _trajectory = load_trajectory(fs_id=fs_id, db=mmdb.db, fs=fs) 199 | else: 200 | # Load Ionic steps from gfs, then convert to trajectory before extending 201 | # (compatibility code for when mpmorph stored trajectories as a list of structure dicts) 202 | ionic_steps_dict = load_ionic_steps(fs_id=fs_id, db=mmdb.db, fs=fs) 203 | _trajectory = convert_ionic_steps_to_trajectory((ionic_steps_dict)) 204 | if trajectory is None: 205 | trajectory = _trajectory 206 | else: 207 | # Eliminate duplicate structure at the start of each trajectory 208 | # (since vasp will output the input structure) 209 | trajectory.extend(_trajectory[1:]) 210 | return trajectory 211 | 212 | 213 | def process_traj(data): 214 | i, fs_id, fs, db_file = data[0], data[1], data[2], data[3] 215 | mmdb = VaspMDCalcDb.from_db_file(db_file, admin=True) 216 | ionic_steps_dict = load_ionic_steps(fs_id, mmdb.db, fs) 217 | 218 | structure = Structure.from_dict(ionic_steps_dict[0]['structure']) 219 | positions = [0] * len(ionic_steps_dict) 220 | for i, step in enumerate(ionic_steps_dict): 221 | _step = [atom['abc'] for atom in step["structure"]["sites"]] 222 | positions[i] = _step 223 | 224 | traj = Trajectory(structure.lattice.matrix, structure.species, positions, 0.002) 225 | return i, traj.as_dict() 226 | 227 | 228 | def load_trajectory(fs_id, db, fs=None): 229 | if not fs: 230 | # Default to trajectories_fs 231 | fs = gridfs.GridFS(db, 'trajectories_fs') 232 | elif not isinstance(fs, gridfs.GridFS): 233 | # Handle fs supplied as str 234 | fs = gridfs.GridFS(db, fs) 235 | 236 | trajectories_json = zlib.decompress(fs.get(fs_id).read()) 237 | trajectories_dict = json.loads(trajectories_json.decode()) 238 | try: 239 | trajectory = Trajectory.from_dict(trajectories_dict) 240 | except AttributeError: 241 | trajectories_dict = json.loads(trajectories_dict) 242 | trajectory = Trajectory.from_dict(trajectories_dict) 243 | return trajectory 244 | 245 | 246 | def load_ionic_steps(fs_id, db, fs): 247 | if not isinstance(fs, gridfs.GridFS): 248 | fs = gridfs.GridFS(db, fs) 249 | ionic_steps_json = zlib.decompress(fs.get(fs_id).read()) 250 | ionic_steps_dict = json.loads(ionic_steps_json.decode()) 251 | del ionic_steps_json 252 | return ionic_steps_dict 253 | 254 | 255 | def insert_gridfs(d, db, collection="fs", compress=True, oid=None, task_id=None): 256 | """ 257 | Insert the given document into GridFS. 258 | Args: 259 | d (dict): the document 260 | collection (string): the GridFS collection name 261 | compress (bool): Whether to compress the data or not 262 | oid (ObjectId()): the _id of the file; if specified, it must not already exist in GridFS 263 | task_id(int or str): the task_id to store into the gridfs metadata 264 | Returns: 265 | file id, the type of compression used. 266 | """ 267 | oid = oid or ObjectId() 268 | compression_type = None 269 | 270 | if compress: 271 | d = zlib.compress(d.encode(), compress) 272 | compression_type = "zlib" 273 | 274 | fs = gridfs.GridFS(db, collection) 275 | if task_id: 276 | # Putting task id in the metadata subdocument as per mongo specs: 277 | # https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst#terms 278 | fs_id = fs.put(d, _id=oid, metadata={"task_id": task_id, "compression": compression_type}) 279 | else: 280 | fs_id = fs.put(d, _id=oid, metadata={"compression": compression_type}) 281 | 282 | return fs_id, compression_type 283 | -------------------------------------------------------------------------------- /mpmorph/runners/amorphous_maker.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import os 4 | import shutil 5 | from collections import OrderedDict 6 | 7 | import numpy as np 8 | from pymatgen.core import Structure, Composition 9 | from pymatgen.ext.matproj import MPRester 10 | from pymatgen.io.vasp.inputs import Poscar 11 | 12 | from typing import Union, Optional, List 13 | 14 | __author__ = 'Eric Sivonxay, Jianli Cheng, and Muratahan Aykol' 15 | __maintainer__ = 'Eric Sivonxay' 16 | __email__ = 'esivonxay@lbl.gov' 17 | 18 | 19 | class AmorphousMaker(object): 20 | def __init__(self, 21 | el_num_dict:dict, 22 | box_scale:Union[float, List[float]], 23 | tol:float=2.0, 24 | packmol_path:str="packmol", 25 | clean:bool=True, 26 | xyz_paths:List=None, 27 | time_seed:bool=True): 28 | """ 29 | Class for generating initial constrained-random packed structures for the 30 | simulation of amorphous or liquid structures. This is a wrapper for "packmol" package. 31 | Only works for cubic boxes for now. 32 | Args: 33 | el_num_dict (dict): dictionary of number of atoms of each species. If 34 | number of molecules is specified, an xyz file with the same name needs to be provided as xyz_paths. 35 | e.g. {"V":22, "Li":10, "O":75, "B":10} 36 | e.g. {"H2O": 20} 37 | box_scale (float) or (numpy array): all lattice vectors are multiplied with this. 38 | e.g. if one scalar value is given, it is the edge length of a cubic simulation box 39 | e.g. if np.array([1.2, 0.9, 1.0]) is given, the unit lattice vectors will be multiplied with this. 40 | tol (float): tolerance factor for how close the atoms can get (angstroms). 41 | e.g. tol = 2.0 angstroms 42 | packmol_path (str): path to the packmol executable 43 | clean (bool): whether the intermedite files generated are deleted. 44 | xyz_paths (list): list of paths (str) to xyz files correpsonding to molecules, if given so in el_num_dict. 45 | file names must match the molecule formula. 46 | time_seed (bool): whether to generate a random seed based on system time 47 | """ 48 | self.el_num_dict = el_num_dict 49 | self.box_scale = box_scale 50 | self.tol = tol 51 | self._lattice = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] 52 | self._structure = None 53 | self._el_dict = None 54 | self.packmol_path = packmol_path 55 | self.clean = clean 56 | self.xyz_paths = xyz_paths 57 | self.time_seed = time_seed 58 | if self.xyz_paths: 59 | assert len(self.xyz_paths) == len(self.el_num_dict.keys()) 60 | self.clean = False 61 | 62 | def __repr__(self): 63 | return "AmorphousMaker: generates constrained-random packed initial structure for MD using packmol." 64 | 65 | @property 66 | def box(self): 67 | """ 68 | Returns: box vectors scaled with box_scale 69 | """ 70 | return (np.array(self._lattice) * self.box_scale).tolist() 71 | 72 | @property 73 | def random_packed_structure(self): 74 | """ 75 | Returns: A constrained-random packed Structure object 76 | """ 77 | self._el_dict = self.call_packmol() 78 | self._structure = self.get_structure(self._el_dict, self.box) 79 | return self._structure 80 | 81 | def call_packmol(self): 82 | """ 83 | Returns: 84 | A dict of coordinates of atoms for each element type 85 | e.g. {'V': [[4.969925, 8.409291, 5.462153], [9.338829, 9.638388, 9.179811], ...] 86 | 'Li': [[5.244308, 8.918049, 1.014577], [2.832759, 3.605796, 2.330589], ...]} 87 | """ 88 | 89 | # this ensures periodic boundaries don't cause problems 90 | pm_l = self.tol / 2 91 | pm_h = self.box_scale - self.tol / 2 92 | try: 93 | len(pm_h) 94 | except: 95 | pm_h = [pm_h for i in range(3)] 96 | 97 | with open("packmol.input", "w") as f: 98 | f.write("tolerance " + str(self.tol) + "\nfiletype xyz\noutput mixture.xyz\n") 99 | for el in self.el_num_dict: 100 | f.write("structure " + el + ".xyz\n" + " number " + str(self.el_num_dict[el]) 101 | + "\n inside box" + 3 * (" " + str(pm_l)) 102 | + (" " + str(pm_h[0])) + (" " + str(pm_h[1])) + (" " + str(pm_h[2])) 103 | + "\nend structure\n\n") 104 | 105 | if self.time_seed: 106 | f.write("seed -1\n") 107 | 108 | if self.xyz_paths: 109 | for path in self.xyz_paths: 110 | try: 111 | shutil.copy2(path, './') 112 | except: 113 | pass 114 | else: 115 | for el in self.el_num_dict.keys(): 116 | with open(el + ".xyz", "w") as f: 117 | f.write("1\ncomment\n" + el + " 0.0 0.0 0.0\n") 118 | 119 | try: 120 | os.system(self.packmol_path + " < packmol.input") 121 | except: 122 | raise OSError("packmol cannot be found!") 123 | if self.clean: 124 | for el in self.el_num_dict.keys(): 125 | os.system("rm " + el + ".xyz") 126 | os.system("rm packmol.input") 127 | return self.xyz_to_dict("mixture.xyz") 128 | 129 | def xyz_to_dict(self, 130 | filename:str): 131 | """ 132 | This is a generic xyz to dictionary convertor. 133 | Used to get the structure from packmol output. 134 | """ 135 | with open(filename, 'r') as f: 136 | lines = f.readlines() 137 | N = int(lines[0].rstrip('\n')) 138 | el_dict = {} 139 | for line in lines[2:]: 140 | l = line.rstrip('\n').split() 141 | if l[0] in el_dict: 142 | el_dict[l[0]].append([float(i) for i in l[1:]]) 143 | else: 144 | el_dict[l[0]] = [[float(i) for i in l[1:]]] 145 | if N != sum([len(x) for x in el_dict.values()]): 146 | raise ValueError("Inconsistent number of atoms") 147 | self._el_dict = OrderedDict(el_dict) 148 | if self.clean: 149 | os.system("rm " + filename) 150 | return self._el_dict 151 | 152 | @staticmethod 153 | def get_structure(el_dict:dict, 154 | lattice:List[List]): 155 | """ 156 | Args: 157 | el_dict (dict): coordinates of atoms for each element type 158 | e.g. {'V': [[4.969925, 8.409291, 5.462153], [9.338829, 9.638388, 9.179811], ...] 159 | 'Li': [[5.244308, 8.918049, 1.014577], [2.832759, 3.605796, 2.330589], ...]} 160 | lattice (list): is the lattice in the form of [[x1,x2,x3],[y1,y2,y3],[z1,z2,z3]] 161 | Returns: pymatgen Structure 162 | """ 163 | species = [] 164 | coords = [] 165 | for el in el_dict.keys(): 166 | for atom in el_dict[el]: 167 | species.append(el) 168 | coords.append(atom) 169 | return Structure(lattice, species, coords, coords_are_cartesian=True) 170 | 171 | def get_poscar(self): 172 | return Poscar(self.random_packed_structure) 173 | 174 | @staticmethod 175 | def xyzdict_to_poscar(el_dict:dict, 176 | lattice:List[List], 177 | filepath:str="POSCAR"): 178 | """ 179 | Generates XYZ file from element coordinate dictionary and lattice 180 | Args: 181 | el_dict (dict): coordinates of atoms for each element type 182 | e.g. {'V': [[4.969925, 8.409291, 5.462153], [9.338829, 9.638388, 9.179811], ...] 183 | 'Li': [[5.244308, 8.918049, 1.014577], [2.832759, 3.605796, 2.330589], ...]} 184 | lattice (list): is the lattice in the form of [[x1,x2,x3],[y1,y2,y3],[z1,z2,z3]] 185 | filepath (str): path to POSCAR to be generated 186 | Returns: 187 | writes a POSCAR file 188 | """ 189 | with open(filepath, "w") as f: 190 | f.write("Parsed form XYZ file\n") 191 | f.write("1.0\n") 192 | for vec in lattice: 193 | f.write(" ".join([str(v) for v in vec]) + "\n") 194 | el_dict = OrderedDict(el_dict) 195 | for key in el_dict.keys(): 196 | f.write(key + " ") 197 | f.write("\n") 198 | for key in el_dict.keys(): 199 | f.write(str(len(el_dict[key])) + " ") 200 | f.write("\nCartesian\n") 201 | for key in el_dict.keys(): 202 | for atom in el_dict[key]: 203 | f.write(" ".join([str(i) for i in atom]) + "\n") 204 | 205 | 206 | 207 | 208 | def get_random_packed(composition: Union[Composition, str], 209 | add_specie=None, 210 | target_atoms: int = 100, 211 | vol_per_atom:float=None, 212 | vol_exp:float=1.0, 213 | modify_species: dict = None, 214 | use_time_seed: bool = True, 215 | mpr: Optional[MPRester] = None): 216 | """ 217 | Helper method to use the AmorphousMaker to generate a randomly packed unit cell. If the volume (per atom) of the 218 | unit cell is not provided, the volume per atom will be predicted using structures from the materials project. 219 | 220 | :param composition: Formula or composition of the desired unit cell 221 | :param add_specie: 222 | :param target_atoms: Desired number of atoms in the unit cell. Generated unit cell will have at least this many 223 | atoms, although may be greater if the number of atoms in the specified formula/composition is evenly 224 | divide target_atoms. 225 | :param vol_per_atom: Volume of the unit cell in cubic Angstroms per atom (A^3/atom) 226 | :param vol_exp: Factor to multiply the volume by. 227 | :param modify_species: 228 | :param use_time_seed: Whether or not to use a time based random seed. Defaults to true 229 | :param mpr: custom MPRester object if additional specifications needed (such as API or endpoint) 230 | :return: 231 | """ 232 | 233 | if type(composition) == str: 234 | composition = Composition(composition) 235 | if type(add_specie) == str: 236 | add_specie = Composition(add_specie) 237 | 238 | if vol_per_atom is None: 239 | if mpr is None: 240 | mpr = MPRester() 241 | 242 | comp_entries = mpr.get_entries(composition.reduced_formula, 243 | inc_structure=True) 244 | if len(comp_entries) > 0: 245 | vols = np.min([entry.structure.volume / entry.structure.num_sites 246 | for entry in comp_entries]) 247 | else: 248 | # Find all Materials project entries containing the elements in the 249 | # desired composition to estimate starting volume. 250 | _entries = mpr.get_entries_in_chemsys( 251 | [str(el) for el in composition.elements], inc_structure=True) 252 | entries = [] 253 | for entry in _entries: 254 | if set(entry.structure.composition.elements) == set(composition.elements): 255 | entries.append(entry) 256 | if len(entry.structure.composition.elements) >= 2: 257 | entries.append(entry) 258 | 259 | vols = [entry.structure.volume / entry.structure.num_sites 260 | for entry in entries] 261 | vol_per_atom = np.mean(vols) 262 | 263 | # Find total composition of atoms in the unit cell 264 | formula, factor = composition.get_integer_formula_and_factor() 265 | integer_composition = Composition(formula) 266 | full_cell_composition = integer_composition * np.ceil(target_atoms / integer_composition.num_atoms) 267 | if add_specie is not None: 268 | full_cell_composition += add_specie 269 | 270 | # Generate dict of elements and amounts for AmorphousMaker 271 | structure = {} 272 | for el in full_cell_composition: 273 | structure[str(el)] = int(full_cell_composition.element_composition.get(el)) 274 | 275 | if modify_species is not None: 276 | for i, v in modify_species.items(): 277 | structure[i] += v 278 | # use packmol to get a random configured structure 279 | packmol_path = os.environ['PACKMOL_PATH'] 280 | amorphous_maker_params = {'box_scale': (vol_per_atom * full_cell_composition.num_atoms * vol_exp) ** (1 / 3), 281 | 'packmol_path': packmol_path, 'xyz_paths': None, 'time_seed': use_time_seed} 282 | 283 | glass = AmorphousMaker(structure, **amorphous_maker_params) 284 | structure = glass.random_packed_structure 285 | return structure 286 | -------------------------------------------------------------------------------- /mpmorph/analysis/structural_analysis.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from copy import deepcopy 3 | from multiprocessing import Pool 4 | 5 | import numpy as np 6 | from pymatgen.analysis import structure_analyzer 7 | from pymatgen.io.vasp.outputs import Xdatcar 8 | from pymatgen.util.coord import get_angle 9 | from scipy.spatial import Voronoi 10 | 11 | __author__ = 'Muratahan Aykol ' 12 | 13 | 14 | def polyhedra_connectivity(structures, pair, cutoff, step_freq=1): 15 | """ 16 | Args: 17 | structures: 18 | pair: 19 | cutoff: 20 | step_freq: 21 | given: Given polyhedra are of this 22 | 23 | Returns: 24 | 25 | """ 26 | n_frames = len(structures) 27 | center_atom = pair[0] 28 | shell_atom = pair[1] 29 | 30 | connectivity = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0} 31 | connectivity_template = deepcopy(connectivity) 32 | connectivity_sub_categories = {} 33 | 34 | for s_index in itertools.count(0, step_freq): 35 | if s_index >= n_frames: 36 | break 37 | structure = structures[s_index] 38 | 39 | polyhedra_list = [] 40 | 41 | for i in range(len(structure)): 42 | current_poly = [] 43 | if str(structure[i].specie) == center_atom: 44 | for j in range(len(structure)): 45 | if str(structure[j].specie) == shell_atom: 46 | d = structure.get_distance(i, j) 47 | if d < cutoff: 48 | current_poly.append(j) 49 | polyhedra_list.append(set(current_poly)) 50 | 51 | for polypair in itertools.combinations(polyhedra_list, 2): 52 | 53 | polyhedra_pair_type = (len(polypair[0]), len(polypair[1])) 54 | 55 | shared_vertices = len(polypair[0].intersection(polypair[1])) 56 | 57 | if shared_vertices in connectivity: 58 | connectivity[shared_vertices] += 1 59 | 60 | if shared_vertices: 61 | if polyhedra_pair_type in connectivity_sub_categories: 62 | if shared_vertices in connectivity_sub_categories[polyhedra_pair_type]: 63 | connectivity_sub_categories[polyhedra_pair_type][shared_vertices] += 1 64 | elif polyhedra_pair_type[::-1] in connectivity_sub_categories: 65 | if shared_vertices in connectivity_sub_categories[polyhedra_pair_type[::-1]]: 66 | connectivity_sub_categories[polyhedra_pair_type[::-1]][shared_vertices] += 1 67 | else: 68 | connectivity_sub_categories[polyhedra_pair_type] = deepcopy(connectivity_template) 69 | if shared_vertices in connectivity_sub_categories[polyhedra_pair_type]: 70 | connectivity_sub_categories[polyhedra_pair_type][shared_vertices] = 1 71 | return connectivity, connectivity_sub_categories 72 | 73 | 74 | def coordination_number_distribution(structures, pair, cutoff, step_freq=1): 75 | """ 76 | Calculates coordination number distribution 77 | Args: 78 | structures: 79 | pair: 80 | cutoff: 81 | step_freq: 82 | 83 | Returns: 84 | 85 | """ 86 | 87 | cn_list = [] 88 | n_frames = len(structures) 89 | for s_index in itertools.count(0, step_freq): 90 | if s_index >= n_frames: 91 | break 92 | structure = structures[s_index] 93 | for i in range(len(structure)): 94 | if str(structure[i].specie) == pair[0]: 95 | cn = 0 96 | for j in range(len(structure)): 97 | if str(structure[j].specie) == pair[1]: 98 | d = structure.get_distance(i, j) 99 | if d < cutoff and np.abs(d) > 0.1: 100 | cn += 1 101 | cn_list.append(cn) 102 | return cn_list 103 | 104 | 105 | def get_cn(structure, pair, cutoff): 106 | cn_list = [] 107 | for i in range(len(structure)): 108 | if str(structure[i].specie) == pair[0]: 109 | cn = 0 110 | for j in range(len(structure)): 111 | if str(structure[j].specie) == pair[1]: 112 | d = structure.get_distance(i, j) 113 | if d < cutoff and np.abs(d) > 0.1: 114 | cn += 1 115 | cn_list.append(cn) 116 | return cn_list 117 | 118 | 119 | class BondAngleDistribution(object): 120 | """ 121 | Bond Angle Distribution 122 | Args: 123 | structures (list): list of structures 124 | cutoffs (dict): a dictionary of cutoffs where keys are tuples of pairs ('A','B') 125 | step_freq: calculate every this many steps 126 | Attributes: 127 | bond_angle_distribution (dict) 128 | """ 129 | 130 | def __init__(self, structures, cutoffs, step_freq=1): 131 | 132 | self.bond_angle_distribution = None 133 | self.structures = structures 134 | self.step_freq = step_freq 135 | self.unique_triplets = self.get_unique_triplets(structures[0]) 136 | if isinstance(cutoffs, dict): 137 | self.cutoffs = cutoffs 138 | self._cutoff_type = 'dict' 139 | elif isinstance(cutoffs, float): 140 | self.cutoffs = cutoffs 141 | self._cutoff_type = 'constant' 142 | else: 143 | raise ValueError("Cutoffs must be specified as dict of pairs or globally as a single flaot.") 144 | 145 | @property 146 | def n_frames(self): 147 | return len(self.structures) 148 | 149 | def get_angle(self, s_index, i, j, k): 150 | """ 151 | Returns **Minimum Image** angle specified by three sites. 152 | 153 | Args: 154 | s_index: Structure index in structures list 155 | i (int): Index of first site. 156 | j (int): Index of second site. 157 | k (int): Index of third site. 158 | 159 | Returns: 160 | (float) Angle in degrees. 161 | """ 162 | structure = self.structures[s_index] 163 | lat_vec = np.array([structure.lattice.a, structure.lattice.b, structure.lattice.c]) 164 | 165 | v1 = structure[i].coords - structure[j].coords 166 | v2 = structure[k].coords - structure[j].coords 167 | 168 | for v in range(3): 169 | if np.fabs(v1[v]) > lat_vec[v] / 2.0: 170 | v1[v] -= np.sign(v1[v]) * lat_vec[v] 171 | if np.fabs(v2[v]) > lat_vec[v] / 2.0: 172 | v2[v] -= np.sign(v2[v]) * lat_vec[v] 173 | return get_angle(v1, v2, units="degrees") 174 | 175 | @staticmethod 176 | def get_unique_triplets(s): 177 | central_atoms = s.symbol_set 178 | import itertools 179 | possible_end_members = [] 180 | for i in itertools.combinations_with_replacement(central_atoms, 2): 181 | possible_end_members.append(i) 182 | unique_triplets = [] 183 | for i in central_atoms: 184 | for j in possible_end_members: 185 | triplet = (j[0], i, j[1]) 186 | unique_triplets.append(triplet) 187 | return unique_triplets 188 | 189 | def _check_skip_triplet(self, s_index, i, n1, n2): 190 | """ 191 | Helper method to find if a triplet should be skipped 192 | Args: 193 | s_index: index of structure in self.structures 194 | i: index of the central site 195 | n1: index of the first neighbor site 196 | n2: index of the second neighbor site 197 | Returns: 198 | True if pair distance is longer than specified in self.cutoffs 199 | """ 200 | ns = [n1, n2] 201 | s = self.structures[s_index] 202 | skip_triplet = False 203 | for j in ns: 204 | pair = (s[i].species_string, s[j].species_string) 205 | if pair not in self.cutoffs: 206 | pair = pair[::-1] 207 | if s.get_distance(i, j) > self.cutoffs[pair]: 208 | skip_triplet = True 209 | break 210 | return skip_triplet 211 | 212 | def get_bond_angle_distribution(self): 213 | bond_angle_dict = {} 214 | for triplet in self.unique_triplets: 215 | bond_angle_dict[triplet] = np.zeros(180 + 1) 216 | 217 | for s_index in itertools.count(0, self.step_freq): 218 | if s_index >= self.n_frames: 219 | break 220 | s = self.structures[s_index] 221 | 222 | # Narrow down the search space around a given atom for neighbors 223 | if self._cutoff_type == 'dict': 224 | neighbor_search_cutoff = max(self.cutoffs.values()) 225 | else: 226 | neighbor_search_cutoff = self.cutoffs 227 | neighbors = s.get_all_neighbors(neighbor_search_cutoff, include_index=True) 228 | 229 | for i in range(len(s)): 230 | el_origin = s[i].species_string 231 | 232 | # get all pair combinations of neoghbor sites of i: 233 | for p in itertools.combinations(neighbors[i], 2): 234 | 235 | # check if pairs are within the defined cutoffs 236 | if self._cutoff_type == 'dict': 237 | if self._check_skip_triplet(s_index, i, p[0][2], p[1][2]): 238 | continue 239 | else: 240 | pass 241 | 242 | angle = self.get_angle(s_index, p[0][2], i, p[1][2]) 243 | 244 | # round to nearest integer 245 | angle = int(np.rint([angle])[0]) 246 | el1 = p[0][0].species_string 247 | el2 = p[1][0].species_string 248 | if (el1, el_origin, el2) in bond_angle_dict: 249 | bond_angle_dict[(el1, el_origin, el2)][angle] += 1 250 | elif (el2, el_origin, el1) in bond_angle_dict: 251 | bond_angle_dict[(el2, el_origin, el1)][angle] += 1 252 | else: 253 | print(el1, el_origin, el2) 254 | raise KeyError("Problem finding the triplet!!!") 255 | for triplet in bond_angle_dict: 256 | total = np.sum(bond_angle_dict[triplet]) 257 | if total != 0.0: 258 | bond_angle_dict[triplet] /= total 259 | self.bond_angle_distribution = bond_angle_dict 260 | 261 | def plot_bond_angle_distribution(self): 262 | import matplotlib.pyplot as plt 263 | if not self.bond_angle_distribution: 264 | self.get_bond_angle_distribution() 265 | plt.figure() 266 | triplets = self.bond_angle_distribution.keys() 267 | legend = [] 268 | for trip in triplets: 269 | legend.append('-'.join(trip)) 270 | for triplet in triplets: 271 | plt.plot(range(180 + 1), self.bond_angle_distribution[triplet]) 272 | 273 | plt.xlabel("Angle (degrees)") 274 | plt.ylabel("Frequency (fractional)") 275 | plt.legend(legend, loc=0) 276 | return plt 277 | 278 | def get_binary_angle_dist_plot(self, title=None): 279 | import matplotlib.pyplot as plt 280 | fig = plt.figure(figsize=(12, 6)) 281 | c = 0 282 | maxes = [] 283 | for triplet in self.bond_angle_distribution: 284 | p = self.bond_angle_distribution[triplet] 285 | c += 1 286 | ax = fig.add_subplot(2, 3, c) 287 | ax.plot(range(len(p)), p) 288 | maxes.append(max(p)) 289 | ax.annotate('-'.join(triplet), (0.75, 0.88), xycoords='axes fraction', size=16) 290 | ax.set_yticklabels([]) 291 | ax.xaxis.set_ticks(np.arange(0, 181, 30)) 292 | plt.gca().set_ylim([0, 0.1]) 293 | if c in [1, 2, 3]: 294 | ax.set_xticklabels([]) 295 | else: 296 | plt.xlabel('Angle (degrees)', fontsize=16) 297 | if c in [1, 4]: 298 | plt.ylabel('Intensity (a.u.)', fontsize=16) 299 | ax.tick_params(axis='both', which='major', labelsize=16) 300 | if c == 2: 301 | if title: 302 | plt.title(title) 303 | for ax in fig.axes: 304 | ax.set_ylim(0.0, max(maxes)) 305 | fig.subplots_adjust(top=0.75) 306 | fig.tight_layout() 307 | return fig 308 | 309 | 310 | def compute_mean_coord(structures, freq=100): 311 | ''' 312 | NOTE: This function will be removed as it has been migrated 313 | to pymatgen. 314 | Calculate average coordination numbers 315 | With voronoi polyhedra construction 316 | args: 317 | - structures: list of Structures 318 | - freq: sampling frequency of coord number [every freq steps] 319 | returns: 320 | - a dictionary of elements and corresponding mean coord numbers 321 | ''' 322 | cn_dict = {} 323 | for el in structures[0].composition.elements: 324 | cn_dict[el.name] = 0.0 325 | count = 0 326 | for t in range(len(structures)): 327 | if t % freq != 0: 328 | continue 329 | count += 1 330 | vor = structure_analyzer.VoronoiCoordFinder(structures[t]) 331 | for atom in range(len(structures[0])): 332 | CN = vor.get_coordination_number(atom) 333 | cn_dict[structures[t][atom].species_string] += CN 334 | el_dict = structures[0].composition.as_dict() 335 | for el in cn_dict: 336 | cn_dict[el] = cn_dict[el] / el_dict[el] / count 337 | return cn_dict 338 | 339 | 340 | class VoronoiAnalysis(object): 341 | """ 342 | NOTE: This class has also been migrated to pymatgen so will be removed! 343 | """ 344 | 345 | def __init__(self): 346 | self.vor_ensemble = None 347 | 348 | @staticmethod 349 | def voronoi_analysis(structure, n=0, cutoff=5.0, qhull_options="Qbb Qc Qz"): 350 | """ 351 | Performs voronoi analysis and returns the polyhedra around atom n 352 | in Schlaefli notation. 353 | 354 | Note: Part of this function is obtained from the pymatgen VoronoiCoordinationFinder 355 | and should be merged with that function in future. 356 | 357 | Args: 358 | - structure: pymatgen Structure object 359 | - n: index of the center atom in structure 360 | - cutoff: cutoff distance around n to search for neighbors 361 | Returns: 362 | - voronoi index of n: 363 | where x_i denotes number of facets with i edges 364 | """ 365 | 366 | center = structure[n] 367 | neighbors = structure.get_sites_in_sphere(center.coords, cutoff) 368 | neighbors = [i[0] for i in sorted(neighbors, key=lambda s: s[1])] 369 | qvoronoi_input = np.array([s.coords for s in neighbors]) 370 | voro = Voronoi(qvoronoi_input, qhull_options=qhull_options) 371 | vor_index = np.array([0, 0, 0, 0, 0, 0, 0, 0]) 372 | 373 | for key in voro.ridge_dict: 374 | if 0 in key: 375 | "This means if the center atom is in key" 376 | if -1 in key: 377 | "This means if an infinity point is in key" 378 | print("Cutoff too short. Exiting.") 379 | return None 380 | else: 381 | try: 382 | vor_index[len(voro.ridge_dict[key]) - 3] += 1 383 | except IndexError: 384 | # If a facet has more than 10 edges, it's skipped here 385 | pass 386 | return vor_index 387 | 388 | def from_structures(self, structures, cutoff=4.0, step_freq=10, qhull_options="Qbb Qc Qz"): 389 | """ 390 | A constructor to perform Voronoi analysis on a list of pymatgen structrue objects 391 | 392 | Args: 393 | structures (list): list of Structures 394 | cutoff (float: cutoff distance around an atom to search for neighbors 395 | step_freq (int): perform analysis every step_freq steps 396 | qhull_options (str): options to pass to qhull 397 | Returns: 398 | A list of [voronoi_tesellation, count] 399 | """ 400 | print("This might take a while...") 401 | voro_dict = {} 402 | step = 0 403 | for structure in structures: 404 | step += 1 405 | if step % step_freq != 0: 406 | continue 407 | 408 | v = [] 409 | for n in range(len(structure)): 410 | v.append(str(self.voronoi_analysis(structure, n=n, cutoff=cutoff, 411 | qhull_options=qhull_options).view())) 412 | for voro in v: 413 | if voro in voro_dict: 414 | voro_dict[voro] += 1 415 | else: 416 | voro_dict[voro] = 1 417 | self.vor_ensemble = sorted(voro_dict.items(), key=lambda x: (x[1], x[0]), reverse=True)[:15] 418 | return self.vor_ensemble 419 | 420 | @property 421 | def plot_vor_analysis(self): 422 | import matplotlib.pyplot as plt 423 | t = zip(*self.vor_ensemble) 424 | labels = t[0] 425 | val = list(t[1]) 426 | tot = np.sum(val) 427 | val = [float(j) / tot for j in val] 428 | pos = np.arange(len(val)) + .5 # the bar centers on the y axis 429 | plt.figure(figsize=(4, 4)) 430 | plt.barh(pos, val, align='center', alpha=0.5) 431 | plt.yticks(pos, labels) 432 | plt.xlabel('Fraction') 433 | plt.title('Voronoi Spectra') 434 | plt.grid(True) 435 | return plt 436 | 437 | 438 | class RadialDistributionFunction(object): 439 | """ 440 | Class to calculate partial radial distribution functions (RDFs) of sites. 441 | Typically used to analyze the pair correlations in liquids or amorphous structures. 442 | Supports multiprocessing: see get_radial_distribution_function method 443 | 444 | Args: 445 | structures (list): a list of Structure objects. 446 | cutoff (float): maximum distance to search for pairs. (defauly = 5.0) 447 | Note cutoff should be smaller than or equal to the half of the edge 448 | length of the box due to periodic boundaires. 449 | bin_size (float): thickness of each coordination shell in Angstroms (default = 0.1) 450 | step_freq (int): compute and store RDFs every step_freq steps 451 | to average later. (default = 2) 452 | smooth (int): number of smoothing passes (default = 1) 453 | title (str): title for the RDF plot. 454 | Returns: 455 | A dictionary of partial radial distribution functions with pairs as keys and RDFs as values. 456 | RDFs themselves are arrays of length cutoff/bin_size. 457 | """ 458 | 459 | def __init__(self, structures, cutoff=5.0, bin_size=0.1, step_freq=2, smooth=1, 460 | title="Radial distribution functions"): 461 | self.structures = structures 462 | self.cutoff = cutoff 463 | self.bin_size = bin_size 464 | self.step_freq = int(step_freq) 465 | self.smooth = smooth 466 | self.n_frames = int(len(self.structures)) 467 | self.n_atoms = len(self.structures[0]) 468 | self.n_species = self.structures[0].composition.as_dict() 469 | self.get_pair_order = None 470 | self.title = title 471 | self.RDFs = {} 472 | ss = self.structures[0].symbol_set 473 | self.pairs = [p for p in itertools.combinations_with_replacement(ss, 2)] 474 | self.counter = 1 475 | 476 | @property 477 | def n_bins(self): 478 | _bins = int(np.ceil(self.cutoff / self.bin_size)) 479 | if _bins < 2: 480 | raise ValueError("More bins required!") 481 | return _bins 482 | 483 | def get_radial_distribution_functions(self, nproc=1): 484 | """ 485 | Args: 486 | nproc: (int) number of processors to utilize (defaults to 1) 487 | Returns: 488 | A dictionary of partial radial distribution functions 489 | with pairs as keys and RDFs as values. 490 | Each RDF arrays of length cutoff/bin_size. 491 | """ 492 | 493 | frames = [(self.structures[i * self.step_freq], self.pairs, self.n_bins, self.cutoff, self.bin_size) for i in 494 | range(int(self.n_frames / self.step_freq))] 495 | self.counter = len(frames) 496 | pool = Pool(nproc) 497 | results = pool.map(_process_frame, frames) 498 | pool.close() 499 | pool.join() 500 | 501 | # Collect all rdfs 502 | for pair in self.pairs: 503 | self.RDFs[pair] = np.zeros(self.n_bins) 504 | for i in results: 505 | for pair in self.pairs: 506 | self.RDFs[pair] += i[pair] 507 | 508 | self.get_pair_order = [] 509 | 510 | for i in self.RDFs.keys(): 511 | self.get_pair_order.append('-'.join(list(i))) 512 | density_of_atom2 = self.n_species[i[1]] / self.structures[0].volume 513 | for j in range(self.n_bins): 514 | r = j * self.bin_size 515 | if r == 0: 516 | continue 517 | self.RDFs[i][j] = self.RDFs[i][j] / self.n_species[ 518 | i[0]] / 4.0 / np.pi / r / r / self.bin_size / density_of_atom2 / self.counter 519 | 520 | if self.smooth: 521 | self.RDFs = get_smooth_rdfs(self.RDFs, passes=self.smooth) 522 | return self.RDFs 523 | 524 | def plot_radial_distribution_functions(self): 525 | """ 526 | :return: a plot of RDFs 527 | """ 528 | import matplotlib.pyplot as plt 529 | x = [] 530 | for j in range(self.n_bins): 531 | r = j * self.bin_size 532 | x.append(r) 533 | rdfs = self.RDFs 534 | plt.figure() 535 | for rdf in rdfs: 536 | plt.plot(x, rdfs[rdf]) 537 | 538 | plt.xlabel("$r$, distance (Angstrom)") 539 | plt.ylabel("g($r$)") 540 | plt.legend(self.get_pair_order, bbox_to_anchor=(0.975, 0.975), loc=0, 541 | borderaxespad=0., prop={'family': 'sans-serif', 'size': 13}) 542 | plt.title(self.title) 543 | return plt 544 | 545 | 546 | def _process_frame(data): 547 | """ 548 | Helper function for parallel rdf computation 549 | """ 550 | coord_frame, pairs, n_bins, cutoff, bin_size = \ 551 | data[0], data[1], data[2], data[3], data[4] 552 | process_RDFs = {} 553 | n_atoms = len(coord_frame) 554 | 555 | for pair in pairs: 556 | process_RDFs[pair] = np.zeros(n_bins) 557 | 558 | distance_matrix = coord_frame.distance_matrix 559 | 560 | for atom1 in range(n_atoms - 1): 561 | atom1_specie = coord_frame[atom1].species_string 562 | for atom2 in range(atom1 + 1, n_atoms): 563 | atom2_specie = coord_frame[atom2].species_string 564 | if distance_matrix[atom1, atom2] > cutoff: 565 | continue 566 | bin_index = int(distance_matrix[atom1, atom2] / bin_size) 567 | # skip itself 568 | if bin_index == 0: 569 | continue 570 | key = (atom1_specie, atom2_specie) 571 | if key in process_RDFs: 572 | process_RDFs[key][bin_index] += 1 573 | if key[::-1] in process_RDFs: 574 | process_RDFs[key[::-1]][bin_index] += 1 575 | return process_RDFs 576 | 577 | 578 | def get_smooth_rdfs(RDFs, passes=1): 579 | """ 580 | Helper function to recursively smooth RDFs using a 5-parameter Savitzky-Golay filter. 581 | Args: 582 | RDFs: A dictionary of partial radial distribution functions 583 | with pairs as keys and RDFs as values. 584 | passes: number of times the filter is applied during smoothing. 585 | Returns 586 | RDFs dictionary with with each RDF smoothed. 587 | """ 588 | if passes == 0: 589 | return RDFs 590 | else: 591 | for rdf in RDFs: 592 | smooth_RDF = deepcopy(RDFs[rdf]) 593 | for j in range(2, len(RDFs[rdf]) - 2): 594 | smooth_RDF[j] = (-3 * RDFs[rdf][j - 2] + 12 * RDFs[rdf][j - 1] 595 | + 17 * RDFs[rdf][j] + 12 * RDFs[rdf][j + 1] - 3 * RDFs[rdf][j + 2]) / 35.0 596 | RDFs[rdf] = smooth_RDF 597 | passes -= 1 598 | return get_smooth_rdfs(RDFs, passes=passes) 599 | 600 | 601 | def get_smooth_rdfs(RDFs, passes=1): 602 | """ 603 | Helper function to recursively smooth RDFs using a 5-parameter Savitzky-Golay filter. 604 | Args: 605 | RDFs: A dictionary of partial radial distribution functions 606 | with pairs as keys and RDFs as values. 607 | passes: number of times the filter is applied during smoothing. 608 | Returns 609 | RDFs dictionary with with each RDF smoothed. 610 | """ 611 | if passes == 0: 612 | return RDFs 613 | else: 614 | for rdf in RDFs: 615 | smooth_RDF = deepcopy(RDFs[rdf]) 616 | for j in range(2, len(RDFs[rdf]) - 2): 617 | smooth_RDF[j] = (-3 * RDFs[rdf][j - 2] + 12 * RDFs[rdf][j - 1] 618 | + 17 * RDFs[rdf][j] + 12 * RDFs[rdf][j + 1] - 3 * RDFs[rdf][j + 2]) / 35.0 619 | RDFs[rdf] = smooth_RDF 620 | passes -= 1 621 | return get_smooth_rdfs(RDFs, passes=passes) 622 | 623 | 624 | def get_sample_structures(structures, n=10, steps_skip_first=1000): 625 | """ 626 | Helper method to extract n unique structures from an MD output 627 | Args: 628 | xdatcar_path: (str) path 629 | Returns: 630 | A list of Structure objects 631 | """ 632 | if type(structures) == type("asdf"): 633 | input_structures = Xdatcar(xdatcar_path).structures 634 | else: 635 | input_structures = structures 636 | output_structures = [] 637 | t = len(input_structures) - steps_skip_first 638 | for i in range(n): 639 | output_structures.append(input_structures[::-1][i * t // n]) 640 | return output_structures 641 | -------------------------------------------------------------------------------- /mpmorph/analysis/examples/Example-Get_D_and_Q.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "%matplotlib inline\n", 12 | "from mpmorph.analysis.diffusion import Activation, Diffusion\n", 13 | "import glob\n", 14 | "p = glob.glob(\"./liquid_Na/Na_df*/run0/XDATCAR.gz\")" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "# Exmaple 1: Obtaining D from an MD run" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "metadata": { 28 | "collapsed": false 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "# Let's get structures for one of the MD runs available in example runs path (p).\n", 33 | "from pymatgen.io.vasp.outputs import Xdatcar\n", 34 | "structures = Xdatcar(p[2]).structures" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 4, 40 | "metadata": { 41 | "collapsed": true 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "# Initialize a Diffusion calculator\n", 46 | "d = Diffusion(structures, 300, 2, skip_first=250)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 5, 52 | "metadata": { 53 | "collapsed": false, 54 | "scrolled": true 55 | }, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "{'D': 0.00022111868110501922,\n", 61 | " 'D_std': 5.5085146875530359e-05,\n", 62 | " 'Dx': 0.0002133696465970773,\n", 63 | " 'Dx_std': 4.0300442811345721e-05,\n", 64 | " 'Dy': 0.00024155617346295966,\n", 65 | " 'Dy_std': 8.844256068973984e-05,\n", 66 | " 'Dz': 0.00020843022325502073,\n", 67 | " 'Dz_std': 5.530485002247422e-05}" 68 | ] 69 | }, 70 | "execution_count": 5, 71 | "metadata": {}, 72 | "output_type": "execute_result" 73 | } 74 | ], 75 | "source": [ 76 | "# Get diffusion coefficients and standard errors from block averaging\n", 77 | "d.getD(\"Na\")" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 6, 83 | "metadata": { 84 | "collapsed": false 85 | }, 86 | "outputs": [ 87 | { 88 | "data": { 89 | "text/plain": [ 90 | "4" 91 | ] 92 | }, 93 | "execution_count": 6, 94 | "metadata": {}, 95 | "output_type": "execute_result" 96 | } 97 | ], 98 | "source": [ 99 | "# Number of time origins (blocks) used in averaging\n", 100 | "d.n_origins" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 7, 106 | "metadata": { 107 | "collapsed": false, 108 | "scrolled": true 109 | }, 110 | "outputs": [ 111 | { 112 | "data": { 113 | "image/png": "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\ngg==\n", 114 | "text/plain": [ 115 | "" 116 | ] 117 | }, 118 | "metadata": {}, 119 | "output_type": "display_data" 120 | } 121 | ], 122 | "source": [ 123 | "# A quick look at all msd (x,y,z) data from all blocks\n", 124 | "d.plot_block_msds()" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 8, 130 | "metadata": { 131 | "collapsed": false 132 | }, 133 | "outputs": [ 134 | { 135 | "data": { 136 | "text/plain": [ 137 | "{'D': 226.12291168765046,\n", 138 | " 'Dx': 234.33511184662052,\n", 139 | " 'Dy': 206.99119084061428,\n", 140 | " 'Dz': 239.8884346960732}" 141 | ] 142 | }, 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "output_type": "execute_result" 146 | } 147 | ], 148 | "source": [ 149 | "# The relaxation time can be roughly estimated too (we recommend setting corr_t larger than this number)\n", 150 | "d.tao" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "# Exmaple 2: Obtaining Activation Energies for Diffusio " 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "Activation calculator can be initialized with a list of dicts as returned by Diffusion.getD()\n", 165 | "But a more convenient way is using the Activation.from_run_paths constructor and point to the\n", 166 | "list of MD runs. " 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 9, 172 | "metadata": { 173 | "collapsed": true 174 | }, 175 | "outputs": [], 176 | "source": [ 177 | "# Let's get the list of temperatures from the paths in p for this example\n", 178 | "T = [ float(t.split(\"/\")[-3].split(\"_\")[2]) for t in p]" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 10, 184 | "metadata": { 185 | "collapsed": false 186 | }, 187 | "outputs": [], 188 | "source": [ 189 | "# Since diffusion coefficients will have to be computed, initialization will take a while.\n", 190 | "a = Activation.from_run_paths(p, T, \"Na\", 400, 1, 500, l_lim=40, skip_first=500)" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 11, 196 | "metadata": { 197 | "collapsed": false 198 | }, 199 | "outputs": [ 200 | { 201 | "data": { 202 | "text/plain": [ 203 | "1676.4721892877608" 204 | ] 205 | }, 206 | "execution_count": 11, 207 | "metadata": {}, 208 | "output_type": "execute_result" 209 | } 210 | ], 211 | "source": [ 212 | "# Only Q: regular least-squares linear regression of Activation Energy Q (in units of K)\n", 213 | "a.LS()" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 12, 219 | "metadata": { 220 | "collapsed": false 221 | }, 222 | "outputs": [ 223 | { 224 | "data": { 225 | "text/plain": [ 226 | "(1692.4958799498236, 281.79995580223056)" 227 | ] 228 | }, 229 | "execution_count": 12, 230 | "metadata": {}, 231 | "output_type": "execute_result" 232 | } 233 | ], 234 | "source": [ 235 | "# Advanced:\n", 236 | "# Q and Q_std: orthongal distance regression to obtain Q allows measurement errors in independent variables too.\n", 237 | "# Therefore the errors obtained in block averaging of D can further be considered along with fitting error to yield\n", 238 | "# standard deviation type error for the calculated Activation Energy\n", 239 | "a.ODR()" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 13, 245 | "metadata": { 246 | "collapsed": false 247 | }, 248 | "outputs": [ 249 | { 250 | "data": { 251 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaQAAAEYCAYAAAATRII7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xl4VOX5//H3jSyyiVQLCFRBxAUXBBGxUpwKCLiC0q9W\nXKBW/KmgFtG6sdXdfpWKQr+tta5QW9uKigaIyuBeF5aCCyoSRRA3lFUBk/v3x5kkk5BJZpKZzJnk\n87quczlzlmfuOdeYm/Oc57mPuTsiIiLZ1iDbAYiIiIASkoiIhIQSkoiIhIISkoiIhIISkoiIhIIS\nkoiIhELoE5KZDTez5WZWaGY949YfaWaLY8tSMzujkjbGmtm7ZrbMzG6tnchFRCQVDbMdQBKWAcOA\nP1Ww/gh3LzKzdsByM/unuxfG72RmEeBk4FB3/8HM9qyNoEVEJDWhT0juvgLAzKzc+u/j3jYFNpRP\nRjEXAbe6+w+x477KVKwiIlJ9oe+yq4yZ9Taz5cByYFyC3fYH+pnZa2a2wMx61V6EIiKSrFBcIZlZ\nPtA2fhXgwHXu/lSi49z9deAQMzsAmGdmC9x9Y7ndGgKt3b2PmR0J/APYN73fQEREaioUCcndB9bw\n+BVmthLoCrxVbvNq4N+x/d4wsyIz28Pdvy7fjpmpsJ+ISDW4u1W9V+Vyrcuu5AubWScz2yX2eh9g\nP+CDCo6ZDRwX229/oFFFyaiYu+fsMmnSpKzHUF/jz+XYFX/2l1yPP11Cn5DMbKiZrQb6AHPMLC+2\nqS+w1MwWEXTDjfZYd52Z3Rs3RPx+YF8zWwbMAs6t3W8gIiLJCEWXXWXcfTbBVU759Y8AjyQ45oK4\n1zuAczIWIBAtiBItiJa8jnSKABDpFCl5LSIilQt9QsoF8YnHphjRkdHsxBGJZOVz0yWX48/l2EHx\nZ1uux58uls7+v1xnZl7T82FTDJ+kcyoi9YeZ4fVwUIOIiNRRSkgiIhIKSkgiIhIKSkgiIhIKSkgi\nIhIKSkgiIhIKSkgiIhIKSkgiklVr165l9OjR/OQnP6FJkyZ07NiR0aNHs2bNmpTbevHFFzn11FPp\n2LEjDRo04KGHHqpwv3Xr1jFy5EjatGlD06ZNOeSQQ3jxxRdLtm/evJnLL7+cTp060axZM/r27cub\nb75Zsv2WW26hd+/etGrVijZt2nDKKafw9ttvp/7lU2ivsLCQa6+9ln333ZemTZuy7777MmHCBAoL\nC1Nqp7xRo0ZxyimnlFk3Z84cmjdvzsSJE6v9napDCUlEsqagoIBevXrxzjvv8PDDD7Ny5UpmzpzJ\n22+/zZFHHsknn3ySUnubN2/m0EMPZdq0aTRr1qzCfTZs2MAxxxyDmZGXl8d7773H3XffTZs2bUr2\nOf/888nPz+fhhx9m+fLlDBw4kAEDBvDZZ58B8MILLzBmzBheffVVFixYQMOGDRkwYADffvttwth+\n//vfJ9yWTHs33XQTf/7zn7nnnntYsWIF06ZNY8aMGdx6660ptVOVhx9+mOHDh3Pbbbfxu9/9Lunj\n0iLbVWLDtASno2aYXPM2ROqLIUOGeMeOHf37778vs37r1q3eoUMHP+mkk6rddosWLfzBBx/caf01\n11zjffv2TXjcd9995w0bNvSnnnqqzPojjjjCJ0yYUOExmzdv9l122cXnzJmTsN0pU6YkGXnF7Z10\n0kk+cuTIMvudd955fvLJJ6fUTnkjR44saWPq1Km+6667+qxZs5KO1d099rezxn+DdYUkIlnxzTff\nMG/ePMaMGUOTJk3KbGvatCkXX3wxc+fOZcOGDQA88MADNGjQIOWrpvKeeOIJjjrqKM4880zatm1L\njx49mD59esn2H374gcLCwgpjeumllypsc+PGjRQVFdG6desaxVZZe0OGDGHBggWsWLECgHfeeYfn\nn3+eE088MaV2EpkwYQLXX389s2fP5pe//GXNv0Q1KCGJSFZ88MEHuDsHHnhghdu7detGUVERH374\nIQC77747Bx54II0aNarR53700UfMmDGDLl26MH/+fC6//HKuvvpqZsyYAUCLFi04+uijufHGG1m7\ndi1FRUU88sgjvPrqqyVdduVddtll9OzZk6OPPjrh53oKdTIrau/iiy9mxIgRHHTQQTRu3JhDDz2U\nkSNHcuGFF6bUTkXmz5/PzTffzGOPPcagQYOSjjPdVO1bREKtYcPgz9TQoUMZOnRojdsrKiqid+/e\n3HTTTQB0796d999/n+nTp3PxxRcD8Mgjj/CrX/2Kjh070rBhQ3r27MlZZ53FW2+VfyA1jBs3jlde\neYWXX34Zs6C+6BdffMEf/vCHkn3cnZdffplt27aVJKaWLVty7bXXJtUewLRp07j//vv5+9//Trdu\n3ViyZAmXXnopnTt3ZtSoUUm3U5FDDz2UDRs2MHnyZH7605/SqlWrqk5jZqSj36+uLOgekkitWb9+\n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\n", 252 | "text/plain": [ 253 | "" 254 | ] 255 | }, 256 | "metadata": {}, 257 | "output_type": "display_data" 258 | } 259 | ], 260 | "source": [ 261 | "f = a.plot(**{'fmt':'o'})" 262 | ] 263 | } 264 | ], 265 | "metadata": { 266 | "kernelspec": { 267 | "display_name": "Python 2", 268 | "language": "python", 269 | "name": "python2" 270 | }, 271 | "language_info": { 272 | "codemirror_mode": { 273 | "name": "ipython", 274 | "version": 2 275 | }, 276 | "file_extension": ".py", 277 | "mimetype": "text/x-python", 278 | "name": "python", 279 | "nbconvert_exporter": "python", 280 | "pygments_lexer": "ipython2", 281 | "version": "2.7.10" 282 | } 283 | }, 284 | "nbformat": 4, 285 | "nbformat_minor": 0 286 | } 287 | --------------------------------------------------------------------------------