├── .flake8 ├── .github └── workflows │ └── WhereWulff.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── README.md ├── WhereWulff ├── adsorption │ ├── MXide_adsorption.py │ └── adsorbate_configs.py ├── analysis │ ├── bulk_stability.py │ ├── equation_of_states.py │ ├── oer.py │ ├── surface_pourbaix.py │ └── wulff_shape.py ├── common │ └── glue_tasks.py ├── dft_settings │ └── settings.py ├── firetasks │ ├── handlers.py │ ├── oer_single_site.py │ ├── slab_ads.py │ ├── static_bulk.py │ └── surface_energy.py ├── fireworks │ ├── oer_single_site.py │ ├── optimize.py │ └── surface_pourbaix.py ├── launchers │ ├── bulkflows.py │ └── slabflows.py ├── reactivity │ └── oer.py ├── tests │ └── test.py └── workflows │ ├── bulk_stability.py │ ├── eos.py │ ├── oer.py │ ├── oer_single_site.py │ ├── slab_ads.py │ ├── static_bulk.py │ ├── surface_energy.py │ ├── surface_pourbaix.py │ └── wulff_shape.py ├── img ├── wherewulff_img.png └── wherewulff_logo.png ├── main_bulk.py ├── main_slab.py ├── setup.py └── wherewulff_env.yml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, E731, W503, F403, F401 3 | max-line-length = 79 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.github/workflows/WhereWulff.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: WhereWulff 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | # This workflow contains a single job called "build" 21 | WhereWulff: 22 | # The type of runner that the job will run on 23 | runs-on: 24 | - self-hosted 25 | - linux 26 | - shared-scratch 27 | 28 | container: 29 | image: docker://ulissigroup/vasp:atomate_stack 30 | options: --user root 31 | credentials: 32 | username: ${{ secrets.DOCKERHUB_USERNAME }} 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | 35 | # Steps represent a sequence of tasks that will be executed as part of the job 36 | steps: 37 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 38 | - name: Checkout the unit_tests branch from the repo 39 | uses: actions/checkout@v2 40 | with: 41 | ref: unit_tests 42 | 43 | - name: Run end-to-end OER workflow on IrO2 44 | run: | 45 | # Set the home environment variable to /home/jovyan to locate the .pmgrc.yaml config file 46 | export HOME=/home/jovyan 47 | # Mongo daemon in the background 48 | mongod --quiet>/dev/null & 49 | # Reset local database 50 | yes | lpad reset 51 | echo "---\n" 52 | # Go to repo directory 53 | export PYTHONPATH=$GITHUB_WORKSPACE 54 | echo "Checking out the code base..." 55 | cd $GITHUB_WORKSPACE 56 | echo "---\n" 57 | # Run the OER workflow with fireworks 58 | python main_slab.py && rlaunch rapidfire 59 | - name: Running regression test 60 | run: | 61 | # Run suite of tests based on the metadata in the local fireworks db 62 | cd tests && python -m unittest regression_test.py 63 | 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bash_history 2 | .ssh 3 | **/__pycache__ 4 | ./atomate_run.sh 5 | main_slab_jh.py 6 | main_bulk_jh.py 7 | POTCAR 8 | *.cif 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: flake8 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: check-added-large-files 10 | - repo: https://github.com/psf/black 11 | rev: 21.7b0 12 | hooks: 13 | - id: black 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Carnegie Mellon University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


WhereWulff

2 | 3 | [![WhereWulff](https://github.com/ulissigroup/mo-wulff-workflow/actions/workflows/WhereWulff.yml/badge.svg)](https://github.com/ulissigroup/mo-wulff-workflow/actions/workflows/WhereWulff.yml) 4 | 5 | ## Introduction 6 | 7 | `WhereWulff` couples deep expertise in Quantum Chemistry and Catalysis with that in workflow engineering, an approach that is slowly gaining traction in the science community [[1]](#1). While the advent of massively parallel computing clusters has given rise to a novel way of 8 | searching chemical space in a high-throughput manner, we argue that as long as scientists are not 9 | equipped with proper software best practices, self-actualization, even when coupled with years of 10 | chemical intuition and heavy compute, is limited. In addition to tackling scientific challenges, we expect our open-source 11 | workflow to serve a didactic purpose, democratizing access to complex material science pipelines 12 | for those in the likes of experimentalists, who would like to corroborate or guide their endeavors 13 | but don’t have the formal theoretical and computational training to do it from scratch. Finally, we encourage the scientific community to tap into `WhereWulff's` modularity in order to plug in other reactivities they might have domain expertise or interest in. 14 | 15 |
16 | 17 |
18 |

Figure 1. WhereWulff general schema that consists in the bulk workflow to get the 19 | equilibrium bulk structure with the most stable magnetic configuration as NM, AFM or FM (Left), and the reactivity workflow that analyzes Wulff Construction, Surface Pourbaix diagram and OER Reactivity for a given material (Right). 20 |

21 |
22 |
23 | 24 | As is common practice in the software realm, we leverage pre-existing open-source software packages with the most noteworthy ones being [Atomate](#2), [FireWorks](#3), [Custodian](#4) and [Pymatgen](#4) in order to deliver `WhereWulff`, which is itself open-sourced. This workflow conducts Density Functional Theory (DFT) calculations using the `Vienna Ab-initio Simulation Package` ([VASP](#5)). 25 | 26 | ## Installation 27 | 28 | After installing [conda](http://conda.pydata.org/), run the following commands to create a new [environment](https://conda.io/docs/user-guide/tasks/manage-environments.html) named wherewulff and install dependencies. 29 | 30 | ```bash 31 | conda env create -f wherewulff_env.yml 32 | conda activate wherewulff 33 | pip install -e . 34 | ``` 35 | 36 | `WhereWulff` main dependencies are [FireWorks](https://materialsproject.github.io/fireworks/), [Atomate](https://atomate.org) and [Pymatgen](https://pymatgen.org), that need further installation steps. 37 | 38 | ### FireWorks and Atomate 39 | 40 | We refer the user to the [Atomate](https://atomate.org/installation.html) installation documentation to have a deeper explanation on how to set-up `FireWorks/Atomate` properly. 41 | 42 | ### Pymatgen 43 | 44 | [Pymatgen](https://pymatgen.org) needs the `.pmgrc.yml` file to be configured with the VASP pseudopotentials, default DFT functional and the [Materials Project]() API token as: 45 | 46 | To configure Pymatgen to find the VASP pseudopotential see [POTCAR setup](https://pymatgen.org/installation.html#) 47 | 48 | ```bash 49 | pmg config -p 50 | pmg config --add PMG_VASP_PSP_DIR 51 | pmg config --add PMG_DEFAULT_FUNCTIONAL PBE_54 52 | ``` 53 | 54 | Is always good practice to test if Pymatgen is able to find a given POTCAR file. The following command should create a new POTCAR file for H atom: 55 | 56 | ```bash 57 | pmg potcar -s H -f PBE_54 58 | ``` 59 | 60 | Don't forget to include your `PMG_MAPI_KEY` to be able to run the Stability Analysis at the end of the Bulk Workflow. 61 | 62 | Your `.pmgrc.yml` file should look like: 63 | ```bash 64 | PMG_DEFAULT_FUNCTIONAL: PBE_54 65 | PMG_MAPI_KEY: "YOUR_API_TOKEN" 66 | PMG_VASP_PSP_DIR: "POTCAR_DIR" 67 | ``` 68 | 69 | ## Run the Workflow 70 | 71 | The following example is how to load the Bulk Workflow to the launchpad and then submitting how to submit it through the FireWorks command line: 72 | 73 | ```python 74 | from WhereWulff.launchers.bulkflows import BulkFlows 75 | 76 | # CIF file pathway 77 | cif_file = "<>" 78 | 79 | # BulkFlow method and config 80 | bulk_flow = BulkFlows(bulk_structure=cif_file, 81 | n_deformations=21, 82 | nm_magmom_buffer=0.6, 83 | conventional_standard=True) 84 | 85 | # Get Launchpad 86 | launchpad = bulk_flow.submit( 87 | hostname="localhost", 88 | db_name="<>", 89 | port="<>", 90 | username="<>", 91 | password="<>", 92 | ) 93 | ``` 94 | 95 | The Bulk workflow is called through the BulkFlow method which is able to submit the workflow to the launchpad for a given `CIF` file consisting in a bulk structure of a metal or metal oxide material. 96 | 97 | The user needs to provide the `CIF` file pathway and the configure the workflow in terms of number of deformations for the `EOS` (Equation of States), the magnetic buffer for non-magnetic species included in the given material and whether to transform the given structure to conventional standrad. 98 | 99 | The submit method inside BulkFlows class needs the MongoDB configuration features such as `hostname`, `db_name`, `port`, `username` and `password`. We encourage the user to not make public this information. 100 | 101 | We encourage the user to use `Fireworks webgui` to make sure the workflow is properly added to the launchpad. Finally the way to run the workflow through the command line shell is as follows (-m flag is for maximum 5 jobs running in parallel): 102 | 103 | ```bash 104 | qlaunch rapidfire -m 5 105 | ``` 106 | 107 | The surface chemistry workflow is called through the SlabFlows method which is able to submit the whole worklfow to the launchpad for a given `CIF` file consisting in a bulk structure. 108 | 109 | ```python 110 | from WhereWulff.launchers.slabflows import SlabFlows 111 | 112 | # CIF file pathway 113 | cif_file = "<>" 114 | 115 | # slabFlows method and config 116 | slab_flows = SlabFlows(cif_file, exclude_hkl=[(1, 0, 0), (1, 1, 1), (0, 0, 1)]) 117 | 118 | # Get Launchpad 119 | launchpad = slab_flows.submit( 120 | hostname="localhost", 121 | db_name="<>", 122 | port="<>", 123 | username="<>", 124 | password="<>", 125 | ) 126 | ``` 127 | 128 | The user needs to provide a `CIF` file pathway, preferably as a result of running the bulk workflow beforehand so then the bulk structure will be with the equilibrium cell parameters and with the magnetic configuration well defined. SlabFlows can be extensibly configured depending to the user needs see [documentation](https://github.com/ulissigroup/wherewulff/blob/main/WhereWulff/launchers/slabflows.py). The submit function inside SlabFlows works in the same way as BulkFlows by providing the required information to being able to connect to the MongoDB database and the launchpad. 129 | 130 | Finally, submitting the workflow must be done through the same command as the previous examples: 131 | 132 | ```bash 133 | qlaunch rapidfire -m 5 134 | ``` 135 | ## Example BaSrCo-001 136 | 137 | We have included all the input and output files from an end-to-end run of the bulk workflow and WhereWulff on BaSrCo2O6 structure. They are organized as follows and can be found on the `example_IO_run` branch: 138 | ```bash 139 | Bulk Optimization: - BaSrCoO_001_bulk folder 140 | Slab Optimization: - BaSrCoO_001_slab folder 141 | Pourbaix Optimizations: - BaSrCoO_001_O_1 for full oxo terminations 142 | - BaSrCoO_001_OH_* for all hydroxyl terminations 143 | OER Reactivity Optimizations: - BaSrCoO_001_Co_OH_* for *OH intermediate on clean termination at Co active site 144 | - BaSrCoO_001_Co_Ox for *O intermediate on clean termination at Co active site 145 | - BaSrCoO_001_Co_OOH_up_* for *OOH up configuration on clean termination at Co active site 146 | - BaSrCoO_001_Co_OOH_down_* for *OOH down configuration on clean termination at Co active site 147 | ``` 148 | Since the OUTCAR and vasprun.xml files are large, they have been uploaded per LFS protocol. In order to download the contents one needs 149 | to have `git-lfs` installed. Subsequently, to download contents one can run the following command inside the repo: `git lfs pull` 150 | 151 | ## Acknowledgements 152 | 153 | This work was supported by the National Research Council (NRC) and the Army Research Office (ARO). The authors acknowledge CMU and UofT. This research also used resources of the National Energy Research Scientific Computing Center (NERSC), a U.S. Department of Energy Office of Science User Facility located at Lawrence Berkeley National Laboratory. 154 | 155 | ## License 156 | 157 | `WhereWulff` is released under the [MIT](https://github.com/ulissigroup/mo-wulff-workflow/blob/main/LICENSE.md) 158 | 159 | ## Citing `WhereWulff` 160 | 161 | If you use this codebase in your work, please consider citing: 162 | 163 | ```bibtex 164 | @article{wherewulff2023, 165 | title = {WhereWulff: A semi-autonomous workflow for systematic catalyst surface reactivity under reaction conditions}, 166 | author = {Rohan Yuri Sanspeur, Javier Heras-Domingo, John R. Kitchin and Zachary Ulissi}, 167 | journal = {in preparation}, 168 | year = {2023}, 169 | } 170 | ``` 171 | 172 | ## References 173 | [1] 174 | Joerg Schaarschmidt, Jie Yuan, Timo Strunk, Ivan Kondov, Sebastiaan P. Huber, Giovanni 175 | Pizzi, Leonid Kahle, Felix T. Bolle, Ivano E. Castelli, Tejs Vegge, Felix Hanke, Tilmann Hickel, 176 | Jorg Neugebauer, Celso R. C. Rego, and Wolfgang Wenzel. Workflow engineering in materials design 177 | within the battery 2030+project. Advanced Energy Materials, page 2102638, 2021. [URL](https://onlinelibrary.wiley.com/doi/10.1002/aenm.202102638). 178 | 179 | [2] 180 | Mathew, K., Montoya, J. H., Faghaninia, A., Dwarakanath, S., Aykol, M., Tang, H., Chu, I., Smidt, T., Bocklund, B., Horton, M., Dagdelen, J., 181 | Wood, B., Liu, Z.-K., Neaton, J., Ong, S. P., Persson, K., Jain, A., Atomate: A high-level interface to generate, execute, and analyze 182 | computational materials science workflows. Comput. Mater. Sci. 139, 140–152 (2017). [URL](https://doi.org/10.1016/j.commatsci.2017.07.030) 183 | 184 | [3] 185 | Jain, Anubhav and Ong, Shyue Ping and Chen, Wei and Medasani, Bharat and Qu, Xiaohui and Kocher, Michael and Brafman, Miriam and Petretto, Guido and Rignanese, Gian-Marco and Hautier, Geoffroy and Gunter, Daniel and Persson, Kristin A., FireWorks: a dynamic workflow system designed for high-throughput applications, Concurrency and Computation: Practice and Experience, (2015) [URL](http://dx.doi.org/10.1002/cpe.3505) 186 | 187 | [4] 188 | Shyue Ping Ong, William Davidson Richards, Anubhav Jain, Geoffroy Hautier, 189 | Michael Kocher, Shreyas Cholia, Dan Gunter, Vincent Chevrier, Kristin A. 190 | Persson, Gerbrand Ceder. *Python Materials Genomics (pymatgen) : A Robust, 191 | Open-Source Python Library for Materials Analysis.* Computational 192 | Materials Science, 2013, 68, 314–319. [URL](https://www.sciencedirect.com/science/article/pii/S0927025612006295) 193 | 194 | [5] 195 | Kresse, Georg and Furthmüller, Jürgen 196 | Efficient iterative schemes for ab initio total-energy calculations using a plane-wave basis set, Physical review B, (1996), [URL](https://journals.aps.org/prb/abstract/10.1103/PhysRevB.54.11169) 197 | -------------------------------------------------------------------------------- /WhereWulff/adsorption/adsorbate_configs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import numpy as np 9 | from pymatgen.core.structure import Molecule 10 | 11 | 12 | # Molecules 13 | 14 | # OH and Ox molecules 15 | OH = Molecule( 16 | ["O", "H"], 17 | [[0, 0, 0], [-1.0, 0.0, 0.422]], 18 | site_properties={"magmom": [0.6, 0.1], "binding_site": [True, False]}, 19 | ) 20 | Ox = Molecule( 21 | ["O"], [[0, 0, 0]], site_properties={"magmom": [0.6], "binding_site": [True]} 22 | ) 23 | OH_Ox_list = [OH, Ox] 24 | OOH_up = Molecule( 25 | ["O", "O", "H"], 26 | [[0, 0, 0], [-1.067, -0.403, 0.796], [-0.696, -0.272, 1.706]], 27 | site_properties={"magmom": [0.6, 0.6, 0.1], "binding_site": [True, False, False]}, 28 | ) 29 | OOH_down = Molecule( 30 | ["O", "O", "H"], 31 | [[0, 0, 0], [-1.067, -0.403, 0.796], [-1.84688848, -0.68892498, 0.25477651]], 32 | site_properties={"magmom": [0.6, 0.6, 0.1], "binding_site": [True, False, False]}, 33 | ) 34 | 35 | oer_adsorbates_dict = {"OH": OH, "Ox": Ox, "OOH_up": OOH_up, "OOH_down": OOH_down} 36 | 37 | O2 = Molecule(["O", "O"], [[0, 0, 0], [0, 0, 1.208]]) 38 | O2_like = O2.copy() 39 | O2_like.add_site_property("anchor", [True, False]) 40 | Oads_pair = [Ox, O2_like] 41 | O2.rotate_sites(theta=np.pi / 6, axis=[1, 0, 0]) 42 | Oxo_coupling = Molecule( 43 | ["O", "O"], 44 | [ 45 | ( 46 | 1.6815, 47 | 0, 48 | 0, 49 | ), 50 | (0, 0, 0), 51 | ], 52 | ) 53 | Oxo_coupling.add_site_property("dimer_coupling", [True, True]) 54 | 55 | OOH_up_OH_like = OOH_up.copy() 56 | OOH_up_OH_like.add_site_property("anchor", [True, False, False]) 57 | OH_pair = [OH, OOH_up_OH_like] 58 | H2O = Molecule( 59 | ["H", "H", "O"], 60 | [ 61 | [2.226191, -9.879001, 2.838300], 62 | [2.226191, -8.287900, 2.667037], 63 | [2.226191, -9.143303, 2.156037], 64 | ], 65 | ) 66 | 67 | # Deacon process 68 | HCl = Molecule(["H", "Cl"], [[0, 0, 0], [0, 1.275, 0]]) 69 | Cl = Molecule(["Cl"], [(0, 0, 0)]) 70 | Cl2 = Molecule(["Cl", "Cl"], [[0, 0, 0], [2, 0, 0]]) 71 | 72 | # CO2 capture 73 | CO = Molecule(["C", "O"], [[0, 0, 1.43], [0, 0, 0]]) 74 | CO.rotate_sites(theta=45, axis=[1, 0, 0]) 75 | CO2 = Molecule( 76 | ["C", "O", "O"], 77 | [ 78 | [0, 0, 0], 79 | [-0.6785328, -0.6785328, -0.6785328], 80 | [0.6785328, 0.6785328, 0.6785328], 81 | ], 82 | ) 83 | CO2_like = CO2.copy() 84 | CO2_like.add_site_property("anchor", [False, False, True]) 85 | 86 | # Nitrate reduction 87 | NO = Molecule("NO", [[0, 0, 1.16620], [0, 0, 0]]) 88 | NO.rotate_sites(theta=45, axis=[1, 0, 0]) 89 | NO2 = Molecule( 90 | ["N", "O", "O"], 91 | [ 92 | [0.01706138, 4.15418327, 5.90139358], 93 | [-0.87449724, 4.72305561, 6.47843921], 94 | [0.92310187, 4.50860583, 5.19279554], 95 | ], 96 | ) 97 | NO3 = Molecule( 98 | ["N", "O", "O", "O"], 99 | [ 100 | [3.8414, 3.1696, 6.1981], 101 | [4.9493, 2.7581, 5.7930], 102 | [3.7675, 3.9390, 7.1791], 103 | [2.7958, 2.8052, 5.6144], 104 | ], 105 | ) 106 | NO3_like = NO3.copy() 107 | NO3_like.add_site_property("anchor", [False, False, False, True]) 108 | NO2_like = NO2.copy() 109 | NO2_like.add_site_property("anchor", [False, False, True]) 110 | 111 | # Methanol formation 112 | CH4 = Molecule( 113 | ["C", "H", "H", "H", "H"], 114 | [ 115 | [2.48676400, 2.48676400, 2.48676400], 116 | [3.11939676, 3.11939676, 3.11939676], 117 | [3.11939676, 1.85413124, 1.85413124], 118 | [1.85413124, 1.85413124, 3.11939676], 119 | [1.85413124, 3.11939676, 1.85413124], 120 | ], 121 | ) 122 | 123 | CH3 = CH4.copy() 124 | CH3.rotate_sites(theta=np.pi / 4, axis=[0, 0, 1]) 125 | CH3.rotate_sites(theta=np.deg2rad(125), axis=[0, 1, 0]) 126 | CH3.remove_sites([4]) 127 | CH3.rotate_sites(theta=np.pi, axis=[0, 1, 0]) 128 | 129 | species = ["O", "C", "H", "H", "H", "H"] 130 | coords = [ 131 | [0.7079, 0, 0], 132 | [-0.7079, 0, 0], 133 | [-1.0732, -0.769, 0.6852], 134 | [-1.0731, -0.1947, -1.0113], 135 | [-1.0632, 0.9786, 0.3312], 136 | [0.9936, -0.8804, -0.298], 137 | ] 138 | CH3OH = Molecule(species, coords) 139 | CH3OH.rotate_sites(theta=-np.pi / 2, axis=[1, 0, 0]) 140 | CH3OH.rotate_sites(theta=np.pi / 4, axis=[0, 1, 0]) 141 | 142 | CH3OH_like = CH3OH.copy() 143 | CH3OH_like.add_site_property("anchor", [True, False, False, False, False, False]) 144 | CH4_pair = [CH4, CH3OH_like] 145 | 146 | # Peroxide 147 | H2O2 = Molecule( 148 | ["H", "H", "O", "O"], 149 | [ 150 | [1.76653038, 0.81443185, 4.81451611], 151 | [0.81443185, 1.76653038, 2.89052189], 152 | [2.28131524, 1.30263586, 4.09387932], 153 | [1.30263586, 2.28131524, 3.61115868], 154 | ], 155 | ) 156 | H2O2.rotate_sites(theta=np.pi / 2, axis=[0, 1, 0]) 157 | 158 | Hx = Molecule(["H"], [[0, 0, 0]]) 159 | OH_like = OH.copy() 160 | OH_like.add_site_property("anchor", [True, False]) 161 | Hads_pair = [Hx, OH_like] 162 | Nx = Molecule(["N"], [[0, 0, 0]]) 163 | NO_like = NO.copy() 164 | NO_like.add_site_property("anchor", [False, True]) 165 | Nads_pair = [Nx, NO_like] 166 | Cx = Molecule(["C"], [[0, 0, 0]]) 167 | CO_like = CO.copy() 168 | CO_like.add_site_property("anchor", [False, True]) 169 | Cads_pair = [Cx, CO_like] 170 | adslist = [ 171 | Oads_pair, 172 | Hads_pair, 173 | Nads_pair, 174 | Cads_pair, 175 | # monatomic adsorption of O, H, N and C can form O2, 176 | # OH, NO and CO with lattice positions respectively 177 | OH_pair, 178 | O2, 179 | [CO, CO2_like], 180 | H2O, 181 | Oxo_coupling, 182 | ] 183 | OOH_list = [OOH_up, OOH_down] 184 | -------------------------------------------------------------------------------- /WhereWulff/analysis/bulk_stability.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import json 9 | import uuid 10 | from pymatgen.analysis.phase_diagram import PDEntry, PhaseDiagram 11 | from pymatgen.analysis.pourbaix_diagram import ( 12 | ELEMENTS_HO, 13 | PourbaixDiagram, 14 | PourbaixPlotter, 15 | ) 16 | 17 | from pymatgen.analysis.structure_analyzer import OxideType 18 | from pymatgen.ext.matproj import MPRester 19 | 20 | from pymatgen.entries.compatibility import ( 21 | MaterialsProjectAqueousCompatibility, 22 | MaterialsProjectCompatibility, 23 | ) 24 | 25 | from pydash.objects import has, get 26 | 27 | from pymatgen.core.structure import Structure 28 | from pymatgen.entries.computed_entries import ComputedEntry 29 | 30 | from fireworks import FiretaskBase, FWAction, explicit_serialize 31 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 32 | 33 | from atomate.utils.utils import env_chk 34 | from atomate.utils.utils import get_logger 35 | from atomate.vasp.database import VaspCalcDb 36 | 37 | 38 | logger = get_logger(__name__) 39 | 40 | 41 | # Correction Dicts from MP2020 42 | composition_correction = { 43 | "V": -1.7, 44 | "Cr": -1.999, 45 | "Mn": -1.668, 46 | "Fe": -2.256, 47 | "Co": -1.638, 48 | "Ni": -2.541, 49 | "W": -4.438, 50 | "Mo": -3.202, 51 | } 52 | oxide_type_correction = {"oxide": -0.687, "peroxide": -0.465, "superoxide": -0.161} 53 | 54 | compat = MaterialsProjectCompatibility() 55 | 56 | 57 | @explicit_serialize 58 | class BulkStabilityAnalysis(FiretaskBase): 59 | """ 60 | Automated Stability Analysis Task to directly get, 61 | Thermodynamic and electrochemical stability of a given material. 62 | 63 | Args: 64 | bulk_formula (e.g RuO2) : structure composition as reduced formula 65 | db_file : To connect to the DB 66 | pbx_plot (default: True) : Save .png in launcher folder for PbxDiagram 67 | ehull_plot : Save .png in launcher folder for PhaseDiagram 68 | 69 | 70 | Returns: 71 | Stability Analysis to DB 72 | """ 73 | 74 | required_params = ["reduced_formula", "db_file"] 75 | optional_params = ["pbx_plot"] 76 | 77 | def run_task(self, fw_spec): 78 | 79 | # Variables 80 | bulk_formula = self["reduced_formula"] 81 | db_file = env_chk(self.get("db_file"), fw_spec) 82 | pbx_plot = self.get("pbx_plot", True) 83 | to_db = self.get("to_db", True) 84 | bulk_stability_uuid = str(uuid.uuid4()) 85 | summary_dict = {"uuid": bulk_stability_uuid} 86 | 87 | # Connect to DB 88 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 89 | 90 | # Get the static_bulk uuids from the static energy FWs 91 | bulk_static_uuids = [ 92 | fw_spec["bulk_static_dict"][k] for k in fw_spec["bulk_static_dict"] 93 | ] 94 | 95 | # Retrieve from DB 96 | docs = [ 97 | mmdb.collection.find_one({"static_bulk_uuid": sb_uuid}) 98 | for sb_uuid in bulk_static_uuids 99 | ] 100 | # Get the doc with the lowest dft_energy 101 | d = sorted(docs, key=lambda x: x["calcs_reversed"][-1]["output"]["energy"])[0] 102 | 103 | # Collect data 104 | mag_label = d["magnetic_ordering"] 105 | logger.info(f"Selecting {mag_label} as the most stable!") 106 | structure_dict = d["calcs_reversed"][0]["output"]["structure"] 107 | dft_energy = d["calcs_reversed"][0]["output"]["energy"] 108 | structure = Structure.from_dict(structure_dict) 109 | oxide_type = OxideType(structure).parse_oxide()[0] 110 | # Add the initial magmoms to the summary dict 111 | summary_dict["orig_magmoms"] = d["orig_inputs"]["incar"]["MAGMOM"] 112 | summary_dict["structure"] = structure.as_dict() 113 | summary_dict["formula_pretty"] = structure.composition.reduced_formula 114 | summary_dict["oxide_type"] = oxide_type 115 | summary_dict["uncorrected_energy"] = dft_energy 116 | # Get Correction 117 | bulk_composition = structure.composition 118 | comp_dict = {str(key): value for key, value in bulk_composition.items()} 119 | comp_dict_pbx = { 120 | str(key): value 121 | for key, value in bulk_composition.items() 122 | if key not in ELEMENTS_HO 123 | } 124 | correction = 0 125 | for i in list(comp_dict.keys()): 126 | if i in composition_correction: 127 | correction += composition_correction[i] * comp_dict[i] 128 | 129 | if oxide_type in list(oxide_type_correction.keys()): 130 | correction += oxide_type_correction[oxide_type] * comp_dict["O"] 131 | 132 | corrected_energy = dft_energy + correction 133 | summary_dict["correction"] = correction 134 | summary_dict["corrected_energy"] = corrected_energy 135 | 136 | # Parameters + data 137 | parameters = {} 138 | parameters["oxide_type"] = str(oxide_type) 139 | 140 | data = {} 141 | data["oxide_type"] = str(oxide_type) 142 | 143 | # Computed Entry 144 | computed_entry = ComputedEntry( 145 | composition=bulk_composition, 146 | energy=dft_energy, 147 | correction=correction, 148 | parameters=parameters, 149 | data=data, 150 | entry_id=f"{bulk_formula}_{mag_label}_{bulk_stability_uuid}", 151 | ) 152 | 153 | # PhaseDiagram Analysis 154 | with MPRester() as mpr: 155 | # Get PhaseDiagram Entry 156 | phd_entry = PDEntry( 157 | bulk_composition, energy=corrected_energy, name="phd_entry" 158 | ) 159 | # chemsys from comp 160 | chemsys = list(comp_dict.keys()) 161 | 162 | # Check compatibility 163 | unprocessed_entries = mpr.get_entries_in_chemsys(chemsys) 164 | processed_entries = compat.process_entries(unprocessed_entries) 165 | processed_entries.append(phd_entry) 166 | 167 | # Build PhaseDiagram 168 | phd = PhaseDiagram(processed_entries) 169 | 170 | # Get PhD info 171 | eform_phd = phd.get_form_energy(phd_entry) 172 | eform_atom_phd = phd.get_form_energy_per_atom(phd_entry) 173 | e_hull_phd = phd.get_e_above_hull(phd_entry) 174 | 175 | # Store PhD data 176 | summary_dict["eform_phd"] = eform_phd 177 | summary_dict["eform_atom_phd"] = eform_atom_phd 178 | summary_dict["e_hull_phd"] = e_hull_phd 179 | 180 | # Pourbaix Diagram Analysis 181 | with MPRester() as mpr: 182 | # chemsys from compostion for pbx (no OH) 183 | chemsys = list(comp_dict_pbx.keys()) 184 | 185 | # Get pbx_entries, pbx_entry and Ef/atom 186 | pbx_entries, pbx_entry, eform_atom_pbx = self.get_pourbaix_entries( 187 | mpr, computed_entry 188 | ) 189 | 190 | pbx_entries.append(pbx_entry) 191 | 192 | # PBX Diagram 193 | pbx = PourbaixDiagram( 194 | pbx_entries, comp_dict=comp_dict_pbx, filter_solids=False 195 | ) 196 | 197 | # Get electrochemical stability at conditions 198 | oer_stability = pbx.get_decomposition_energy(pbx_entry, pH=0, V=1.23) # OER 199 | 200 | # Store PBX data 201 | summary_dict["eform_atom_pbx"] = eform_atom_pbx 202 | summary_dict["oer_stability"] = oer_stability 203 | 204 | # Get pbx plot 205 | if pbx_plot: 206 | plt = PourbaixPlotter(pbx).plot_entry_stability( 207 | pbx_entry, label_domains=True 208 | ) 209 | plt.savefig( 210 | f"{bulk_formula}_{mag_label}_pbx.png", dpi=300 211 | ) # FIXME: Think this should be mag_label 212 | 213 | # Export json file 214 | with open(f"{bulk_formula}_{mag_label}_stability_analysis.json", "w") as f: 215 | f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) 216 | 217 | # To_DB 218 | if to_db: 219 | mmdb.collection = mmdb.db[f"{bulk_formula}_stability_analysis"] 220 | mmdb.collection.insert_one(summary_dict) 221 | 222 | # Logger 223 | logger.info("Stability Analysis Completed!") 224 | 225 | def get_pourbaix_entries( 226 | self, mpr, comp_entry, solid_compat="MaterialsProject2020Compatibility" 227 | ): 228 | """ 229 | A helper function to get all entries necessary to generate PBX 230 | """ 231 | import warnings 232 | import itertools 233 | from pymatgen.core.composition import Composition 234 | from pymatgen.core.periodic_table import Element 235 | from pymatgen.analysis.phase_diagram import PhaseDiagram 236 | from pymatgen.analysis.pourbaix_diagram import IonEntry, PourbaixEntry 237 | from pymatgen.core.ion import Ion 238 | from pymatgen.entries.compatibility import ( 239 | Compatibility, 240 | MaterialsProject2020Compatibility, 241 | MaterialsProjectCompatibility, 242 | ) 243 | 244 | # Selecting compatibility 245 | if solid_compat == "MaterialsProjectCompatibility": 246 | solid_compat = MaterialsProjectCompatibility() 247 | elif solid_compat == "MaterialsProject2020Compatibility": 248 | solid_compat = MaterialsProject2020Compatibility() 249 | elif isinstance(solid_compat, Compatibility): 250 | solid_compat = solid_compat 251 | 252 | # Comp_entry 253 | entry_composition = comp_entry.composition 254 | comp_dict = { 255 | str(key): value 256 | for key, value in entry_composition.items() 257 | if key not in ELEMENTS_HO 258 | } 259 | chemsys = list(comp_dict.keys()) 260 | 261 | # Store PBX entries 262 | pbx_entries = [] 263 | 264 | if isinstance(chemsys, str): 265 | chemsys = chemsys.split("-") 266 | 267 | # Get ion entries first, because certain ions have reference 268 | url = "/pourbaix_diagram/reference_data/" + "-".join(chemsys) 269 | ion_data = mpr._make_request(url) 270 | ion_ref_comps = [Composition(d["Reference Solid"]) for d in ion_data] 271 | ion_ref_elts = list( 272 | itertools.chain.from_iterable(i.elements for i in ion_ref_comps) 273 | ) 274 | ion_ref_entries = mpr.get_entries_in_chemsys( 275 | list(set([str(e) for e in ion_ref_elts] + ["O", "H"])), 276 | property_data=["e_above_hull"], 277 | compatible_only=False, 278 | ) 279 | 280 | with warnings.catch_warnings(): 281 | warnings.filterwarnings( 282 | "ignore", 283 | message="You did not provide the required O2 and H2O energies.", 284 | ) 285 | compat = MaterialsProjectAqueousCompatibility(solid_compat=solid_compat) 286 | 287 | with warnings.catch_warnings(): 288 | warnings.filterwarnings("ignore", "Failed to guess oxidation states.*") 289 | ion_ref_entries = compat.process_entries(ion_ref_entries) 290 | ion_ref_pd = PhaseDiagram(ion_ref_entries) 291 | 292 | # position the ion energies relative to most stable reference state 293 | for n, i_d in enumerate(ion_data): 294 | ion = Ion.from_formula(i_d["Name"]) 295 | refs = [ 296 | e 297 | for e in ion_ref_entries 298 | if e.composition.reduced_formula == i_d["Reference Solid"] 299 | ] 300 | if not refs: 301 | raise ValueError("Reference soldi not contained in entry list") 302 | stable_ref = sorted(refs, key=lambda x: x.data["e_above_hull"])[0] 303 | rf = stable_ref.composition.get_reduced_composition_and_factor()[1] 304 | 305 | solid_diff = ( 306 | ion_ref_pd.get_form_energy(stable_ref) 307 | - i_d["Reference solid energy"] * rf 308 | ) 309 | elt = i_d["Major_Elements"][0] 310 | correction_factor = ion.composition[elt] / stable_ref.composition[elt] 311 | energy = i_d["Energy"] + solid_diff * correction_factor 312 | ion_entry = IonEntry(ion, energy) 313 | pbx_entries.append(PourbaixEntry(ion_entry, "ion-{}".format(n))) 314 | 315 | # Construct the solid pourbaix entries from filtered ion_ref entries 316 | extra_elts = ( 317 | set(ion_ref_elts) 318 | - {Element(s) for s in chemsys} 319 | - {Element("H"), Element("O")} 320 | ) 321 | for entry in ion_ref_entries: 322 | entry_elts = set(entry.composition.elements) 323 | # Ensure no OH chemsys or extraneous elements from ion references 324 | if not ( 325 | entry_elts <= {Element("H"), Element("O")} 326 | or extra_elts.intersection(entry_elts) 327 | ): 328 | # Create new computed entry 329 | form_e = ion_ref_pd.get_form_energy(entry) 330 | new_entry = ComputedEntry( 331 | entry.composition, form_e, entry_id=entry.entry_id 332 | ) 333 | pbx_entry = PourbaixEntry(new_entry) 334 | pbx_entries.append(pbx_entry) 335 | 336 | # New Computed Entry 337 | formation_energy = ion_ref_pd.get_form_energy(comp_entry) 338 | formation_energy_per_atom = ion_ref_pd.get_form_energy_per_atom(comp_entry) 339 | new_entry = ComputedEntry( 340 | comp_entry.composition, formation_energy, entry_id=comp_entry.entry_id 341 | ) 342 | pbx_entry = PourbaixEntry(new_entry) 343 | 344 | return pbx_entries, pbx_entry, formation_energy_per_atom 345 | -------------------------------------------------------------------------------- /WhereWulff/analysis/equation_of_states.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import json 9 | import uuid 10 | 11 | from pydash.objects import has, get 12 | 13 | from pymatgen.core.structure import Structure 14 | from pymatgen.analysis.eos import EOS 15 | 16 | from fireworks import FiretaskBase, FWAction, explicit_serialize 17 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 18 | 19 | from atomate.utils.utils import env_chk 20 | from atomate.utils.utils import get_logger 21 | from atomate.vasp.database import VaspCalcDb 22 | 23 | 24 | logger = get_logger(__name__) 25 | 26 | 27 | @explicit_serialize 28 | class FitEquationOfStateFW(FiretaskBase): 29 | """ 30 | Automated analysis task to fit an EOS after transmutter workflow. 31 | 32 | Args: 33 | eos (default: vinet): Selecting the EOS used for fitting E vs Vol. 34 | plot (default: True) : Exporting automatically EOS plot. 35 | to_db (default: True) : Adding new data to EOS collection. 36 | db_file : Environment variable to connect to the DB. 37 | 38 | Return: 39 | Equilibrium structure for a given Bulk using 40 | (Energy vs Volume) deformations. 41 | 42 | """ 43 | 44 | required_params = ["magnetic_ordering", "db_file"] 45 | optional_params = ["eos", "plot", "to_db"] 46 | 47 | def run_task(self, fw_spec): 48 | 49 | # Variables 50 | bulk_uuid = fw_spec.get("oriented_uuid") 51 | magnetic_ordering = self["magnetic_ordering"] 52 | eos = self.get("eos", "vinet") 53 | db_file = env_chk(self.get("db_file"), fw_spec) 54 | to_db = self.get("to_db", True) 55 | plot = self.get("plot", True) 56 | summary_dict = {"eos": eos, "magnetic_ordering": magnetic_ordering} 57 | 58 | # new uuid for the eos-analysis 59 | eos_uuid = uuid.uuid4() 60 | summary_dict["eos_uuid"] = eos_uuid 61 | 62 | # Connect to DB 63 | all_task_ids = [] 64 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 65 | 66 | # Find optimization through bulk_uuid 67 | d = mmdb.collection.find_one({"uuid": bulk_uuid}) 68 | 69 | # Get geometry from optimization 70 | structure_dict = d["calcs_reversed"][-1]["output"]["structure"] 71 | structure = Structure.from_dict(structure_dict) 72 | pretty_formula = structure.composition.reduced_formula 73 | all_task_ids.append(d["task_id"]) 74 | # Retriving bulk deformations 75 | # Get all the UUIDs that were sent from the deformation FWs 76 | deformation_uuids = [fw_spec[k] for k in fw_spec if "deformation" in k] 77 | docs = [ 78 | mmdb.collection.find_one({"deform_uuid": deform_uuid}) 79 | for deform_uuid in deformation_uuids 80 | ] 81 | # Store optimized Bulk and pretty-formula 82 | summary_dict["structure_orig"] = structure.as_dict() 83 | summary_dict["formula_pretty"] = pretty_formula 84 | 85 | # Get (energy, volume) from the deformations 86 | energies, volumes = [], [] 87 | for i, d in enumerate(docs): 88 | if i == 0: 89 | magmoms_list = d["orig_inputs"]["incar"][ 90 | "MAGMOM" 91 | ] # Store the magmoms from the first deformation 92 | # Assert that the deformations are all consistent in their orderings 93 | assert magmoms_list == d["orig_inputs"]["incar"]["MAGMOM"] 94 | s = Structure.from_dict(d["calcs_reversed"][-1]["output"]["structure"]) 95 | energies.append(d["calcs_reversed"][-1]["output"]["energy"]) 96 | volumes.append(s.volume) 97 | all_task_ids.append(d["task_id"]) 98 | 99 | # Append to summary_dict 100 | summary_dict["energies"] = energies 101 | summary_dict["volumes"] = volumes 102 | summary_dict["all_task_ids"] = all_task_ids 103 | 104 | # Fit the equation-of-states 105 | eos = EOS(eos_name=eos) 106 | eos_fit = eos.fit(volumes, energies) 107 | summary_dict["volume_eq"] = eos_fit.v0 108 | summary_dict["energy_eq"] = eos_fit.e0 109 | 110 | # Scale optimized structure to the equilibrium volume 111 | structure.scale_lattice(eos_fit.v0) 112 | # Decorate the optimized structure with the relevant magmoms as a property 113 | structure.add_site_property("magmom", magmoms_list) 114 | summary_dict["structure_eq"] = structure.as_dict() 115 | 116 | # Store data on summary_dict 117 | summary_dict[ 118 | "task_label" 119 | ] = f"{pretty_formula}_{magnetic_ordering}_eos_{eos_uuid}" 120 | 121 | # Add results to db or json file 122 | if to_db: 123 | mmdb.collection = mmdb.db[f"{pretty_formula}_eos"] 124 | mmdb.collection.insert_one(summary_dict) 125 | 126 | # Export json file 127 | with open(f"{pretty_formula}_{magnetic_ordering}_eos_analysis.json", "w") as f: 128 | f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) 129 | 130 | # Export plot 131 | if plot: 132 | eos_plot = eos_fit.plot() 133 | eos_plot.savefig( 134 | f"{pretty_formula}_{magnetic_ordering}_eos_plot.png", dpi=300 135 | ) 136 | 137 | # logger 138 | logger.info("EOS Fitting Completed!") 139 | # Exit function by sending the eos_uuid to the static_bulk FireTask 140 | return FWAction( 141 | update_spec={f"eos_uuid_{magnetic_ordering}": summary_dict["task_label"]} 142 | ) 143 | -------------------------------------------------------------------------------- /WhereWulff/analysis/oer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import json 9 | import uuid 10 | 11 | import re 12 | import numpy as np 13 | 14 | from pymatgen.core import Structure 15 | from pymatgen.core.composition import Composition 16 | from pymatgen.core.surface import Slab 17 | 18 | from pydash.objects import has, get 19 | 20 | from fireworks import FiretaskBase, FWAction, explicit_serialize 21 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 22 | 23 | from atomate.utils.utils import env_chk 24 | from atomate.utils.utils import get_logger 25 | from atomate.vasp.database import VaspCalcDb 26 | 27 | logger = get_logger(__name__) 28 | 29 | 30 | @explicit_serialize 31 | class OER_SingleSiteAnalyzer(FiretaskBase): 32 | """ 33 | Post-processing FireTask to derive Delta G's for 34 | OER (WNA) mechanism in a single active site and 35 | derive the theoretical overpotential. 36 | 37 | Args: 38 | reduced_formula (e.g IrO2) : Formula of the given material. 39 | miller_index (e.g [1,1,0]): Crystallographic orientation from slab model. 40 | metal_site (e.g Ir) : Targeted element as reactive site in the slab model. 41 | slab_uuid (str) : Unique hash to identify previous jobs in the same run. 42 | ads_slabs_uuid (str) : Unique hashes from PBX to identify those DFT runs. 43 | surface_termination (str) : Either clean, OH or Ox from Surface Pourbaix workflow. 44 | db_file (env) : Environment variable to connect to the DB. 45 | 46 | Returns: 47 | OER Single site Reactivity post-processing for a given surface 48 | and DB json data. 49 | """ 50 | 51 | required_params = [ 52 | "reduced_formula", 53 | "miller_index", 54 | "metal_site", 55 | "slab_uuid", 56 | "ads_slab_uuids", 57 | "surface_termination", 58 | "db_file", 59 | "surface_pbx_uuid", 60 | ] 61 | optional_params = ["to_db"] 62 | 63 | def run_task(self, fw_spec): 64 | 65 | # Variables 66 | db_file = env_chk(self.get("db_file"), fw_spec) 67 | to_db = self.get("to_db", True) 68 | self.reduced_formula = self["reduced_formula"] 69 | self.miller_index = self["miller_index"] 70 | self.metal_site = self["metal_site"] 71 | slab_uuid = self["slab_uuid"] 72 | ads_slab_uuids = self["ads_slab_uuids"] 73 | surface_termination = self["surface_termination"] 74 | surface_pbx_uuid = self["surface_pbx_uuid"] 75 | 76 | # parent_dict = fw_spec[f"{self.reduced_formula}_{self.miller_index}_surface_pbx"] 77 | # surface_pbx_uuid = parent_dict["surface_pbx_uuid"] 78 | 79 | # Get the dynamic adslab uuids from the fw_spec. 80 | # Note that this will be different from the orig_ads_slab_uuids 81 | # if the AdSlab Continuation is triggered from wall time issues 82 | ads_slab_uuids = [ 83 | fw_spec[k]["adslab_uuid"] 84 | for k in fw_spec 85 | if f"{self.reduced_formula}-{self.miller_index}" in k 86 | ] 87 | 88 | # Summary dict 89 | summary_dict = { 90 | "reduced_formula": self.reduced_formula, 91 | "miller_index": self.miller_index, 92 | "metal_site": self.metal_site, 93 | "slab_uuid": slab_uuid, 94 | "ads_slab_uuids": ads_slab_uuids, 95 | } 96 | 97 | # OER variables 98 | self.ref_energies = {"H2O": -14.25994015, "H2": -6.77818501} 99 | 100 | # Reactivity uuid 101 | oer_single_site_uuid = uuid.uuid4() 102 | summary_dict["oer_single_site"] = str(oer_single_site_uuid) 103 | 104 | # Connect to DB 105 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 106 | 107 | # Retrieve the surface termination from pbx collection 108 | pbx_collection = mmdb.db[ 109 | f"{self.reduced_formula}-{self.miller_index}_surface_pbx" 110 | ] 111 | doc_termination = pbx_collection.find_one( 112 | {"surface_pbx_uuid": surface_pbx_uuid} 113 | ) 114 | 115 | stable_termination = Slab.from_dict( 116 | doc_termination[f"slab_{surface_termination}"] 117 | ) # ox or oh 118 | 119 | # Filter OER intermediates (reference, OH_n, O_n, OOH_up_n, OOH_down_n) 120 | oer_intermediates_uuid, oer_intermediates_energy = {}, {} 121 | dft_energy_oh_min, dft_energy_ooh_up_min, dft_energy_ooh_down_min = ( 122 | np.inf, 123 | np.inf, 124 | np.inf, 125 | ) 126 | for n, ads_slab_uuid in enumerate(ads_slab_uuids): 127 | doc_oer = mmdb.collection.find_one({"uuid": ads_slab_uuid}) 128 | oer_task_label = doc_oer["task_label"] 129 | adsorbate_label = oer_task_label.split("-")[3] 130 | # reference active site 131 | if "reference" in adsorbate_label: 132 | dft_energy_reference = doc_oer["calcs_reversed"][-1]["output"]["energy"] 133 | oer_uuid_reference = ads_slab_uuid 134 | oer_intermediates_uuid["reference"] = oer_uuid_reference 135 | oer_intermediates_energy["reference"] = dft_energy_reference 136 | # select OH intermediate as min dft energy 137 | if re.match("^OH_.*", adsorbate_label): 138 | dft_energy_oh = doc_oer["calcs_reversed"][-1]["output"]["energy"] 139 | if dft_energy_oh <= dft_energy_oh_min: 140 | dft_energy_oh_min = dft_energy_oh 141 | oer_uuid_oh = adsorbate_label 142 | oer_intermediates_uuid["OH"] = oer_uuid_oh 143 | oer_intermediates_energy["OH"] = dft_energy_oh_min 144 | # select Ox intermediate as min dft energy (no rotation - just one) 145 | if "O_" in adsorbate_label: 146 | dft_energy_oh = doc_oer["calcs_reversed"][-1]["output"]["energy"] 147 | oer_uuid_ox = adsorbate_label 148 | oer_intermediates_uuid["Ox"] = oer_uuid_ox 149 | oer_intermediates_energy["Ox"] = dft_energy_oh 150 | # select OOH_up intermediate as min dft energy 151 | if re.match("^OOH_up_.*", adsorbate_label): 152 | dft_energy_ooh_up = doc_oer["calcs_reversed"][-1]["output"]["energy"] 153 | if dft_energy_ooh_up <= dft_energy_ooh_up_min: 154 | oer_uuid_ooh_up = adsorbate_label 155 | dft_energy_ooh_up_min = dft_energy_ooh_up 156 | oer_intermediates_uuid["OOH_up"] = oer_uuid_ooh_up 157 | oer_intermediates_energy["OOH_up"] = dft_energy_ooh_up_min 158 | # select OOH_down intermediate as min dft energy 159 | if re.match("^OOH_down_.*", adsorbate_label): 160 | dft_energy_ooh_down = doc_oer["calcs_reversed"][-1]["output"]["energy"] 161 | if dft_energy_ooh_down <= dft_energy_ooh_down_min: 162 | oer_uuid_ooh_down = adsorbate_label 163 | dft_energy_ooh_down_min = dft_energy_ooh_down 164 | oer_intermediates_uuid["OOH_down"] = oer_uuid_ooh_down 165 | oer_intermediates_energy["OOH_down"] = dft_energy_ooh_down_min 166 | 167 | # Add termination as OER intermediate 168 | if surface_termination == "ox": 169 | oer_intermediates_energy["Ox"] = stable_termination.energy 170 | 171 | if surface_termination == "oh": 172 | oer_intermediates_energy["OH"] = stable_termination.energy 173 | 174 | # Add both oer dicts into summary_dict 175 | summary_dict["oer_uuids"] = oer_intermediates_uuid 176 | summary_dict["oer_energies"] = oer_intermediates_energy 177 | 178 | # Compute delta G 179 | delta_g_oer_dict = {} 180 | 181 | # Eads_OH 182 | eads_oh = self.Eads_OH( 183 | oer_intermediates_energy["OH"], 184 | oer_intermediates_energy["reference"], 185 | thermo_correction=None, 186 | ) 187 | 188 | delta_g_oer_dict["g_oh"] = eads_oh 189 | 190 | # Eads_Ox 191 | eads_ox = self.Eads_Ox( 192 | oer_intermediates_energy["Ox"], 193 | oer_intermediates_energy["reference"], 194 | thermo_correction=None, 195 | ) 196 | 197 | delta_g_oer_dict["g_ox"] = eads_ox 198 | 199 | # Eads_OOH 200 | eads_ooh_up = self.Eads_OOH( 201 | oer_intermediates_energy["OOH_up"], 202 | oer_intermediates_energy["reference"], 203 | thermo_correction=None, 204 | ) 205 | 206 | eads_ooh_down = self.Eads_OOH( 207 | oer_intermediates_energy["OOH_down"], 208 | oer_intermediates_energy["reference"], 209 | thermo_correction=None, 210 | ) 211 | 212 | # Select between OOH_up and OOH_down 213 | if eads_ooh_up <= eads_ooh_down: 214 | delta_g_oer_dict["g_ooh"] = eads_ooh_up 215 | 216 | if eads_ooh_down <= eads_ooh_up: 217 | delta_g_oer_dict["g_ooh"] = eads_ooh_down 218 | 219 | # O2 evolution 220 | o2_evol = self.oxygen_evolution(delta_g_oer_dict["g_ooh"], std_potential=4.92) 221 | 222 | delta_g_oer_dict["g_o2"] = o2_evol 223 | 224 | # Linear Relationships - Theoretical overpotential - PDS 225 | oer_dict = self.linear_relationships_and_overpotential(delta_g_oer_dict) 226 | overpotential = oer_dict["overpotential"] 227 | pds_step = oer_dict["PDS"] 228 | 229 | # Add oer_dict to summary_dict 230 | summary_dict["oer_info"] = oer_dict 231 | summary_dict["overpotential"] = overpotential 232 | summary_dict["PDS"] = pds_step 233 | 234 | # Export to json file 235 | with open( 236 | f"{self.reduced_formula}_{self.miller_index}_{self.metal_site}_oer.json", 237 | "w", 238 | ) as f: 239 | f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) 240 | 241 | # To DB -> (This should be unique every time) 242 | if to_db: 243 | mmdb.collection = mmdb.db[ 244 | f"{self.reduced_formula}-{self.miller_index}_{self.metal_site}_oer_single_site" 245 | ] 246 | mmdb.collection.insert_one(summary_dict) 247 | 248 | # Logger 249 | logger.info( 250 | f"{self.reduced_formula}-{self.miller_index} -> (overpotential: {overpotential}, PDS: {pds_step})" 251 | ) 252 | 253 | # Send the summary_dict to the child FW (?) 254 | return FWAction( 255 | stored_data={ 256 | f"{self.reduced_formula}_{self.miller_index}_{self.metal_site}_oer_single_site": { 257 | "oer_single_site_uuid": str(oer_single_site_uuid), 258 | "overpotential": overpotential, 259 | "PDS": pds_step, 260 | "oer_info": oer_dict, 261 | } 262 | } 263 | ) 264 | 265 | # TODO: Abstract the min energy 266 | def _get_min_energy_intermediate(self): 267 | """Returns min DFT energy across same intermediate""" 268 | return 269 | 270 | def Eads_OH(self, energy_oh, energy_clean, thermo_correction=None): 271 | """ 272 | Reaction: H2O + (*) --> OH* + H+ + e- 273 | Args: 274 | energy_oh 275 | energy_clean 276 | thermo_correction 277 | Returns: 278 | Delta G(OH) value 279 | """ 280 | eads_oh = ( 281 | energy_oh 282 | - energy_clean 283 | - (self.ref_energies["H2O"] - (0.5 * self.ref_energies["H2"])) 284 | ) 285 | if thermo_correction: 286 | eads_oh = eads_oh + thermo_correction 287 | return eads_oh 288 | else: 289 | return eads_oh 290 | 291 | def Eads_Ox(self, energy_ox, energy_clean, thermo_correction=None): 292 | """ 293 | Reaction: OH* --> O* + H+ + e- 294 | Args: 295 | energy_ox 296 | energy_clean 297 | thermo_correction 298 | Returns: 299 | Delta G(Ox) value 300 | """ 301 | eads_ox = ( 302 | energy_ox 303 | - energy_clean 304 | - (self.ref_energies["H2O"] - self.ref_energies["H2"]) 305 | ) 306 | if thermo_correction: 307 | eads_ox = eads_ox + thermo_correction 308 | return eads_ox 309 | else: 310 | return eads_ox 311 | 312 | def Eads_OOH(self, energy_ooh, energy_clean, thermo_correction=None): 313 | """ 314 | Reaction: O* + H2O --> OOH* + H+ + e- 315 | Args: 316 | energy_ooh 317 | energy_clean 318 | thermo_correction 319 | Returns: 320 | Delta G(OOH) value 321 | """ 322 | eads_ooh = ( 323 | energy_ooh 324 | - energy_clean 325 | - ((2 * self.ref_energies["H2O"]) - (1.5 * self.ref_energies["H2"])) 326 | ) 327 | if thermo_correction: 328 | eads_ooh = eads_ooh + thermo_correction 329 | return eads_ooh 330 | else: 331 | return eads_ooh 332 | 333 | def oxygen_evolution(self, eads_ooh, std_potential=4.92): 334 | """ 335 | Reaction: OOH* --> O2(g) + (*) + H+ + e- 336 | Args: 337 | eads_ooh 338 | std_potential (default: 4.92) 339 | Returns: 340 | Delta G of the O2 evolution (last step) 341 | """ 342 | o2_release = std_potential - eads_ooh 343 | return o2_release 344 | 345 | def linear_relationships_and_overpotential(self, delta_g_dict): 346 | """ 347 | Computes linear relationships and derives theoretical overpotential 348 | Args: 349 | delta_g_dict = {"g_oh", "g_ox", "g_ooh", "g_o2"} 350 | Returns: 351 | Dictionary with linear relationships and overpotential. 352 | """ 353 | # Linear relationships 354 | ox_oh = delta_g_dict["g_ox"] - delta_g_dict["g_oh"] 355 | ooh_ox = delta_g_dict["g_ooh"] - delta_g_dict["g_ox"] 356 | linear_relationships_dict = { 357 | "g_oh": delta_g_dict["g_oh"], 358 | "ox_oh": ox_oh, 359 | "ooh_ox": ooh_ox, 360 | "g_o2": delta_g_dict["g_o2"], 361 | } 362 | 363 | # Find max in linear_rel. dict 364 | find_max_step = max( 365 | linear_relationships_dict, key=linear_relationships_dict.get 366 | ) 367 | 368 | # Theoretical overpotential 369 | oer_overpotential = linear_relationships_dict[find_max_step] - 1.23 370 | 371 | # Result Dict 372 | result_dict = {**delta_g_dict, **linear_relationships_dict} 373 | result_dict["overpotential"] = oer_overpotential 374 | result_dict["PDS"] = find_max_step 375 | 376 | return result_dict 377 | -------------------------------------------------------------------------------- /WhereWulff/analysis/wulff_shape.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import json 9 | import uuid 10 | 11 | from pydash.objects import has, get 12 | 13 | from fireworks import FiretaskBase, FWAction, explicit_serialize 14 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 15 | 16 | from atomate.utils.utils import env_chk 17 | from atomate.utils.utils import get_logger 18 | from atomate.vasp.database import VaspCalcDb 19 | 20 | 21 | logger = get_logger(__name__) 22 | 23 | 24 | # Help function to avovid tuples as key. 25 | def json_format(dt): 26 | dt_new = {} 27 | for k, v in dt.items(): 28 | k_str = "".join(map(str, k)) 29 | dt_new.update({k_str: v}) 30 | return dt_new 31 | 32 | 33 | @explicit_serialize 34 | class WulffShapeFW(FiretaskBase): 35 | """ 36 | FireTask to do the Wulff-Shape Analysis. 37 | 38 | Args: 39 | tag: datetime string for folder name and id. 40 | db_file: database file path 41 | slab_formula: Reduced formula of the slab model e.g (RuO2) 42 | miller_index: Crystallographic orientations of the slab model. 43 | wulff_plot (default: False): Get automatically the wulffshape plot. 44 | to_db (default: True): Save the data on the db or in a json_file. 45 | 46 | return: 47 | summary_dict (JSON) with Wulff-Shape information inside. 48 | """ 49 | 50 | required_params = ["bulk_structure", "db_file"] 51 | optional_params = ["wulff_plot", "to_db"] 52 | 53 | def run_task(self, fw_spec): 54 | 55 | # Variables 56 | db_file = env_chk(self.get("db_file"), fw_spec) 57 | to_db = self.get("to_db", True) 58 | wulff_plot = self.get("wulff_plot", True) 59 | bulk_structure = self["bulk_structure"] 60 | Ev2Joule = 16.0219 # eV/Angs2 to J/m2 61 | summary_dict = {} 62 | 63 | # new uuid for the wulff-shape 64 | wulff_shape_uuid = uuid.uuid4() 65 | summary_dict["wulff_uuid"] = wulff_shape_uuid 66 | 67 | # Bulk formula 68 | bulk_formula = bulk_structure.composition.reduced_formula 69 | 70 | # Connect to DB and Surface Energies collection 71 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 72 | collection = mmdb.db["surface_energies"] 73 | 74 | # Find Surface energies for the given material + facet 75 | # docs = collection.find({"task_label": {"$regex": "^{}".format(bulk_formula)}}) 76 | reduced_formula_miller_indices = [ 77 | k for k in fw_spec if bulk_formula in k 78 | ] # retrieve all the keys with reduced_formula from fw_spec 79 | docs = [ 80 | collection.find_one( 81 | { 82 | "oriented_uuid": fw_spec[rfmi]["oriented_uuid"], 83 | "slab_uuid": fw_spec[rfmi]["slab_uuid"], 84 | } 85 | ) 86 | for rfmi in reduced_formula_miller_indices 87 | ] 88 | 89 | # Surface energy and structures dictionary 90 | surface_energies_dict = {} 91 | structures_dict = {} 92 | for d in docs: 93 | slab_struct = d["slab_struct"] # as dict 94 | miller_index = tuple(map(int, d["miller_index"])) 95 | surface_energy = abs( 96 | round(d["surface_energy"] * Ev2Joule, 4) 97 | ) # Round to 4 decimals 98 | surface_energies_dict.update({miller_index: surface_energy}) 99 | structures_dict.update({d["miller_index"]: slab_struct}) 100 | 101 | # Wulff Analysis 102 | wulffshape_obj, wulff_info, area_frac_dict = self.get_wulff_analysis( 103 | bulk_structure, surface_energies_dict 104 | ) 105 | 106 | # Store data on summary_dict 107 | summary_dict["task_label"] = "{}_wulff_shape_{}".format( 108 | bulk_formula, wulff_shape_uuid 109 | ) 110 | summary_dict["surface_energies"] = json_format(surface_energies_dict) 111 | summary_dict["wulff_info"] = wulff_info 112 | summary_dict["area_fractions"] = area_frac_dict 113 | summary_dict["slab_structures"] = structures_dict 114 | 115 | # Plot 116 | if wulff_plot: 117 | w_plot = wulffshape_obj.get_plot() 118 | w_plot.savefig("{}_wulff_shape.png".format(bulk_formula), dpi=100) 119 | 120 | # Add results to db 121 | if to_db: 122 | mmdb.collection = mmdb.db["{}_wulff_shape_analysis".format(bulk_formula)] 123 | mmdb.collection.insert_one(summary_dict) 124 | 125 | else: 126 | with open("{}_wulff_shape_analysis.json".format(bulk_formula), "w") as f: 127 | f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) 128 | 129 | # Logger 130 | logger.info("Wulff-Shape Analysis, Done!") 131 | 132 | # Send the uuid as unique wulff-shape 133 | return FWAction( 134 | update_spec={"wulff_uuid": wulff_shape_uuid}, 135 | propagate=True, 136 | ) 137 | 138 | def get_wulff_analysis(self, bulk_structure, surface_energies_dict): 139 | """ 140 | Makes the wulff analysis as a function of bulk lattice, facets 141 | and their surface energy. 142 | 143 | Args: 144 | bulk_structure (Structure): Easy way to get lattice paramenters. 145 | surface_enegies_dict (dict): {hkl: surface_energy (J/m2)} 146 | 147 | Return: 148 | Wulff Shape Analysis information. 149 | """ 150 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer 151 | from pymatgen.analysis.wulff import WulffShape 152 | 153 | # Conventional standard structure 154 | SGA = SpacegroupAnalyzer(bulk_structure) 155 | bulk_struct = SGA.get_conventional_standard_structure() 156 | 157 | # Get key/values from energies dict J/m^2 158 | miller_list = surface_energies_dict.keys() 159 | e_surf_list = surface_energies_dict.values() 160 | 161 | # WulffShape Analysis 162 | wulffshape_obj = WulffShape(bulk_struct.lattice, miller_list, e_surf_list) 163 | 164 | # Collect wulffshape properties 165 | shape_factor = wulffshape_obj.shape_factor 166 | anisotropy = wulffshape_obj.anisotropy 167 | weight_surf_energy = wulffshape_obj.weighted_surface_energy # J/m2 168 | shape_volume = wulffshape_obj.volume 169 | effective_radius = wulffshape_obj.effective_radius 170 | area_frac_dict = json_format( 171 | wulffshape_obj.area_fraction_dict 172 | ) # {hkl: area_hkl/total area on wulff} 173 | 174 | wulff_info = { 175 | "shape_factor": shape_factor, 176 | "anisotropy": anisotropy, 177 | "weight_surf_energy": weight_surf_energy, 178 | "volume": shape_volume, 179 | "effective_radius": effective_radius, 180 | } 181 | 182 | return wulffshape_obj, wulff_info, area_frac_dict 183 | -------------------------------------------------------------------------------- /WhereWulff/common/glue_tasks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from fireworks import explicit_serialize, FiretaskBase 9 | from monty.shutil import gzip_dir 10 | 11 | 12 | @explicit_serialize 13 | class GzipPrevDir(FiretaskBase): 14 | """ 15 | Task to gzip a specific directory through calc_dir. 16 | """ 17 | 18 | required_params = ["calc_dir"] 19 | optional_params = [] 20 | 21 | def run_task(self, fw_spec=None): 22 | cwd = self["calc_dir"] 23 | gzip_dir(cwd) 24 | -------------------------------------------------------------------------------- /WhereWulff/dft_settings/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import numpy as np 9 | 10 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer 11 | from pymatgen.analysis.local_env import VoronoiNN 12 | from pymatgen.analysis.adsorption import AdsorbateSiteFinder 13 | from pymatgen.io.vasp.sets import MVLSlabSet 14 | from pymatgen.io.vasp.inputs import Kpoints 15 | 16 | 17 | # Get bulk initial magnetic moments 18 | def set_bulk_magmoms(structure, nm_magmom_buffer=0.6, tol=0.1, scale_factor=1.2): 19 | """ 20 | Returns decorated bulk structure with initial magnetic moments, 21 | based on crystal-field theory for TM. 22 | """ 23 | struct = structure.copy() 24 | # Voronoi NN 25 | voronoi_nn = VoronoiNN(tol=tol) 26 | # SPG Analysis 27 | sga = SpacegroupAnalyzer(struct) 28 | sym_struct = sga.get_symmetrized_structure() 29 | # Magnetic moments 30 | element_magmom = {} 31 | for idx in sym_struct.equivalent_indices: 32 | site = sym_struct[idx[0]] 33 | if site.specie.is_transition_metal: 34 | cn = voronoi_nn.get_cn(sym_struct, idx[0], use_weights=True) 35 | cn = round(cn, 5) 36 | # Filter between Oh or Td Coordinations 37 | if cn > 5.0: 38 | coordination = "oct" 39 | else: 40 | coordination = "tet" 41 | # Spin configuration depending on row 42 | if site.specie.row >= 5.0: 43 | spin_config = "low" 44 | else: 45 | spin_config = "high" 46 | # Magnetic moment per metal site 47 | magmom = site.specie.get_crystal_field_spin( 48 | coordination=coordination, spin_config=spin_config 49 | ) 50 | # Add to dict 51 | element_magmom.update( 52 | { 53 | str(site.species_string): abs(scale_factor * float(magmom)) 54 | if magmom > 0.01 55 | else nm_magmom_buffer 56 | } 57 | ) 58 | 59 | elif site.specie.is_chalcogen: # O 60 | element_magmom.update({str(site.species_string): 0.6}) 61 | 62 | else: 63 | element_magmom.update({str(site.species_string): nm_magmom_buffer}) 64 | 65 | magmoms = [element_magmom[site.species_string] for site in struct] 66 | 67 | # Decorate 68 | for site, magmom in zip(struct.sites, magmoms): 69 | site.properties["magmom"] = round(magmom, 2) 70 | return struct 71 | 72 | 73 | class SelectiveDynamics(AdsorbateSiteFinder): 74 | """ 75 | Different methods for Selective Dynamics. 76 | """ 77 | 78 | def __init__(self, slab): 79 | self.slab = slab.copy() 80 | 81 | @classmethod 82 | def center_of_mass(cls, slab): 83 | """Method based of center of mass.""" 84 | sd_list = [] 85 | sd_list = [ 86 | [False, False, False] 87 | if site.frac_coords[2] < slab.center_of_mass[2] 88 | else [True, True, True] 89 | for site in slab.sites 90 | ] 91 | new_sp = slab.site_properties 92 | new_sp["selective_dynamics"] = sd_list 93 | return slab.copy(site_properties=new_sp) 94 | 95 | 96 | # Theoretical DFT Level 97 | class MOSurfaceSet(MVLSlabSet): 98 | """ 99 | Custom VASP input class for MO slab calcs 100 | """ 101 | 102 | def __init__( 103 | self, 104 | structure, 105 | psp_version="PBE_54", 106 | bulk=False, 107 | set_mix=False, 108 | auto_dipole=True, 109 | initial_magmoms=None, 110 | **kwargs 111 | ): 112 | 113 | super(MOSurfaceSet, self).__init__( 114 | structure, bulk=bulk, set_mix=False, **kwargs 115 | ) 116 | 117 | # self.structure = structure 118 | self.psp_version = psp_version 119 | self.bulk = bulk 120 | self.auto_dipole = auto_dipole 121 | self.initial_magmoms = initial_magmoms 122 | self.set_mix = set_mix 123 | 124 | # Change the default PBE version from Pymatgen 125 | psp_versions = ["PBE", "PBE_52", "PBE_54"] 126 | assert self.psp_version in psp_versions 127 | # MOSurfaceSet.CONFIG["POTCAR_FUNCTIONAL"] = self.psp_version 128 | self.potcar_functional = self.psp_version 129 | 130 | # Dipolar moment correction 131 | def _get_center_of_mass(self): 132 | """From coordinates, weighted by specie, Return center of mass""" 133 | weights = [s.species.weight for s in self.structure] 134 | center_of_mass = np.average(self.structure.frac_coords, weights=weights, axis=0) 135 | return list(center_of_mass) 136 | 137 | @property 138 | def incar(self): 139 | incar = super(MOSurfaceSet, self).incar 140 | 141 | # Direct of reciprocal (depending if its bulk or slab) 142 | if self.bulk: 143 | incar["LREAL"] = False 144 | else: 145 | incar["LREAL"] = True 146 | 147 | # Setting auto_dipole correction (for slabs only) 148 | # if not self.bulk and self.auto_dipole: 149 | # incar["LDIPOL"] = True 150 | # incar["IDIPOL"] = 3 151 | # incar["DIPOL"] = self._get_center_of_mass() 152 | 153 | # Setting magnetic moments for children 154 | if self.initial_magmoms: 155 | incar["MAGMOM"] = self.initial_magmoms 156 | 157 | if "LDAUPRINT" in incar.keys(): 158 | incar["LDAUPRINT"] = 0 # silent mode 159 | 160 | # Incar Settings for optimization 161 | incar_config = { 162 | "GGA": "PE", 163 | "ENCUT": 500, 164 | "EDIFF": 1e-4, 165 | "EDIFFG": -0.05, 166 | "ISYM": 0, 167 | "SYMPREC": 1e-10, 168 | "ISPIN": 2, 169 | "ISIF": 0, 170 | "NSW": 300, 171 | "NCORE": 4, 172 | "LWAVE": True, 173 | "LCHARG": False, 174 | "LVTOT": False, 175 | "ISTART": 1, 176 | "NELM": 80, 177 | "NCORE": 4, 178 | "ISMEAR": 0, 179 | "SIGMA": 0.2, 180 | } 181 | # Update incar 182 | incar.update(incar_config) 183 | incar.update(self.user_incar_settings) 184 | 185 | return incar 186 | 187 | @property 188 | def kpoints(self): 189 | """ 190 | Monkhorst-pack Gamma Centered scheme: 191 | bulks [50/a x 50/b x 50/c] 192 | slabs [30/a x 30/b x 1] 193 | """ 194 | abc = np.array(self.structure.lattice.abc) 195 | 196 | if self.bulk: 197 | kpts = tuple(np.ceil(50.0 / abc).astype("int")) 198 | return Kpoints.gamma_automatic(kpts=kpts, shift=(0, 0, 0)) 199 | 200 | else: 201 | kpts = np.ceil(30.0 / abc).astype("int") 202 | kpts[2] = 1 203 | kpts = tuple(kpts) 204 | return Kpoints.gamma_automatic(kpts=kpts, shift=(0, 0, 0)) 205 | -------------------------------------------------------------------------------- /WhereWulff/firetasks/handlers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import uuid 9 | 10 | from pydash.objects import has, get 11 | 12 | from pymatgen.core import Structure 13 | from pymatgen.core.surface import Slab 14 | 15 | from fireworks import FiretaskBase, FWAction, explicit_serialize 16 | from atomate.utils.utils import env_chk 17 | from atomate.vasp.fireworks.core import OptimizeFW 18 | from atomate.vasp.database import VaspCalcDb 19 | from atomate.common.firetasks.glue_tasks import ( 20 | CopyFiles, 21 | DeleteFilesPrevFolder, 22 | GzipDir, 23 | ) 24 | 25 | from WhereWulff.dft_settings.settings import MOSurfaceSet 26 | from WhereWulff.common.glue_tasks import GzipPrevDir 27 | 28 | 29 | @explicit_serialize 30 | class ContinueOptimizeFW(FiretaskBase): 31 | """ 32 | Custom OptimizeFW Firetask that handles wall-time issues 33 | 34 | Args: 35 | is_bulk (bool): Determines DFT settings depending if its bulk or slab model. 36 | counter (int) : Counter wheter is a parent (counter = 0) or child job (counter > 0). 37 | db_file : Environment variable check to be able to connect to the database. 38 | vasp_cmd : Environment variable to execute vasp. 39 | 40 | Returns: 41 | Return a continuous Firetask that handles wall_times and files transfering 42 | between parent -> child -> terminal node. 43 | 44 | """ 45 | 46 | required_params = ["is_bulk", "counter", "db_file", "vasp_cmd"] 47 | optional_params = [] 48 | 49 | def run_task(self, fw_spec): 50 | 51 | # Variables 52 | is_bulk = self["is_bulk"] 53 | counter = self["counter"] 54 | db_file = env_chk(self.get("db_file"), fw_spec) 55 | vasp_cmd = self["vasp_cmd"] 56 | 57 | # Connect to DB 58 | client = VaspCalcDb.from_db_file(db_file, admin=True) 59 | db = client.db 60 | 61 | # Get the launch_id for the parent_FW 62 | launch_id = self.launchpad.fireworks.find_one({"fw_id": self.fw_id})[ 63 | "launches" 64 | ][0] 65 | 66 | # Parent Directory name 67 | parent_dir_name = self.launchpad.launches.find_one({"launch_id": launch_id})[ 68 | "launch_dir" 69 | ] 70 | 71 | # Check whether the FW hits a wall_time 72 | try: 73 | wall_time_reached_errors = [ 74 | correction["errors"][0] == "Walltime reached" 75 | for correction in db["tasks"].find_one({"uuid": fw_spec["uuid"]})[ 76 | "custodian" 77 | ][0]["corrections"] 78 | ] 79 | except (KeyError, TypeError) as e: 80 | print(f"{e}: Had trouble detecting errors in VaspRun...") 81 | wall_time_reached_errors = [] 82 | pass 83 | 84 | # Imtermediate nodes that mutate the workflow 85 | if counter < fw_spec["max_tries"] and any(wall_time_reached_errors): 86 | # Retrieving structure from parent 87 | 88 | if counter == 0: # root node 89 | uuid_lineage = [] 90 | else: 91 | uuid_lineage = fw_spec["uuid_lineage"] # inherit from parent 92 | 93 | # Retrieving lastest geometry from parent 94 | structure = Structure.from_dict( 95 | db["tasks"].find_one({"uuid": fw_spec["uuid"]})["output"]["structure"] 96 | ) 97 | 98 | # Slab object of parent - Only needed if doing slab or adslab 99 | if not fw_spec["is_bulk"]: 100 | slab = Slab.from_dict( 101 | db["tasks"].find_one({"uuid": fw_spec["uuid"]})["slab"] 102 | ) 103 | 104 | # Retriving magnetic moments from parent 105 | magmoms = structure.site_properties["magmom"] 106 | 107 | # counts 108 | counter += 1 109 | # vasp_input_set of parent, to be inherited by child, except for magmoms and structure 110 | vasp_input_set_parent_dict = fw_spec["_tasks"][0]["vasp_input_set"] 111 | # Update structure and magmoms tied to the parent input set with that of the child 112 | vasp_input_set_parent_dict["structure"] = structure.as_dict() 113 | vasp_input_set_parent_dict["user_incar_settings"] = {"MAGMOM": magmoms} 114 | vasp_input_set_parent_updated_struct_magmoms = MOSurfaceSet.from_dict( 115 | vasp_input_set_parent_dict 116 | ) 117 | 118 | # Create a unique uuid for child 119 | fw_new_uuid = uuid.uuid4() 120 | 121 | # UUID provenance for downstream nodes 122 | uuid_lineage.append(fw_spec["uuid"]) 123 | 124 | # OptimizeFW for child 125 | fw_new = OptimizeFW( 126 | name=fw_spec["name"], 127 | structure=structure, 128 | max_force_threshold=None, 129 | vasp_input_set=vasp_input_set_parent_updated_struct_magmoms, 130 | vasp_cmd=vasp_cmd, 131 | db_file=db_file, 132 | job_type="normal", 133 | spec={ 134 | "counter": counter, 135 | "_add_launchpad_and_fw_id": True, 136 | "_pass_job_info": True, 137 | "uuid_lineage": uuid_lineage, 138 | "uuid": fw_new_uuid, 139 | "max_tries": fw_spec["max_tries"], 140 | "name": fw_spec["name"], # pass parent name to child 141 | "wall_time": fw_spec["wall_time"], 142 | "is_bulk": True if fw_spec["is_bulk"] else False, 143 | "is_adslab": fw_spec.get("is_adslab"), 144 | }, 145 | ) 146 | 147 | # Appending extra tasks 148 | fw_new.tasks[3]["additional_fields"].update({"uuid": fw_new_uuid}) 149 | 150 | # Disable gunzip in RunVaspCustodian 151 | fw_new.tasks[1].update({"gzip_output": False}) 152 | 153 | # Insert a CopyFilesFromCalcLoc Task into the childFW to inherit 154 | fw_new.tasks.insert( 155 | 1, CopyFiles(from_dir=parent_dir_name, files_to_copy=["WAVECAR"]) 156 | ) 157 | 158 | # Insert a DeleteFiles Task into the childFW to delete previous WAVECAR 159 | fw_new.tasks.insert( 160 | 2, DeleteFilesPrevFolder(files=["WAVECAR"], calc_dir=parent_dir_name) 161 | ) 162 | 163 | # GzipPrevFolder 164 | # fw_new.tasks.insert(3, GzipPrevDir(calc_dir=parent_dir_name)) 165 | 166 | fw_new.tasks.append( 167 | ContinueOptimizeFW( 168 | is_bulk=is_bulk, counter=counter, db_file=db_file, vasp_cmd=vasp_cmd 169 | ) 170 | ) 171 | 172 | # Make sure that the child task doc from VaspToDB has the "Slab" object with wyckoff positions 173 | if counter > 0 and not fw_spec["is_bulk"]: 174 | fw_new.tasks[5]["additional_fields"].update({"slab": slab}) 175 | 176 | # Get the environment that the parent ran on (either laikapack or nersc for now) and enforce that 177 | # child runs on the same resource/filesystem. Additionally, if the root ran on laikapack and 178 | # job triggered walltime handler, then the child can relinquish wall_time constraints 179 | import os 180 | 181 | if "nid" in os.environ["HOSTNAME"] or "cori" in os.environ["HOSTNAME"]: 182 | fw_new.tasks[3].update({"wall_time": fw_spec["wall_time"]}) 183 | host = ( 184 | "nersc" # this needs to be in the fworker config as query on nersc 185 | ) 186 | elif "mo-wflow" in os.environ["HOSTNAME"]: 187 | # Switch off wall-time handling in child 188 | fw_new.spec["wall_time"] = None 189 | fw_new.tasks[3].update({"wall_time": None}) 190 | host = "laikapack" # should be in laikapack config 191 | 192 | # Pin the children down to the same filesystem as the root 193 | fw_new.spec["host"] = host 194 | fw_new.tasks[5].update({"defuse_unsuccesful": False}) 195 | 196 | # Bulk Continuation 197 | if is_bulk: 198 | return FWAction(detours=[fw_new]) 199 | 200 | # Slab Continuation 201 | elif not is_bulk and not fw_spec.get("is_adslab"): 202 | fw_new.spec["oriented_uuid"] = fw_spec["oriented_uuid"] 203 | return FWAction(detours=[fw_new]) 204 | 205 | # Adslab Continuation 206 | elif fw_spec["is_adslab"]: 207 | fw_new.spec["oriented_uuid"] = fw_spec["oriented_uuid"] 208 | fw_new.spec["slab_uuid"] = fw_spec["slab_uuid"] 209 | return FWAction(detours=[fw_new]) 210 | 211 | # Terminal node 212 | else: 213 | if is_bulk: 214 | # TODO: Possible by detours 215 | # fw_spec["_tasks"].append(GzipDir().to_dict()) 216 | # self.launchpad.fireworks.find_one_and_update( 217 | # {"fw_id": self.fw_id}, {"$set": {"spec._tasks": fw_spec["_tasks"]}} 218 | # ) 219 | return FWAction( 220 | update_spec={"oriented_uuid": fw_spec["uuid"]}, propagate=True 221 | ) 222 | 223 | elif not is_bulk and not fw_spec.get("is_adslab"): 224 | return FWAction( 225 | update_spec={ 226 | "oriented_uuid": fw_spec["oriented_uuid"] 227 | if "oriented_uuid" in fw_spec 228 | else None, 229 | "slab_uuid": fw_spec["uuid"], 230 | } 231 | ) 232 | elif fw_spec["is_adslab"]: 233 | return FWAction( 234 | update_spec={ 235 | fw_spec["name"]: { 236 | "oriented_uuid": fw_spec["oriented_uuid"], 237 | "slab_uuid": fw_spec["slab_uuid"], 238 | "adslab_uuid": fw_spec["uuid"], 239 | } 240 | } 241 | ) 242 | -------------------------------------------------------------------------------- /WhereWulff/firetasks/oer_single_site.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import uuid 9 | import numpy as np 10 | from pydash.objects import has, get 11 | 12 | from pymatgen.core.structure import Structure 13 | from pymatgen.core.surface import Slab 14 | 15 | from fireworks import FiretaskBase, FWAction, explicit_serialize 16 | 17 | from atomate.utils.utils import env_chk, get_logger 18 | from atomate.vasp.database import VaspCalcDb 19 | from atomate.vasp.config import VASP_CMD, DB_FILE 20 | 21 | from WhereWulff.reactivity.oer import OER_SingleSite 22 | from WhereWulff.adsorption.adsorbate_configs import oer_adsorbates_dict 23 | from WhereWulff.workflows.oer_single_site import OERSingleSite_WF 24 | 25 | logger = get_logger(__name__) 26 | 27 | 28 | @explicit_serialize 29 | class OERSingleSiteFireTask(FiretaskBase): 30 | """ 31 | OER Single Site FireTask. 32 | 33 | Args: 34 | reduced_formula (str): Reduced formula of the given material e.g RuO2 35 | miller_index (str): Miller index of the given surface e.g 110 36 | metal_site (str): OER site composition (selecting the metal) 37 | vasp_cmd (env): Environment variable to call vasp 38 | db_file (env): Environment variable to connect to the DB 39 | 40 | Returns: 41 | OERSingleSite Firetaks. 42 | """ 43 | 44 | required_params = [ 45 | "reduced_formula", 46 | "miller_index", 47 | "slab_orig", 48 | "bulk_like_sites", 49 | "ads_dict_orig", 50 | "metal_site", 51 | "applied_potential", 52 | "applied_pH", 53 | "vasp_cmd", 54 | "db_file", 55 | "surface_pbx_uuid", 56 | ] 57 | optional_params = [] 58 | 59 | def run_task(self, fw_spec): 60 | 61 | # Variables 62 | reduced_formula = self["reduced_formula"] 63 | miller_index = self["miller_index"] 64 | # slab_orig = self["slab_orig"] 65 | bulk_like_sites = self["bulk_like_sites"] 66 | ads_dict_orig = self["ads_dict_orig"] 67 | metal_site = self["metal_site"] 68 | applied_potential = self["applied_potential"] 69 | applied_pH = self["applied_pH"] 70 | vasp_cmd = self["vasp_cmd"] 71 | db_file = env_chk(self.get("db_file"), fw_spec) 72 | surface_pbx_uuid = self["surface_pbx_uuid"] 73 | 74 | # User-defined parameters ! 75 | # applied_potential = 1.60 # volts 76 | # applied_pH = 0 # pH conditions 77 | user_point = np.array([applied_pH, applied_potential]) 78 | 79 | parent_dict = fw_spec[f"{reduced_formula}_{miller_index}_surface_pbx"] 80 | surface_pbx_uuid = parent_dict["surface_pbx_uuid"] 81 | 82 | # Connect to DB 83 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 84 | 85 | # Get PBX collection from DB 86 | pbx_collection = mmdb.db[f"{reduced_formula}-{miller_index}_surface_pbx"] 87 | pbx_doc = pbx_collection.find_one({"surface_pbx_uuid": surface_pbx_uuid}) 88 | 89 | # Decide most stable termination at given (V, pH) 90 | clean_2_oh_list = pbx_doc["clean_2_OH"] # clean -> OH 91 | oh_2_ox_list = pbx_doc["OH_2_Ox"] # OH -> Ox 92 | 93 | surface_termination = self._get_surface_stable_termination( 94 | user_point, clean_2_oh_list, oh_2_ox_list 95 | ) 96 | 97 | # Retrieve the surface termination clean/OH/Ox geometries 98 | clean_surface = Slab.from_dict(pbx_doc["slab_clean"]) 99 | stable_surface = Slab.from_dict(pbx_doc[f"slab_{surface_termination}"]) 100 | 101 | # Retrieve the surface termination as input 102 | if surface_termination == "ox": 103 | stable_surface_orig = ads_dict_orig["O_1"] 104 | elif surface_termination == "oh": 105 | n_oh_rotation = pbx_doc["n_oh_rotation"] 106 | stable_surface_orig = ads_dict_orig[f"OH_{n_oh_rotation}"] 107 | else: 108 | stable_surface_orig = clean_surface 109 | 110 | # Generate OER single site intermediates (WNA) 111 | oer_wna = OER_SingleSite( 112 | stable_surface, 113 | slab_orig=stable_surface_orig, 114 | slab_clean=clean_surface, 115 | bulk_like_sites=bulk_like_sites, 116 | metal_site=metal_site, 117 | adsorbates=oer_adsorbates_dict, 118 | ) 119 | 120 | oer_intermediates_dict = oer_wna.generate_oer_intermediates() 121 | 122 | # Logger 123 | logger.info( 124 | f"{reduced_formula}-{miller_index} at (pH = {applied_pH}, V = {applied_potential} is: {surface_termination}" 125 | ) 126 | 127 | # OER_WF 128 | oer_wf = OERSingleSite_WF( 129 | oer_dict=oer_intermediates_dict, 130 | slab=clean_surface, 131 | metal_site=metal_site, 132 | slab_uuid=parent_dict["slab_uuid"], 133 | oriented_uuid=parent_dict["oriented_uuid"], 134 | surface_termination=surface_termination, 135 | vasp_cmd=vasp_cmd, 136 | db_file=db_file, 137 | surface_pbx_uuid=surface_pbx_uuid, 138 | ) 139 | 140 | return FWAction(detours=[oer_wf]) 141 | 142 | def _get_surface_stable_termination(self, user_point, clean_2_oh, oh_2_ox): 143 | """ 144 | Helper function to detect whether a point lies above or below the pbx lines. 145 | READ This: https://math.stackexchange.com/a/274728 146 | """ 147 | # Cross product 148 | is_above = ( 149 | lambda point, origin, end: np.cross(point - origin, end - origin) <= 0 150 | ) 151 | 152 | # Clean -> OH boundary 153 | clean_2_oh_origin = np.array([0, clean_2_oh[0]]) 154 | clean_2_oh_end = np.array([14, clean_2_oh[-1]]) 155 | 156 | above_clean = is_above(user_point, clean_2_oh_origin, clean_2_oh_end) 157 | 158 | # OH -> Ox boundary 159 | oh_2_ox_origin = np.array([0, oh_2_ox[0]]) 160 | oh_2_ox_end = np.array([14, oh_2_ox[-1]]) 161 | 162 | above_oh = is_above(user_point, oh_2_ox_origin, oh_2_ox_end) 163 | 164 | # decide 165 | if not above_clean: 166 | surface_termination = "clean" 167 | elif above_clean and not above_oh: 168 | surface_termination = "oh" 169 | elif above_clean and above_oh: 170 | surface_termination = "ox" 171 | 172 | return surface_termination 173 | -------------------------------------------------------------------------------- /WhereWulff/firetasks/slab_ads.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import uuid 9 | import numpy as np 10 | from pydash.objects import has, get 11 | 12 | from pymatgen.core.structure import Structure 13 | from pymatgen.core.surface import Slab 14 | 15 | from fireworks import FiretaskBase, FWAction, explicit_serialize 16 | 17 | from atomate.utils.utils import env_chk 18 | from atomate.vasp.database import VaspCalcDb 19 | from atomate.vasp.config import VASP_CMD, DB_FILE 20 | 21 | from WhereWulff.workflows.surface_pourbaix import SurfacePBX_WF 22 | 23 | 24 | @explicit_serialize 25 | class SlabAdsFireTask(FiretaskBase): 26 | """ 27 | Slab_Ads OptimizeFW. 28 | 29 | Args: 30 | bulk_structure : PMG structure object (already deserialized). 31 | reduced_formula : Slab reduced formula. 32 | slabs : PMG Slab object. 33 | adsorbates : List of adsorbates as PMG molecule objects 34 | metal_site : Active site for reactivity 35 | applied_potential: Applied potential for surface pourbaix diagram 36 | applied_pH : Applied pH for surface pourbaix diagram 37 | db_file : Directs for db.json file. 38 | vasp_cmd : VASP command. 39 | 40 | Returns: 41 | SLAB_ADS Firetasks. 42 | 43 | """ 44 | 45 | required_params = [ 46 | "bulk_structure", 47 | "reduced_formula", 48 | "slabs", 49 | "adsorbates", 50 | "vasp_cmd", 51 | "db_file", 52 | "metal_site", 53 | "applied_potential", 54 | "applied_pH", 55 | ] 56 | optional_params = ["_pass_job_info", "_add_launchpad_and_fw_id"] 57 | 58 | def run_task(self, fw_spec): 59 | 60 | # Variables 61 | bulk_structure = self["bulk_structure"] 62 | reduced_formula = self["reduced_formula"] 63 | slabs = self["slabs"] 64 | adsorbates = self["adsorbates"] 65 | vasp_cmd = self["vasp_cmd"] 66 | db_file = env_chk(self.get("db_file"), fw_spec) 67 | wulff_uuid = fw_spec.get("wulff_uuid", None) 68 | metal_site = self.get("metal_site", "") 69 | applied_potential = self.get("applied_potential", 1.60) 70 | applied_pH = self.get("applied_pH", 0.0) 71 | 72 | # Connect to DB 73 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 74 | 75 | # Slab_Ads 76 | if slabs is None: 77 | # Get wulff-shape collection from DB 78 | if wulff_uuid is not None: 79 | collection = mmdb.db[f"{reduced_formula}_wulff_shape_analysis"] 80 | wulff_metadata = collection.find_one( 81 | {"task_label": f"{reduced_formula}_wulff_shape_{wulff_uuid}"} 82 | ) 83 | 84 | # Filter by surface contribution 85 | filtered_slab_miller_indices = [ 86 | k for k, v in wulff_metadata["area_fractions"].items() if v > 0.0 87 | ] 88 | # Create the set of reduced_formulas 89 | bulk_slab_keys = [ 90 | "_".join([reduced_formula, miller_index]) 91 | for miller_index in filtered_slab_miller_indices 92 | ] 93 | else: 94 | # This is the case where there is no Wulff shape because 95 | # there is only one miller index 96 | # Get the bulk_slab_key from the fw_spec 97 | bulk_slab_keys = [k for k in fw_spec if f"{reduced_formula}" in k] 98 | filtered_slab_miller_indices = [ 99 | bsk.split("_")[-1] for bsk in bulk_slab_keys 100 | ] 101 | 102 | # Re-build PMG Slab object from optimized structures 103 | slab_candidates = [] 104 | for miller_index, bulk_slab_key in zip( 105 | filtered_slab_miller_indices, bulk_slab_keys 106 | ): 107 | # Retrieve the oriented_uuid and the slab_uuid for the surface orientation 108 | oriented_uuid = fw_spec.get(bulk_slab_key)["oriented_uuid"] 109 | slab_uuid = fw_spec.get(bulk_slab_key)["slab_uuid"] 110 | slab_wyckoffs = [ 111 | site["properties"]["bulk_wyckoff"] 112 | for site in mmdb.db["tasks"].find_one({"uuid": slab_uuid})["slab"][ 113 | "sites" 114 | ] 115 | ] 116 | slab_equivalents = [ 117 | site["properties"]["bulk_equivalent"] 118 | for site in mmdb.db["tasks"].find_one({"uuid": slab_uuid})["slab"][ 119 | "sites" 120 | ] 121 | ] 122 | slab_forces = mmdb.db["tasks"].find_one({"uuid": slab_uuid})["output"][ 123 | "forces" 124 | ] 125 | slab_struct = Structure.from_dict( 126 | mmdb.db["tasks"].find_one({"uuid": slab_uuid})["output"][ 127 | "structure" 128 | ] 129 | ) 130 | # Retrieve original structure from the root node via the uuid_lineage field 131 | # in the spec of the terminal node 132 | if ( 133 | not len( 134 | mmdb.db["fireworks"].find_one({"spec.uuid": slab_uuid})["spec"][ 135 | "uuid_lineage" 136 | ] 137 | ) 138 | < 1 139 | ): 140 | orig_slab_uuid = mmdb.db["fireworks"].find_one( 141 | {"spec.uuid": slab_uuid} 142 | )["spec"]["uuid_lineage"][0] 143 | 144 | else: 145 | orig_slab_uuid = slab_uuid 146 | 147 | # Original Structure 148 | slab_struct_orig = Structure.from_dict( 149 | mmdb.db["tasks"].find_one({"uuid": orig_slab_uuid})["input"][ 150 | "structure" 151 | ] 152 | ) 153 | # Initialize from original magmoms instead of output ones. 154 | orig_magmoms = mmdb.db["tasks"].find_one({"uuid": orig_slab_uuid})[ 155 | "orig_inputs" 156 | ]["incar"]["MAGMOM"] 157 | orig_site_properties = slab_struct.site_properties 158 | # Replace the magmoms with the initial values 159 | orig_site_properties["magmom"] = orig_magmoms 160 | slab_struct = slab_struct.copy(site_properties=orig_site_properties) 161 | slab_struct.add_site_property("bulk_wyckoff", slab_wyckoffs) 162 | slab_struct.add_site_property("bulk_equivalent", slab_equivalents) 163 | slab_struct.add_site_property("forces", slab_forces) 164 | # Original Structure site decoration 165 | slab_struct_orig = slab_struct_orig.copy( 166 | site_properties=orig_site_properties 167 | ) 168 | slab_struct_orig.add_site_property("bulk_wyckoff", slab_wyckoffs) 169 | slab_struct_orig.add_site_property("bulk_equivalent", slab_equivalents) 170 | 171 | # Oriented unit cell Structure output and input 172 | orient_struct = Structure.from_dict( 173 | mmdb.db["tasks"].find_one({"uuid": oriented_uuid})["output"][ 174 | "structure" 175 | ] 176 | ) 177 | oriented_struct_orig = Structure.from_dict( 178 | mmdb.db["tasks"].find_one({"uuid": oriented_uuid})["input"][ 179 | "structure" 180 | ] 181 | ) 182 | 183 | # Oriented unit cell site properties 184 | oriented_wyckoffs = [ 185 | site["properties"]["bulk_wyckoff"] 186 | for site in mmdb.db["tasks"].find_one({"uuid": slab_uuid})["slab"][ 187 | "oriented_unit_cell" 188 | ]["sites"] 189 | ] 190 | oriented_equivalents = [ 191 | site["properties"]["bulk_equivalent"] 192 | for site in mmdb.db["tasks"].find_one({"uuid": slab_uuid})["slab"][ 193 | "oriented_unit_cell" 194 | ]["sites"] 195 | ] 196 | 197 | # Decorate oriented unit cell with site properties 198 | orient_struct.add_site_property("bulk_wyckoff", oriented_wyckoffs) 199 | orient_struct.add_site_property("bulk_equivalent", oriented_equivalents) 200 | 201 | oriented_struct_orig.add_site_property( 202 | "bulk_wyckoff", oriented_wyckoffs 203 | ) 204 | oriented_struct_orig.add_site_property( 205 | "bulk_equivalent", oriented_equivalents 206 | ) 207 | 208 | # Optimized Slab object 209 | slab_candidates.append( 210 | ( 211 | # Output 212 | Slab( 213 | slab_struct.lattice, 214 | slab_struct.species, 215 | slab_struct.frac_coords, 216 | miller_index=list(map(int, miller_index)), 217 | oriented_unit_cell=orient_struct, 218 | shift=0, 219 | scale_factor=0, 220 | energy=mmdb.db["tasks"].find_one({"uuid": slab_uuid})[ 221 | "output" 222 | ]["energy"], 223 | site_properties=slab_struct.site_properties, 224 | ), 225 | # Input 226 | Slab( 227 | slab_struct_orig.lattice, 228 | slab_struct_orig.species, 229 | slab_struct_orig.frac_coords, 230 | miller_index=list(map(int, miller_index)), 231 | oriented_unit_cell=oriented_struct_orig, 232 | shift=0, 233 | scale_factor=0, 234 | energy=0, 235 | site_properties=slab_struct_orig.site_properties, 236 | ), 237 | oriented_uuid, 238 | slab_uuid, 239 | ) 240 | ) 241 | # Generate independent WF for OH/Ox terminations + Surface PBX 242 | hkl_pbx_wfs = [] 243 | for slab_out, slab_inp, oriented_uuid, slab_uuid in slab_candidates: 244 | hkl_pbx_wf = SurfacePBX_WF( 245 | bulk_structure=bulk_structure, 246 | slab=slab_out, 247 | slab_orig=slab_inp, 248 | slab_uuid=slab_uuid, 249 | oriented_uuid=oriented_uuid, 250 | adsorbates=adsorbates, 251 | vasp_cmd=vasp_cmd, 252 | db_file=db_file, 253 | metal_site=metal_site, 254 | applied_potential=applied_potential, 255 | applied_pH=applied_pH, 256 | ) 257 | hkl_pbx_wfs.append(hkl_pbx_wf) 258 | 259 | return FWAction(detours=hkl_pbx_wfs) 260 | -------------------------------------------------------------------------------- /WhereWulff/firetasks/static_bulk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import numpy as np 9 | from pydash.objects import has, get 10 | import uuid 11 | 12 | from pymatgen.core.structure import Structure 13 | 14 | from fireworks import FiretaskBase, FWAction, explicit_serialize 15 | from atomate.utils.utils import env_chk 16 | from atomate.vasp.database import VaspCalcDb 17 | from atomate.vasp.fireworks.core import StaticFW 18 | 19 | from WhereWulff.dft_settings.settings import MOSurfaceSet 20 | 21 | 22 | @explicit_serialize 23 | class StaticBulkFireTask(FiretaskBase): 24 | """ 25 | Equilibrium Bulk structure StaticFW- 26 | 27 | Args: 28 | reduced_formula (e.g RuO2) : structure composition as reduced formula. 29 | bulks : Equilibrium bulks from EOS_fitting. 30 | vasp_cmd : Environment variable for VASP. 31 | db_file : To connect to the DB. 32 | 33 | 34 | Returns: 35 | Static (Single-point) calculation of each equilibrium structure. 36 | """ 37 | 38 | required_params = ["reduced_formula", "bulks", "vasp_cmd", "db_file"] 39 | optional_params = [] 40 | 41 | def run_task(self, fw_spec): 42 | 43 | # Variables 44 | reduced_formula = self["reduced_formula"] 45 | bulks = self["bulks"] 46 | vasp_cmd = self["vasp_cmd"] 47 | db_file = env_chk(self.get("db_file"), fw_spec) 48 | 49 | # Connect to DB 50 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 51 | 52 | # StaticBulk 53 | if bulks is None: 54 | # Get equilibrium bulk from DB 55 | # Retrieve the uuids for the EOS FireTasks from the spec 56 | eos_uuids = [fw_spec[k] for k in fw_spec if "eos_uuid" in k] 57 | collection = mmdb.db[f"{reduced_formula}_eos"] 58 | bulk_metadata_docs = [ 59 | collection.find_one({"task_label": eos_uuid}) for eos_uuid in eos_uuids 60 | ] 61 | 62 | bulk_candidates = {"magnetic_order": [], "structure": [], "energy": []} 63 | for d in bulk_metadata_docs: 64 | magnetic_ordering = d["magnetic_ordering"] 65 | structure_eq = Structure.from_dict(d["structure_eq"]) 66 | energy_eq = d["energy_eq"] 67 | bulk_candidates["magnetic_order"].append(magnetic_ordering) 68 | bulk_candidates["structure"].append(structure_eq.as_dict()) 69 | bulk_candidates["energy"].append(energy_eq) 70 | # Generate a set of StaticFW additions that will calc. DFT energy 71 | bulk_static_fws = [] 72 | bulk_static_uuids = {} 73 | for magnetic_order, struct in zip( 74 | bulk_candidates["magnetic_order"], bulk_candidates["structure"] 75 | ): 76 | # Create unique uuid for each StaticFW 77 | static_bulk_uuid = uuid.uuid4() 78 | struct = Structure.from_dict(struct) 79 | vasp_input_set = MOSurfaceSet( 80 | struct, user_incar_settings={"NSW": 0}, bulk=True 81 | ) 82 | name = f"{struct.composition.reduced_formula}_{magnetic_order}_static_energy" 83 | bulk_static_fw = StaticFW( 84 | struct, 85 | name=name, 86 | vasp_input_set=vasp_input_set, 87 | vasp_cmd=vasp_cmd, 88 | db_file=db_file, 89 | ) 90 | bulk_static_fw.tasks[3]["additional_fields"].update( 91 | { 92 | "magnetic_ordering": magnetic_order, 93 | "static_bulk_uuid": static_bulk_uuid, 94 | } 95 | ) 96 | # Pass the static_bulk_uuid to the bulk_stability FW 97 | bulk_static_uuids[ 98 | f"static_bulk_uuid_{magnetic_order}" 99 | ] = static_bulk_uuid 100 | # bulk_static_fw.tasks[3].update( 101 | # { 102 | # "task_fields_to_push": { 103 | # f"static_bulk_uuid_{magnetic_order}": static_bulk_uuid 104 | # } 105 | # } 106 | # ) 107 | bulk_static_fws.append(bulk_static_fw) 108 | 109 | return FWAction( 110 | detours=bulk_static_fws, 111 | update_spec={"bulk_static_dict": bulk_static_uuids}, 112 | propagate=True, 113 | ) 114 | -------------------------------------------------------------------------------- /WhereWulff/firetasks/surface_energy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import json 9 | 10 | from pydash.objects import has, get 11 | 12 | from pymatgen.core import Structure 13 | from pymatgen.core.composition import Composition 14 | from pymatgen.core.surface import Slab 15 | 16 | from fireworks import FiretaskBase, FWAction, explicit_serialize 17 | from fireworks.utilities.fw_serializers import DATETIME_HANDLER 18 | 19 | from atomate.utils.utils import env_chk 20 | from atomate.utils.utils import get_logger 21 | from atomate.vasp.database import VaspCalcDb 22 | 23 | 24 | logger = get_logger(__name__) 25 | 26 | METAL_BULK_ENERGIES = { 27 | "Ti": -7.8335, 28 | "Cr": -9.6530, 29 | "Ru": -9.2744, 30 | "O": -7.48175514 - 0.27, 31 | "Ba": -1.9190, 32 | "Sr": -1.6831, 33 | "Co": -7.0922, 34 | } # ev/Atom from MP? 35 | 36 | 37 | @explicit_serialize 38 | class SurfaceEnergyFireTask(FiretaskBase): 39 | """ 40 | Computes the surface energy for stoichiometric slab models. 41 | 42 | Args: 43 | slab_formula: Reduced formula of the slab model e.g (RuO2) 44 | miller_index: Crystallographic orientations of the slab model. 45 | db_file: database file path 46 | to_db (default: True): Save the data on the db or in a json_file. 47 | 48 | return: 49 | summary_dict (DB/JSON) with surface energy information. 50 | """ 51 | 52 | required_params = ["slab_formula", "miller_index", "db_file"] 53 | optional_params = ["to_db"] 54 | 55 | def run_task(self, fw_spec): 56 | 57 | # Variables 58 | db_file = env_chk(self.get("db_file"), fw_spec) 59 | slab_formula = self["slab_formula"] 60 | miller_index = self["miller_index"] 61 | oriented_uuid = fw_spec.get("oriented_uuid") 62 | slab_uuid = fw_spec.get("slab_uuid") 63 | to_db = self.get("to_db", True) 64 | summary_dict = { 65 | "task_label": "{}_{}_surface_energy".format(slab_formula, miller_index), 66 | "slab_formula": slab_formula, 67 | "miller_index": miller_index, 68 | "oriented_uuid": oriented_uuid, 69 | "slab_uuid": slab_uuid, 70 | } 71 | 72 | # Collect and store tasks_ids 73 | all_task_ids = [] 74 | 75 | mmdb = VaspCalcDb.from_db_file(db_file, admin=True) 76 | 77 | oriented = mmdb.collection.find_one({"uuid": oriented_uuid}) 78 | slab = mmdb.collection.find_one({"uuid": slab_uuid}) 79 | 80 | all_task_ids.append(oriented["uuid"]) 81 | all_task_ids.append(slab["uuid"]) 82 | 83 | # Get Structures from DB 84 | oriented_struct = Structure.from_dict( 85 | oriented["calcs_reversed"][-1]["output"]["structure"] 86 | ) 87 | slab_struct = Structure.from_dict( 88 | slab["calcs_reversed"][-1]["output"]["structure"] 89 | ) 90 | 91 | # Get DFT Energies from DB 92 | oriented_E = oriented["calcs_reversed"][-1]["output"]["energy"] 93 | slab_E = slab["calcs_reversed"][-1]["output"]["energy"] 94 | 95 | # Build Slab Object 96 | slab_obj = Slab( 97 | slab_struct.lattice, 98 | slab_struct.species, 99 | slab_struct.frac_coords, 100 | miller_index=list(map(int, miller_index)), 101 | oriented_unit_cell=oriented_struct, 102 | shift=0, 103 | scale_factor=0, 104 | energy=slab_E, 105 | ) 106 | 107 | slab_Area = slab_obj.surface_area 108 | 109 | # Formulas 110 | self.oriented_formula = oriented_struct.composition.reduced_formula 111 | self.slab_formula = slab_struct.composition.reduced_formula 112 | 113 | # Compositions 114 | bulk_comp = oriented_struct.composition.as_dict() 115 | slab_comp = slab_struct.composition.as_dict() 116 | 117 | bulk_unit_form_dict = Composition( 118 | {el: bulk_comp[el] for el in bulk_comp.keys() if el != "O"} 119 | ).as_dict() 120 | slab_unit_form_dict = Composition( 121 | {el: slab_comp[el] for el in bulk_comp.keys() if el != "O"} 122 | ).as_dict() 123 | 124 | bulk_unit_form = sum(bulk_unit_form_dict.values()) 125 | slab_unit_form = sum(slab_unit_form_dict.values()) 126 | slab_bulk_ratio = slab_unit_form / bulk_unit_form 127 | self.oriented_struct = ( 128 | oriented_struct # make the struct_obj accessible to the non-stoich method 129 | ) 130 | self.slab_struct = ( 131 | slab_struct # make the struct_obj accessible to the non-stoich method 132 | ) 133 | 134 | # Calc. surface energy - Assumes symmetric 135 | if ( 136 | not slab_obj.is_polar() 137 | # and slab_obj.is_symmetric() 138 | ): 139 | surface_energy = self.get_non_stoich_surface_energy( 140 | slab_E, oriented_E, slab_Area 141 | ) 142 | 143 | # Summary dict 144 | summary_dict["oriented_struct"] = oriented_struct.as_dict() 145 | summary_dict["slab_struct"] = slab_struct.as_dict() 146 | summary_dict["oriented_E"] = oriented_E 147 | summary_dict["slab_E"] = slab_E 148 | summary_dict["slab_Area"] = slab_Area 149 | summary_dict["is_polar"] = str(slab_obj.is_polar()) 150 | summary_dict["is_symmetric"] = str(slab_obj.is_symmetric()) 151 | if self.slab_formula == self.oriented_formula: 152 | summary_dict["is_stoichiometric"] = str(True) 153 | else: 154 | summary_dict["is_stoichiometric"] = str(False) 155 | 156 | summary_dict["N"] = slab_bulk_ratio 157 | summary_dict["surface_energy"] = surface_energy 158 | 159 | # Add results to db 160 | if to_db: 161 | mmdb.collection = mmdb.db["surface_energies"] 162 | mmdb.collection.insert_one(summary_dict) 163 | 164 | else: 165 | with open( 166 | "{}_{}_surface_energy.json".format(self.slab_formula, miller_index), "w" 167 | ) as f: 168 | f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) 169 | 170 | # Logger 171 | logger.info( 172 | "{}_{} Surface Energy: {} [eV/A**2]".format( 173 | self.slab_formula, miller_index, surface_energy 174 | ) 175 | ) 176 | 177 | # Send the summary_dict to the child FW 178 | return FWAction( 179 | update_spec={ 180 | f"{self.oriented_formula}_{miller_index}": { 181 | "oriented_uuid": oriented_uuid, 182 | "slab_uuid": slab_uuid, 183 | } 184 | }, 185 | propagate=True, 186 | ) 187 | 188 | def get_surface_energy(self, slab_E, oriented_E, slab_bulk_ratio, slab_Area): 189 | """ 190 | Surface energy for non-dipolar, symmetric and stoichiometric 191 | Units: eV/A**2 192 | 193 | Args: 194 | slab_E: DFT energy from slab optimization [eV] 195 | oriented_E: DFT energy from oriented bulk optimization [eV] 196 | slab_bulk_ratio: slab units formula per bulk units formula 197 | slab_area: Area from the slab model XY plane [A**2] 198 | Return: 199 | gamma_hkl - Surface energy for symmetric and stoichiometric model. 200 | """ 201 | gamma_hkl = (slab_E - (slab_bulk_ratio * oriented_E)) / ( 202 | 2 * slab_Area 203 | ) # scaling for bulk! 204 | return gamma_hkl 205 | 206 | def get_non_stoich_surface_energy(self, slab_E, oriented_E, slab_Area): 207 | """ 208 | Surface energy that relaxes the non-stoichiometric assumption. Assumes that the 209 | deltamu(T,p) for bringing the specie from 0K to standard temperature is negligible 210 | for solids, using only the bulk energies of the metals to correct for the excess 211 | or deficiency. We pick the oxygen as the reference and correct for the metals. 212 | FIXME: Need to make this more general for intermetallics, which will not have oxygen 213 | Args: 214 | slab_E: DFT energy from slab optimization [eV] 215 | oriented_E: DFT energy from oriented bulk optimization [eV] 216 | slab_Area: Area from the slab model XY plae [A**2], still assumes symmetric 217 | Return: 218 | gamma_hkl: Surface Energy for symmetric and non-stoichiometric model 219 | """ 220 | # FIXME: Need to cycle through the potential references to see which one 221 | # yields excess_deficiency_factors that are integers 222 | # reference = "O" 223 | bulk_num_atoms_dict = self.oriented_struct.composition.get_el_amt_dict() 224 | slab_num_atoms_dict = self.slab_struct.composition.get_el_amt_dict() 225 | for reference in bulk_num_atoms_dict: 226 | bulk_mole_fractions_dict = { 227 | k: bulk_num_atoms_dict[k] / self.oriented_struct.composition.num_atoms 228 | for k in bulk_num_atoms_dict 229 | } 230 | slab_bulk_ratio = slab_num_atoms_dict[reference] / ( 231 | bulk_mole_fractions_dict[reference] 232 | * self.oriented_struct.composition.num_atoms 233 | ) 234 | excess_deficiency_factors_dict = { 235 | k: round( 236 | ( 237 | ( 238 | ( 239 | bulk_mole_fractions_dict[k] 240 | * slab_num_atoms_dict[reference] 241 | ) 242 | / bulk_mole_fractions_dict[reference] 243 | ) 244 | - slab_num_atoms_dict[k] 245 | ), 246 | 2, 247 | ) 248 | for k in slab_num_atoms_dict 249 | } 250 | # Check if the excess_deficiency factors are integers - if yes pass else continue until you find one 251 | if all( 252 | [v - int(v) == 0 for k, v in excess_deficiency_factors_dict.items()] 253 | ): 254 | break 255 | else: 256 | continue 257 | 258 | corrections_dict = { 259 | k: METAL_BULK_ENERGIES[k] * excess_deficiency_factors_dict[k] 260 | for k in excess_deficiency_factors_dict 261 | if k != reference 262 | } 263 | 264 | surface_energy = ( 265 | slab_E 266 | - (slab_bulk_ratio * oriented_E) 267 | + sum(list(corrections_dict.values())) 268 | ) / (2 * slab_Area) 269 | return surface_energy 270 | -------------------------------------------------------------------------------- /WhereWulff/fireworks/oer_single_site.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from pymatgen.core import surface 9 | from fireworks import Firework 10 | 11 | from atomate.vasp.fireworks.core import OptimizeFW 12 | from atomate.vasp.config import VASP_CMD, DB_FILE 13 | from atomate.utils.utils import get_meta_from_structure 14 | 15 | from WhereWulff.analysis.oer import OER_SingleSiteAnalyzer 16 | 17 | 18 | def OER_SingleSiteAnalyzer_FW( 19 | reduced_formula="", 20 | name="", 21 | miller_index="", 22 | metal_site="", 23 | slab_uuid="", 24 | ads_slab_uuids="", 25 | surface_termination="", 26 | parents=None, 27 | db_file=DB_FILE, 28 | surface_pbx_uuid="", 29 | ): 30 | """ 31 | Converts the OER_SingleSiteAnalyzer FireTask to FireWorks. 32 | """ 33 | 34 | # FW 35 | fw = Firework( 36 | OER_SingleSiteAnalyzer( 37 | reduced_formula=reduced_formula, 38 | miller_index=miller_index, 39 | metal_site=metal_site, 40 | slab_uuid=slab_uuid, 41 | ads_slab_uuids=ads_slab_uuids, 42 | surface_termination=surface_termination, 43 | db_file=db_file, 44 | to_db=True, 45 | surface_pbx_uuid=surface_pbx_uuid, 46 | ), 47 | name=name, 48 | parents=parents, 49 | ) 50 | 51 | return fw 52 | -------------------------------------------------------------------------------- /WhereWulff/fireworks/optimize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from typing import Counter 9 | from atomate.vasp.fireworks.core import OptimizeFW 10 | from atomate.vasp.config import VASP_CMD, DB_FILE 11 | from atomate.utils.utils import get_meta_from_structure 12 | 13 | from WhereWulff.dft_settings.settings import MOSurfaceSet 14 | from WhereWulff.firetasks.handlers import ContinueOptimizeFW 15 | 16 | 17 | def Bulk_FW( 18 | bulk, 19 | name="", 20 | vasp_input_set=None, 21 | parents=None, 22 | wall_time=172800, 23 | vasp_cmd=VASP_CMD, 24 | db_file=DB_FILE, 25 | ): 26 | """ 27 | Function to generate a bulk firework. Returns an OptimizeFW for the specified slab. 28 | 29 | Args: 30 | bulk (Struct Object) : Structure corresponding to the slab to be calculated. 31 | name (string) : name of firework 32 | parents (default: None) : parent FWs 33 | add_slab_metadata (default: True) : Whether to add slab metadata to task doc. 34 | wall_time (default: 172800) : 2 days in seconds 35 | vasp_cmd : vasp_comand 36 | db_file : Path to the dabase file 37 | 38 | Returns: 39 | Firework correspoding to bulk calculation. 40 | """ 41 | import uuid 42 | 43 | # Generate a unique ID for Bulk_FW 44 | fw_bulk_uuid = uuid.uuid4() 45 | 46 | # DFT Method 47 | if not vasp_input_set: 48 | vasp_input_set = MOSurfaceSet(bulk, bulk=True) 49 | 50 | # FW 51 | fw = OptimizeFW( 52 | name=name, 53 | structure=bulk, 54 | max_force_threshold=None, 55 | vasp_input_set=vasp_input_set, 56 | vasp_cmd=vasp_cmd, 57 | db_file=db_file, 58 | parents=parents, 59 | job_type="normal", 60 | spec={ 61 | "counter": 0, 62 | "_add_launchpad_and_fw_id": True, 63 | "_pass_job_info": True, 64 | "uuid_lineage": [], 65 | "uuid": fw_bulk_uuid, 66 | "wall_time": wall_time, 67 | "max_tries": 5, 68 | "name": name, 69 | "is_bulk": True, 70 | }, 71 | ) 72 | # Switch-off GzipDir for WAVECAR transferring 73 | fw.tasks[1].update({"gzip_output": False}) 74 | 75 | # Append Continue-optimizeFW for wall-time handling 76 | fw.tasks.append( 77 | ContinueOptimizeFW(is_bulk=True, counter=0, db_file=db_file, vasp_cmd=vasp_cmd) 78 | ) 79 | 80 | # Add bulk_uuid through VaspToDb 81 | fw.tasks[3]["additional_fields"].update({"uuid": fw_bulk_uuid}) 82 | fw.tasks[3].update({"defuse_unsuccessful": False}) 83 | 84 | # Switch-on WalltimeHandler in RunVaspCustodian 85 | if wall_time is not None: 86 | fw.tasks[1].update({"wall_time": wall_time}) 87 | 88 | return fw 89 | 90 | 91 | def Slab_FW( 92 | slab, 93 | name="", 94 | parents=None, 95 | vasp_input_set=None, 96 | add_slab_metadata=True, 97 | wall_time=172800, 98 | vasp_cmd=VASP_CMD, 99 | db_file=DB_FILE, 100 | ): 101 | """ 102 | Function to generate a slab firework. Returns an OptimizeFW for the specified slab. 103 | 104 | Args: 105 | slab (Slab Object) : Slab corresponding to the slab to be calculated. 106 | name (string) : name of firework 107 | parents (default: None) : parent FWs 108 | add_slab_metadata (default: True) : Whether to add slab metadata to task doc. 109 | wall_time (default: 172800) : 2 days in seconds 110 | vasp_cmd : vasp_comand 111 | db_file : Path to the dabase file 112 | 113 | Returns: 114 | Firework correspoding to slab calculation. 115 | """ 116 | import uuid 117 | 118 | # Generate a unique ID for Slab_FW 119 | fw_slab_uuid = uuid.uuid4() 120 | 121 | # DFT Method 122 | if not vasp_input_set: 123 | vasp_input_set = MOSurfaceSet(slab, bulk=False) 124 | 125 | # FW 126 | fw = OptimizeFW( 127 | name=name, 128 | structure=slab, 129 | max_force_threshold=None, 130 | vasp_input_set=vasp_input_set, 131 | vasp_cmd=vasp_cmd, 132 | db_file=db_file, 133 | parents=parents, 134 | job_type="normal", 135 | spec={ 136 | "counter": 0, 137 | "_add_launchpad_and_fw_id": True, 138 | "_pass_job_info": True, 139 | "uuid_lineage": [], 140 | "uuid": fw_slab_uuid, 141 | "wall_time": wall_time, 142 | "max_tries": 5, 143 | "name": name, 144 | "is_bulk": False, 145 | }, 146 | ) 147 | # Switch-off GzipDir for WAVECAR transferring 148 | fw.tasks[1].update({"gzip_output": False}) 149 | 150 | # Append Continue-optimizeFW for wall-time handling 151 | fw.tasks.append( 152 | ContinueOptimizeFW(is_bulk=False, counter=0, db_file=db_file, vasp_cmd=vasp_cmd) 153 | ) 154 | 155 | # Add slab_uuid through VaspToDb 156 | fw.tasks[3]["additional_fields"].update({"uuid": fw_slab_uuid}) 157 | fw.tasks[3].update({"defuse_unsuccesful": False}) 158 | 159 | # Switch-on WalltimeHandler in RunVaspCustodian 160 | if wall_time is not None: 161 | fw.tasks[1].update({"wall_time": wall_time}) 162 | 163 | # Add slab metadata 164 | if add_slab_metadata: 165 | parent_structure_metadata = get_meta_from_structure(slab.oriented_unit_cell) 166 | fw.tasks[3]["additional_fields"].update( 167 | { 168 | "slab": slab, 169 | "parent_structure": slab.oriented_unit_cell, 170 | "parent_structure_metadata": parent_structure_metadata, 171 | } 172 | ) 173 | 174 | return fw 175 | 176 | 177 | def AdsSlab_FW( 178 | slab, 179 | name="", 180 | oriented_uuid="", 181 | slab_uuid="", 182 | ads_slab_uuid="", 183 | is_adslab=True, 184 | parents=None, 185 | vasp_input_set=None, 186 | add_slab_metadata=True, 187 | wall_time=172800, 188 | vasp_cmd=VASP_CMD, 189 | db_file=DB_FILE, 190 | ): 191 | """ 192 | Function to generate a ads_slab firework. Returns an OptimizeFW for the specified slab. 193 | 194 | Args: 195 | slab (Slab Object) : Slab corresponding to the slab to be calculated. 196 | name (string) : name of firework 197 | parents (default: None) : parent FWs 198 | add_slab_metadata (default: True) : Whether to add slab metadata to task doc. 199 | wall_time (default: 172800) : 2 days in seconds 200 | vasp_cmd : vasp_comand 201 | db_file : Path to the dabase file 202 | 203 | Returns: 204 | Firework correspoding to slab calculation. 205 | """ 206 | 207 | # DFT Method 208 | if not vasp_input_set: 209 | vasp_input_set = MOSurfaceSet(slab, bulk=False) 210 | 211 | # FW 212 | fw = OptimizeFW( 213 | name=name, 214 | structure=slab, 215 | max_force_threshold=None, 216 | vasp_input_set=vasp_input_set, 217 | vasp_cmd=vasp_cmd, 218 | db_file=db_file, 219 | parents=parents, 220 | job_type="normal", 221 | spec={ 222 | "counter": 0, 223 | "_add_launchpad_and_fw_id": True, 224 | "_pass_job_info": True, 225 | "uuid_lineage": [], 226 | "uuid": ads_slab_uuid, 227 | "wall_time": wall_time, 228 | "name": name, 229 | "max_tries": 5, 230 | "is_adslab": is_adslab, 231 | "oriented_uuid": oriented_uuid, # adslab FW should get terminal node ids 232 | "slab_uuid": slab_uuid, 233 | "is_bulk": False, 234 | }, 235 | ) 236 | # Switch-off GzipDir for WAVECAR transferring 237 | fw.tasks[1].update({"gzip_output": False}) 238 | 239 | # Append Continue-optimizeFW for wall-time handling 240 | fw.tasks.append( 241 | ContinueOptimizeFW(is_bulk=False, counter=0, db_file=db_file, vasp_cmd=vasp_cmd) 242 | ) 243 | 244 | # Add slab_uuid through VaspToDb 245 | fw.tasks[3]["additional_fields"].update({"uuid": ads_slab_uuid}) 246 | fw.tasks[3].update({"defuse_unsuccesful": False}) 247 | 248 | # Switch-on WalltimeHandler in RunVaspCustodian 249 | if wall_time is not None: 250 | fw.tasks[1].update({"wall_time": wall_time}) 251 | 252 | # Add slab metadata 253 | if add_slab_metadata: 254 | parent_structure_metadata = get_meta_from_structure(slab.oriented_unit_cell) 255 | fw.tasks[3]["additional_fields"].update( 256 | { 257 | "slab": slab, 258 | "parent_structure": slab.oriented_unit_cell, 259 | "parent_structure_metadata": parent_structure_metadata, 260 | } 261 | ) 262 | 263 | return fw 264 | -------------------------------------------------------------------------------- /WhereWulff/fireworks/surface_pourbaix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from fireworks import Firework 9 | 10 | from atomate.vasp.fireworks.core import OptimizeFW 11 | from atomate.vasp.config import VASP_CMD, DB_FILE 12 | from atomate.utils.utils import get_meta_from_structure 13 | 14 | from WhereWulff.analysis.surface_pourbaix import SurfacePourbaixDiagramAnalyzer 15 | 16 | 17 | def SurfacePBX_FW( 18 | reduced_formula="", 19 | name="", 20 | miller_index="", 21 | slab_uuid="", 22 | oriented_uuid="", 23 | ads_slab_uuids="", 24 | parents=None, 25 | db_file=DB_FILE, 26 | surface_pbx_uuid="", 27 | ): 28 | 29 | # FW 30 | fw = Firework( 31 | SurfacePourbaixDiagramAnalyzer( 32 | reduced_formula=reduced_formula, 33 | miller_index=miller_index, 34 | slab_uuid=slab_uuid, 35 | oriented_uuid=oriented_uuid, 36 | ads_slab_uuids=ads_slab_uuids, 37 | db_file=db_file, 38 | to_db=True, 39 | surface_pbx_uuid=surface_pbx_uuid, 40 | ), 41 | name=name, 42 | parents=parents, 43 | ) 44 | 45 | return fw 46 | -------------------------------------------------------------------------------- /WhereWulff/launchers/bulkflows.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | import numpy as np 11 | from pymatgen.analysis.magnetism.analyzer import ( 12 | CollinearMagneticStructureAnalyzer, 13 | MagneticStructureEnumerator, 14 | Ordering, 15 | ) 16 | 17 | from pymatgen.io.cif import CifParser 18 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer 19 | 20 | from pymatgen.core.structure import Structure 21 | from pymatgen.core.periodic_table import Element 22 | 23 | from pymatgen.transformations.standard_transformations import ( 24 | AutoOxiStateDecorationTransformation, 25 | ) 26 | 27 | from fireworks import LaunchPad 28 | from atomate.vasp.config import VASP_CMD, DB_FILE 29 | 30 | from WhereWulff.dft_settings.settings import ( 31 | set_bulk_magmoms, 32 | ) 33 | from WhereWulff.workflows.eos import BulkOptimize_WF, EOS_WF 34 | from WhereWulff.workflows.static_bulk import StaticBulk_WF 35 | from WhereWulff.workflows.bulk_stability import StabilityBulk_WF 36 | 37 | import warnings 38 | warnings.filterwarnings("ignore") 39 | 40 | 41 | # Bulk structure workflow method 42 | class BulkFlows: 43 | """ 44 | BulkFlow is a general method to automatize DFT workflows to find the Equilibrium Bulk 45 | Structure with the right magnetic moments and Ordering. 46 | 47 | Args: 48 | bulk_structure : CIF file path. 49 | n_deformations (default: 21) : Number of volume deformations for the EOS fitting. 50 | nm_magmom_buffer (default: 0.6) : VASP needs a MAGMOM buffer even if should be 0. 51 | conventional_standard (default: True) : To select if bulk structure should be conventional standard. 52 | vasp_cmd : VASP execution command (configured in my_fworkers.py file). 53 | db_file : Directs to db.json file for mongodb database configuration. 54 | 55 | Returns: 56 | Submits the BulkFlow workflow to the launchpad and ready for execution! 57 | """ 58 | 59 | def __init__( 60 | self, 61 | bulk_structure, 62 | n_deformations=21, 63 | nm_magmom_buffer=0.6, 64 | conventional_standard=True, 65 | vasp_cmd=VASP_CMD, 66 | db_file=DB_FILE, 67 | ): 68 | 69 | # Bulk structure 70 | self.nm_magmom_buffer = nm_magmom_buffer 71 | self.bulk_structure = self._read_cif_file(bulk_structure) 72 | self.original_bulk_structure = self.bulk_structure.copy() 73 | self.n_deformations = n_deformations 74 | # Convetional standard unit cell 75 | if conventional_standard: 76 | self.bulk_structure = self._get_conventional_standard() 77 | # Decorate with oxidations states 78 | self.bulk_structure = self._get_oxidation_states() 79 | # Decorate the bulk structure with sites properties 80 | self.bulk_structure = self._get_wyckoffs_positions() 81 | # Get magmoms for metals 82 | self.magmoms_dict = self._get_metals_magmoms() 83 | # Get magnetic orderings 84 | self.magnetic_orderings_dict = self._get_magnetic_orderings() 85 | # Get bulk structures dict for NM, AFM, FM 86 | self.bulk_structures_dict = self._get_all_bulk_magnetic_configurations() 87 | 88 | # VASP_CMD and DB_FILE 89 | self.vasp_cmd = vasp_cmd 90 | self.db_file = db_file 91 | 92 | # Workflow 93 | # self.workflows_list = self._get_all_wfs() 94 | # self.workflows_list = self._get_opt_wf() 95 | 96 | def _read_cif_file(self, bulk_structure, primitive=False): 97 | """Parse CIF file with PMG""" 98 | struct = CifParser(bulk_structure).get_structures(primitive=primitive)[0] 99 | return struct 100 | 101 | def _get_oxidation_states(self): 102 | """Decorates bulk with oxidation states""" 103 | oxid_transformer = AutoOxiStateDecorationTransformation() 104 | struct_new = oxid_transformer.apply_transformation(self.bulk_structure) 105 | return struct_new 106 | 107 | def _get_conventional_standard(self): 108 | """Convert Bulk structure to conventional standard""" 109 | SGA = SpacegroupAnalyzer(self.bulk_structure) 110 | bulk_structure = SGA.get_conventional_standard_structure() 111 | return bulk_structure 112 | 113 | def _get_wyckoffs_positions(self): 114 | """Decorates the bulk structure with wyckoff positions""" 115 | bulk_structure = self.bulk_structure.copy() 116 | SGA = SpacegroupAnalyzer(bulk_structure) 117 | bulk_structure.add_site_property( 118 | "bulk_wyckoff", SGA.get_symmetry_dataset()["wyckoffs"] 119 | ) 120 | bulk_structure.add_site_property( 121 | "bulk_equivalent", SGA.get_symmetry_dataset()["equivalent_atoms"].tolist() 122 | ) 123 | return bulk_structure 124 | 125 | def _get_metals_magmoms(self): 126 | """Returns dict with metal symbol and magmoms assigned""" 127 | bulk_structure = set_bulk_magmoms(self.bulk_structure, self.nm_magmom_buffer) 128 | metals_symb = [ 129 | site.species_string 130 | for site in self.bulk_structure 131 | if site.specie.element.is_metal 132 | ] 133 | magmoms_list = bulk_structure.site_properties["magmom"] 134 | magmoms_dict = {} 135 | for metal, magmom in zip( 136 | metals_symb, magmoms_list 137 | ): # Note this results in a sublist of tuples and is OK provided the orders for both align. 138 | magmoms_dict.update({str(metal): magmom}) 139 | return magmoms_dict 140 | 141 | def _get_magnetic_orderings(self): 142 | """Returns a dict with AFM and FM magnetic structures orderings""" 143 | magnetic_orderings_dict = {} 144 | try: 145 | enumerator = MagneticStructureEnumerator( 146 | self.bulk_structure, 147 | default_magmoms=self.magmoms_dict, 148 | automatic=True, 149 | truncate_by_symmetry=True, 150 | ) 151 | except ValueError as e: 152 | # This is probably happening because the magmoms are zero on the 153 | # metal atoms. Need to catch and give a small buffer to the 154 | # TMO ions. Note that in this scenario, there is no need for 155 | # magnetic configurations. 156 | buffer_magmom = list( 157 | np.zeros(self.original_bulk_structure.num_sites, dtype=float) 158 | + self.nm_magmom_buffer 159 | ) 160 | magnetic_orderings_dict.update({"NM": buffer_magmom}) 161 | print(f"Encountered issue: {e}. Will give slight buffer to metal ions") 162 | return magnetic_orderings_dict 163 | 164 | ordered_structures = enumerator.ordered_structures 165 | 166 | for ord_struct in ordered_structures: 167 | analyzer = CollinearMagneticStructureAnalyzer(ord_struct) 168 | struct_with_spin = analyzer.get_structure_with_spin() 169 | struct_with_spin.sort() # Sort so that it is aligned with original_bulk 170 | if analyzer.ordering == Ordering.AFM: 171 | if struct_with_spin.num_sites == self.original_bulk_structure.num_sites: 172 | afm_magmom = [ 173 | float(site.specie.spin) 174 | if ( 175 | site.specie.element.is_metal 176 | and abs(float(site.specie.spin)) > 0.01 177 | ) 178 | else self.nm_magmom_buffer 179 | for site in struct_with_spin 180 | ] 181 | magnetic_orderings_dict.update({"AFM": afm_magmom}) 182 | elif analyzer.ordering == Ordering.FM: 183 | if struct_with_spin.num_sites == self.original_bulk_structure.num_sites: 184 | fm_magmom = [ 185 | float(site.specie.spin) 186 | if ( 187 | site.specie.element.is_metal 188 | and abs(float(site.specie.spin)) > 0.01 189 | ) 190 | else self.nm_magmom_buffer 191 | for site in struct_with_spin 192 | ] 193 | magnetic_orderings_dict.update({"FM": fm_magmom}) 194 | 195 | # Adding non-magnetic 196 | nm_magmom = list( 197 | np.zeros(self.original_bulk_structure.num_sites, dtype=float) 198 | + self.nm_magmom_buffer 199 | ) 200 | magnetic_orderings_dict.update({"NM": nm_magmom}) 201 | return magnetic_orderings_dict 202 | 203 | def _get_all_bulk_magnetic_configurations(self): 204 | """Decorates the original bulk structure with NM, AFM and FM""" 205 | bulk_structure = self.original_bulk_structure.copy() 206 | magnetic_orderings = self.magnetic_orderings_dict 207 | # Add AFM, FM and NM 208 | bulk_structures_dict = {} 209 | for k, v in magnetic_orderings.items(): 210 | bulk_new = bulk_structure.copy() 211 | bulk_new.add_site_property("magmom", v) 212 | bulk_structures_dict.update({k: bulk_new.as_dict()}) 213 | return bulk_structures_dict 214 | 215 | def _get_opt_wf(self): 216 | """Returns bulk optimization workflow to be launched""" 217 | bulk_structure = Structure.from_dict(self.bulk_structures_dict["NM"]) 218 | opt_wf, parents_fws = BulkOptimize_WF( 219 | bulk_structure, vasp_cmd=self.vasp_cmd, db_file=self.db_file 220 | ) 221 | return opt_wf, parents_fws 222 | 223 | def _get_eos_wfs(self, parents=None): 224 | """Returns the list of workflows to be launched""" 225 | # wfs for NM + AFM + FM 226 | wfs = [] 227 | for mag_ord, bulk_struct in self.bulk_structures_dict.items(): 228 | bulk_struct = Structure.from_dict(bulk_struct) 229 | eos_wf = EOS_WF( 230 | bulk_struct, 231 | n_deformations=self.n_deformations, 232 | magnetic_ordering=mag_ord, 233 | parents=parents, 234 | vasp_cmd=self.vasp_cmd, 235 | db_file=self.db_file, 236 | ) 237 | wfs.append(eos_wf) 238 | return wfs 239 | 240 | def _get_all_wfs(self): 241 | """Once again""" 242 | eos_wf, fws_all = EOS_WF( 243 | self.bulk_structures_dict, 244 | n_deformations=self.n_deformations, 245 | vasp_cmd=self.vasp_cmd, 246 | db_file=self.db_file, 247 | ) 248 | return eos_wf, fws_all 249 | 250 | def _get_all_wfs_old(self): 251 | """Lets see""" 252 | wfs = [] 253 | # Optimize 254 | bulk_structure = Structure.from_dict(self.bulk_structures_dict["NM"]) 255 | opt_wf, opt_parents = BulkOptimize_WF( 256 | bulk_structure, vasp_cmd=self.vasp_cmd, db_file=self.db_file 257 | ) 258 | # breakpoint() 259 | wfs.append(opt_wf) 260 | # OES + Fitting 261 | for mag_ord, bulk_struct in self.bulk_structures_dict.items(): 262 | bulk_struct = Structure.from_dict(bulk_struct) 263 | eos_wf = EOS_WF( 264 | bulk_struct, 265 | magnetic_ordering=mag_ord, 266 | parents=opt_parents, 267 | vasp_cmd=self.vasp_cmd, 268 | db_file=self.db_file, 269 | ) 270 | wfs.append(eos_wf) 271 | return wfs 272 | 273 | def _get_bulk_static_wfs(self, parents=None): 274 | """Returns all the BulkStatic FW""" 275 | bulk_static_wfs, parents_fws = StaticBulk_WF( 276 | self.bulk_structure, 277 | parents=parents, 278 | vasp_cmd=self.vasp_cmd, 279 | db_file=self.db_file, 280 | ) 281 | return bulk_static_wfs, parents_fws 282 | 283 | def _get_stability_wfs(self, parents=None): 284 | """Returns all the BulkStability FW""" 285 | bulk_stability = StabilityBulk_WF( 286 | self.bulk_structure, parents=parents, db_file=self.db_file 287 | ) 288 | return bulk_stability 289 | 290 | def _get_parents(self, workflow_list): 291 | """Returns an unpacked list of parents from a set of wfs""" 292 | wf_fws = [wf.fws for wf in workflow_list] 293 | fws = [fw for wf in wf_fws for fw in wf] 294 | return fws 295 | 296 | def submit(self, hostname, db_name, port, username, password, reset=False): 297 | """Submit Full Workflow to Launchpad!""" 298 | launchpad = ( 299 | LaunchPad( 300 | host=hostname, 301 | name=db_name, 302 | port=port, 303 | username=username, 304 | password=password, 305 | ) 306 | if hostname 307 | else LaunchPad() 308 | ) 309 | if reset: 310 | launchpad.reset("", require_password=False) 311 | 312 | # Optimization + Deformations + EOS_FIT 313 | _, eos_parents = self._get_all_wfs() 314 | # parents_list = self._get_parents(self.workflows_list) 315 | 316 | # Static_FW + StabilityAnalysis 317 | _, static_list = self._get_bulk_static_wfs(parents=eos_parents) 318 | 319 | # StabilityAnalis 320 | bulk_stability = self._get_stability_wfs(parents=static_list) 321 | launchpad.add_wf(bulk_stability) 322 | 323 | return launchpad 324 | 325 | def submit_local(self, reset=True): 326 | """Submit Full Workflow to Launchpad!""" 327 | launchpad = LaunchPad() 328 | 329 | if reset: 330 | launchpad.reset("", require_password=False) 331 | 332 | # Optimization + Deformation + EOS_FIT 333 | parents_list = self._get_parents(self.workflows_list) 334 | 335 | # Static_FW + StabilityAnalysis 336 | bulk_static = self._get_bulk_static_wfs(parents=parents_list) 337 | launchpad.add_wf(bulk_static) 338 | 339 | return launchpad 340 | -------------------------------------------------------------------------------- /WhereWulff/launchers/slabflows.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | import numpy as np 11 | 12 | from pymatgen.io.cif import CifParser 13 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer 14 | from pymatgen.core.surface import ( 15 | SlabGenerator, 16 | get_symmetrically_distinct_miller_indices, 17 | ) 18 | 19 | from pymatgen.analysis.local_env import VoronoiNN 20 | 21 | from pymatgen.transformations.standard_transformations import ( 22 | AutoOxiStateDecorationTransformation, 23 | ) 24 | 25 | from fireworks import LaunchPad, Workflow 26 | from atomate.vasp.config import VASP_CMD, DB_FILE 27 | 28 | from WhereWulff.dft_settings.settings import ( 29 | set_bulk_magmoms, 30 | SelectiveDynamics, 31 | ) 32 | 33 | from WhereWulff.workflows.surface_energy import SurfaceEnergy_WF 34 | from WhereWulff.workflows.wulff_shape import WulffShape_WF 35 | from WhereWulff.workflows.slab_ads import SlabAds_WF 36 | from WhereWulff.workflows.oer import OER_WF 37 | from WhereWulff.adsorption.adsorbate_configs import OH_Ox_list 38 | 39 | 40 | # Surface Workflow method 41 | class SlabFlows: 42 | """ 43 | SlabFlows is a general method to automatize DFT Workflows for Surface Chemistry and Catalysis. 44 | 45 | Args: 46 | bulk_structure : CIF file path. 47 | conventional_standard (default: True) : To select if bulk structure should be conventional standard. 48 | add_magmoms (default: True) : Decorates bulk structure with MAGMOM based on Crystal field Theory. 49 | include_bulk_opt (default: True) : To select if oriented bulk should be optimized (required for surface energy analysis). 50 | max_index (default: 1) : Maximum number for (h,k,l) miller indexes. 51 | symmetrize (default: True) : To enforce that top/bottom layers are symmetrized while slicing the slab model. 52 | non_stoichiometric (default: False) : To consider non-stoichiometric surfaces or not. 53 | slab_repeat (default: [2,2,1]): Slab model supercell in the xy plane. 54 | selective_dynamics (default: False) : Contraint bottom-half of the slab model. 55 | wulff_analysis (default: True) : Add Wulff shape Analysis in the workflow (To prioritize surfaces). 56 | exclude_hkl (default: list) : List of tupple miller indexes [(h, k, l), (h', k', l')] to not compute. 57 | stop_at_wulff_an (default: False) : Stop workflow at Wulff Shape level. (avoid pbx and reactivity). 58 | adsorbates_list (default: List) : List of adsorbates as Molecule PMG objects (OH/Ox) 59 | applied_potential (default: 1.60) : Applied potential to determine the most stable termination at given voltage. 60 | applied_pH (default: 0.0) : Applied pH to determine the most stable termination at give pH. 61 | metal_site (default: "") : Metal site where the reactivity should be explored (e.g. IrO2 --> Ir) 62 | vasp_input_set (default: None) : To select DFT method for surface optimizations. 63 | vasp_cmd : VASP execution command (configured in my_fworker.py file) 64 | db_file : Directs to db.json file for mongodb database configuration. 65 | 66 | Returns: 67 | The launchpad ready for execution! 68 | """ 69 | 70 | def __init__( 71 | self, 72 | bulk_structure, 73 | conventional_standard=True, 74 | add_magmoms=True, 75 | include_bulk_opt=True, 76 | max_index=1, 77 | symmetrize=True, 78 | non_stoichiometric=False, 79 | slab_repeat=[2, 2, 1], 80 | selective_dynamics=False, 81 | exclude_hkl=None, 82 | stop_at_wulff_analysis=False, 83 | adsorbates=OH_Ox_list, 84 | applied_potential=1.60, 85 | applied_pH=0, 86 | metal_site="", 87 | vasp_input_set=None, 88 | vasp_cmd=VASP_CMD, 89 | db_file=DB_FILE, 90 | ): 91 | 92 | # Bulk structure 93 | self.bulk_structure = self._read_cif_file(bulk_structure) 94 | if conventional_standard: 95 | self.bulk_structure = self._get_conventional_standard() 96 | if add_magmoms: 97 | self.bulk_structure = set_bulk_magmoms(self.bulk_structure) 98 | 99 | # Slab modeling parameters 100 | self.include_bulk_opt = include_bulk_opt 101 | self.max_index = max_index 102 | self.symmetrize = symmetrize 103 | self.non_stoichiometric = non_stoichiometric 104 | self.slab_repeat = slab_repeat 105 | self.selective_dynamics = selective_dynamics 106 | self.stop_at_wulff_analysis = stop_at_wulff_analysis 107 | self.exclude_hkl = exclude_hkl 108 | 109 | # Reactive site 110 | self.metal_site = metal_site 111 | 112 | # DFT method and vasp_cmd and db_file 113 | self.vasp_input_set = vasp_input_set 114 | self.vasp_cmd = vasp_cmd 115 | self.db_file = db_file 116 | 117 | # General info 118 | self.bulk_formula = self._get_bulk_formula() 119 | self.miller_indices = self._get_miller_indices() 120 | self.slab_structures = self._get_slab_structures() 121 | self.workflows_list = self._get_all_wfs() 122 | self.adsorbates = adsorbates 123 | 124 | # PBX conditions 125 | self.applied_potential = applied_potential 126 | self.applied_pH = applied_pH 127 | 128 | def _read_cif_file(self, bulk_structure, primitive=False): 129 | """Parse CIF file with PMG""" 130 | struct = CifParser(bulk_structure).get_structures(primitive=primitive)[0] 131 | oxid_transformer = AutoOxiStateDecorationTransformation() 132 | struct_new = oxid_transformer.apply_transformation(struct) 133 | return struct_new 134 | 135 | def _get_conventional_standard(self): 136 | """Convert Bulk structure to conventional standard""" 137 | SGA = SpacegroupAnalyzer(self.bulk_structure) 138 | bulk_structure = SGA.get_conventional_standard_structure() 139 | return bulk_structure 140 | 141 | def _get_bulk_formula(self): 142 | """Returns Bulk formula""" 143 | bulk_formula = self.bulk_structure.composition.reduced_formula 144 | return bulk_formula 145 | 146 | def _get_miller_indices(self): 147 | """Returns a list of Crystallographic orientations (hkl)""" 148 | miller_indices = get_symmetrically_distinct_miller_indices( 149 | self.bulk_structure, max_index=self.max_index 150 | ) 151 | if self.exclude_hkl: 152 | miller_indices = set(miller_indices) - set(self.exclude_hkl) 153 | return list(miller_indices) 154 | 155 | def _get_miller_vector(self, slab): 156 | """Returns the unit vector aligned with the miller index.""" 157 | mvec = np.cross(slab.lattice.matrix[0], slab.lattice.matrix[1]) 158 | return mvec / np.linalg.norm(mvec) 159 | 160 | def _count_surface_metals(self, slab): 161 | """Check whether the metal_site is on top of the surface for reactivity""" 162 | spg = SpacegroupAnalyzer(slab.oriented_unit_cell) 163 | ucell = spg.get_symmetrized_structure() 164 | v = VoronoiNN() 165 | unique_indices = [equ[0] for equ in ucell.equivalent_indices] 166 | 167 | # Check oriented cell atoms coordination 168 | cn_dict = {} 169 | for i in unique_indices: 170 | el = ucell[i].species_string 171 | if el not in cn_dict.keys(): 172 | cn_dict[el] = [] 173 | cn = v.get_cn(ucell, i, use_weights=True) 174 | cn = float("%.5f" % (round(cn, 5))) 175 | if cn not in cn_dict[el]: 176 | cn_dict[el].append(cn) 177 | 178 | # Check if metal_site in top layer 179 | active_sites = [] 180 | for i, site in enumerate(slab): 181 | if site.frac_coords[2] > slab.center_of_mass[2]: 182 | if self.metal_site in site.species_string: 183 | cn = float("%.5f" % (round(v.get_cn(slab, i, use_weights=True), 5))) 184 | if cn < min(cn_dict[site.species_string]): 185 | active_sites.append(site) 186 | 187 | # Min c parameter reference to bottom layer 188 | bottom_c = min([site.c for site in slab]) 189 | 190 | return sum([site.c - bottom_c for site in active_sites]) 191 | 192 | def _get_slab_structures(self, ftol=0.01): 193 | """Returns a list of slab structures""" 194 | slab_list = [] 195 | for mi_index in self.miller_indices: 196 | slab_gen = SlabGenerator( 197 | self.bulk_structure, 198 | miller_index=mi_index, 199 | min_slab_size=4, 200 | min_vacuum_size=8, 201 | in_unit_planes=True, 202 | center_slab=True, 203 | reorient_lattice=True, 204 | lll_reduce=True, 205 | ) 206 | 207 | if self.symmetrize: 208 | all_slabs = slab_gen.get_slabs(symmetrize=self.symmetrize, ftol=ftol) 209 | 210 | else: 211 | all_slabs = slab_gen.get_slabs(symmetrize=False, ftol=ftol) 212 | 213 | slab_candidates = [] 214 | for slab in all_slabs: 215 | slab_formula = slab.composition.reduced_formula 216 | if (not slab.is_polar() and slab.is_symmetric): 217 | if self.non_stoichiometric: 218 | slab.make_supercell(self.slab_repeat) 219 | slab_candidates.append(slab) 220 | else: 221 | if slab_formula == self.bulk_formula: 222 | slab.make_supercell(self.slab_repeat) 223 | slab_candidates.append(slab) 224 | 225 | # This is new! 226 | if len(slab_candidates) >= 1: 227 | count_metal = 0 228 | for slab_cand in slab_candidates: 229 | count = self._count_surface_metals(slab_cand) 230 | if count > count_metal: 231 | count_metal = count 232 | slab_list.append(slab_cand) 233 | else: 234 | slab_list.append(slab_candidates[0]) 235 | 236 | return slab_list 237 | 238 | def _get_all_wfs(self): 239 | """Returns the list of workflows to be launched""" 240 | # wfs for oriented bulk + slab model 241 | wfs = [] 242 | for slab in self.slab_structures: 243 | if self.selective_dynamics: 244 | slab = SelectiveDynamics.center_of_mass(slab) 245 | slab_wf = SurfaceEnergy_WF( 246 | slab, 247 | include_bulk_opt=self.include_bulk_opt, 248 | vasp_cmd=self.vasp_cmd, 249 | db_file=self.db_file, 250 | ) 251 | wfs.append(slab_wf) 252 | return wfs 253 | 254 | def _get_wulff_analysis(self, parents=None): 255 | """Returns Wulff Shape analysis""" 256 | wulff_wf, parents_fws = WulffShape_WF( 257 | self.bulk_structure, 258 | parents=parents, 259 | vasp_cmd=self.vasp_cmd, 260 | db_file=self.db_file, 261 | ) 262 | return wulff_wf, parents_fws 263 | 264 | def _get_ads_slab_wfs(self, parents=None): 265 | """Returns all the Ads_slabs fireworks""" 266 | ads_slab_wfs, parents_fws = SlabAds_WF( 267 | self.bulk_structure, 268 | self.adsorbates, 269 | parents=parents, 270 | vasp_cmd=self.vasp_cmd, 271 | db_file=self.db_file, 272 | metal_site=self.metal_site, 273 | applied_potential=self.applied_potential, 274 | applied_pH=self.applied_pH, 275 | ) 276 | return ads_slab_wfs, parents_fws 277 | 278 | def _get_oer_reactivity(self, parents=None): 279 | """Returns all the OER ads_slab fireworks""" 280 | oer_fws = [] 281 | for hkl in self.miller_indices: 282 | miller_index = "".join(list(map(str, hkl))) 283 | oer_fw = OER_WF( 284 | self.bulk_structure, 285 | miller_index=miller_index, 286 | metal_site=self.metal_site, 287 | applied_potential=self.applied_potential, 288 | applied_pH=self.applied_pH, 289 | parents=parents, 290 | vasp_cmd=self.vasp_cmd, 291 | db_file=self.db_file, 292 | ) 293 | oer_fws.append(oer_fw) 294 | 295 | # convert fws list into wf 296 | wf_name = f"{self.bulk_structure.composition.reduced_formula}-{miller_index} OER Single Site WNA" 297 | oer_wf = self._convert_to_workflow(oer_fws, name=wf_name, parents=parents) 298 | return oer_wf 299 | 300 | def _get_parents(self, workflow_list): 301 | """Returns an unpacked list of parents from a set of wfs""" 302 | wf_fws = [wf.fws for wf in workflow_list] 303 | fws = [fw for wf in wf_fws for fw in wf] 304 | return fws 305 | 306 | def _convert_to_workflow(self, fws_list, name="", parents=None): 307 | """Helper function that converts list of fws into a workflow""" 308 | if parents is not None: 309 | fws_list.extend(parents) 310 | wf = Workflow(fws_list, name=name) 311 | return wf 312 | 313 | def submit(self, hostname, db_name, port, username, password, reset=False): 314 | """Submit Full Workflow to Launchpad !""" 315 | launchpad = ( 316 | LaunchPad( 317 | host=hostname, 318 | name=db_name, 319 | port=port, 320 | username=username, 321 | password=password, 322 | ) 323 | if hostname 324 | else LaunchPad() 325 | ) 326 | if reset: 327 | launchpad.reset("", require_password=False) 328 | 329 | parents_list = self._get_parents(self.workflows_list) 330 | 331 | # Wulff shape analysis 332 | if self.stop_at_wulff_analysis: 333 | wulff_wf = self._get_wulff_analysis(parents=parents_list) 334 | launchpad.add_wf(wulff_wf) 335 | 336 | else: 337 | # Add Wulff Analysis 338 | if len(self.miller_indices) > 1: 339 | wulff_wf, wulff_parents = self._get_wulff_analysis(parents=parents_list) 340 | 341 | # Surface Pourbaix Diagram (OH/Ox) 342 | ads_slab_wf, ads_slab_fws = self._get_ads_slab_wfs( 343 | parents=wulff_parents 344 | ) 345 | else: # case where there is only one surface orientation 346 | # skip the Wulff 347 | ads_slab_wf, ads_slab_fws = self._get_ads_slab_wfs(parents=parents_list) 348 | 349 | launchpad.add_wf(ads_slab_wf) 350 | 351 | return launchpad 352 | -------------------------------------------------------------------------------- /WhereWulff/reactivity/oer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import numpy as np 9 | 10 | from pymatgen.core.structure import Structure, Molecule 11 | from pymatgen.core.periodic_table import Element 12 | 13 | 14 | from WhereWulff.adsorption.MXide_adsorption import MXideAdsorbateGenerator 15 | from WhereWulff.adsorption.adsorbate_configs import oer_adsorbates_dict 16 | 17 | 18 | class OER_SingleSite(object): 19 | """ 20 | This class automatically generates the OER (single site) intermediates required to 21 | study the Water Nucleophilic Attack (WNA) on top of a Metal oxide surface. 22 | 23 | Args: 24 | slab (PMG Slab object): Should be the most stable termination comming from the PBX analysis 25 | metal_site (string: Ir) : User selected metal_site composition when the material is bi-metallic 26 | adsorbates (Dict) : Is a dict of the well-known OER-WNA adsorbates (OH, Ox, OOH) 27 | random_state (default: 42) : This method should choose the active site automatically and (pseudo-randomly) 28 | 29 | Return: 30 | A dictionary of intermediates e.g. {"reference", "OH_0", "OH_1",...,"OOH_up_0",..., "OOH_down_0",...,} 31 | """ 32 | 33 | def __init__( 34 | self, 35 | slab, 36 | slab_orig, 37 | slab_clean, 38 | bulk_like_sites, 39 | metal_site="", 40 | adsorbates=oer_adsorbates_dict, 41 | random_state=42, 42 | ): 43 | self.slab = slab 44 | self.slab_orig = slab_orig 45 | self.slab_clean = slab_clean 46 | self.bulk_like_sites = bulk_like_sites 47 | self.metal_site = metal_site 48 | self.adsorbates = adsorbates 49 | self.random_state = random_state 50 | 51 | # We need to remove oxidation states 52 | self.slab_clean.remove_oxidation_states() 53 | self.slab_clean.oriented_unit_cell.remove_oxidation_states() 54 | 55 | # Inspect slab site properties to determine termination (OH/Ox) 56 | ( 57 | self.surface_coverage, 58 | self.ads_species, 59 | self.ads_indices, 60 | self.termination_info, 61 | ) = self._get_surface_termination() 62 | 63 | # Cache all the idx 64 | if not self.surface_coverage[0] == "clean": 65 | self.all_ads_indices = self.ads_indices.copy() 66 | 67 | # Select active site composition 68 | active_sites_dict = self._group_ads_sites_by_metal() 69 | assert ( 70 | self.metal_site in active_sites_dict.keys() 71 | ), f"There is no available {self.metal_site} on the surface" 72 | self.ads_indices = active_sites_dict[self.metal_site] 73 | 74 | # Generate slab reference to place the adsorbates 75 | self.ref_slab, self.reactive_idx = self._get_reference_slab() 76 | 77 | # Mxide method 78 | self.mxidegen = self._mxidegen() 79 | if not self.surface_coverage[0] == "clean": 80 | 81 | # Shifted bulk_like_sites 82 | self.bulk_like_dict = self._get_shifted_bulk_like_sites() 83 | 84 | # Selected site 85 | self.selected_site = self.bulk_like_dict[self.reactive_idx] 86 | else: 87 | self.selected_site = [self.bulk_like_sites[self.reactive_idx]] 88 | 89 | # np seed 90 | np.random.seed(self.random_state) 91 | 92 | def _get_surface_termination(self): 93 | """Helper function to get whether the surface is OH or Ox terminated""" 94 | termination_info = [ 95 | [idx, site.specie, site.frac_coords] 96 | for idx, site in enumerate(self.slab.sites) 97 | if "surface_properties" in site.properties 98 | and site.properties["surface_properties"] == "adsorbate" 99 | ] 100 | 101 | # Filter sites information 102 | ads_species = [site[1] for site in termination_info] 103 | ads_indices = [site[0] for site in termination_info] 104 | 105 | # OH or Ox coverage 106 | surface_coverage = ["oxo" if Element("H") not in ads_species else "oh"] 107 | 108 | if len(termination_info) == 0: # clean termination 109 | surface_coverage = ["clean"] 110 | return surface_coverage, ads_species, ads_indices, termination_info 111 | 112 | return surface_coverage, ads_species, ads_indices, termination_info 113 | 114 | def _find_nearest_bulk_like_site(self, bulk_like_sites, reactive_idx): 115 | """Find reactive site by min distance between bulk-like and selected reactive site""" 116 | ox_site = [site for idx, site in enumerate(self.slab) if idx == reactive_idx][0] 117 | 118 | min_dist = np.inf 119 | for bulk_like_site in bulk_like_sites: 120 | dist = np.linalg.norm(bulk_like_site - ox_site.coords) 121 | if dist <= min_dist: 122 | min_dist = dist 123 | nn_site = bulk_like_site 124 | return nn_site 125 | 126 | def _find_nearest_hydrogen(self, site_idx, search_list): 127 | """Depending on how the surface atoms are sorted we need to find the nearest H""" 128 | fixed_site = [site for idx, site in enumerate(self.slab) if idx == site_idx][0] 129 | 130 | min_dist = np.inf 131 | for site in search_list: 132 | dist = np.linalg.norm(fixed_site.frac_coords - site[2]) 133 | if dist <= min_dist: 134 | min_dist = dist 135 | nn_site = site 136 | return nn_site[0] 137 | 138 | def _find_nearest_metal(self, reactive_idx): 139 | """Find reactive site by min distance between any metal and oxygen""" 140 | reactive_site = [ 141 | site for idx, site in enumerate(self.slab) if idx == reactive_idx 142 | ][0] 143 | 144 | min_dist = np.inf 145 | for site in self.slab: 146 | if Element(site.specie).is_metal: 147 | dist = site.distance(reactive_site) 148 | if dist <= min_dist: 149 | min_dist = dist 150 | closest_metal = site 151 | return closest_metal 152 | 153 | def _group_ads_sites_by_metal(self): 154 | """Groups self.ads_indices by metal""" 155 | sites_by_metal = {} 156 | for ads_idx in self.ads_indices: 157 | close_site = self._find_nearest_metal(ads_idx) 158 | if str(close_site.specie) not in sites_by_metal.keys(): 159 | sites_by_metal.update({str(close_site.specie): [ads_idx]}) 160 | elif str(close_site.specie) in sites_by_metal.keys(): 161 | sites_by_metal[str(close_site.specie)].append(ads_idx) 162 | if self.surface_coverage[0] == "clean": 163 | end_idx = np.where( 164 | self.slab_clean.frac_coords[:, 2] >= self.slab_clean.center_of_mass[2] 165 | )[0][-1] 166 | metal_idx = [] 167 | for bulk_idx, bulk_like_site in enumerate(self.bulk_like_sites): 168 | min_dist = np.inf # initialize the min_dist register 169 | min_metal_idx = 0 # initialize the min_ox_idx 170 | for idx, site in enumerate(self.slab_clean): 171 | if ( 172 | site.species_string != "O" # FIXME 173 | and site.frac_coords[2] > self.slab_clean.center_of_mass[2] 174 | ): # metal 175 | dist = np.linalg.norm(bulk_like_site - site.coords) 176 | if dist < min_dist: 177 | min_dist = dist # update the dist register 178 | min_metal_idx = idx # update the idx register 179 | min_specie = site.species_string 180 | if ( 181 | idx == end_idx 182 | ): # make sure that len(bulk_like_sites) == len(ox_idx) 183 | metal_idx.append(min_metal_idx) 184 | if min_specie not in sites_by_metal: 185 | sites_by_metal[min_specie] = [] 186 | sites_by_metal[min_specie].append((min_metal_idx, bulk_idx)) 187 | else: 188 | sites_by_metal[min_specie].append((min_metal_idx, bulk_idx)) 189 | 190 | return sites_by_metal 191 | 192 | def _get_reference_slab(self): 193 | """Random selects a termination adsorbate and clean its""" 194 | 195 | if self.surface_coverage[0] == "oxo": 196 | ref_slab = self.slab.copy() 197 | # Orig magmoms for the adslabs 198 | ref_slab.add_site_property( 199 | "magmom", self.slab_orig.site_properties["magmom"] 200 | ) 201 | reactive_site = np.random.choice(self.ads_indices) 202 | ref_slab.remove_sites(indices=[reactive_site]) 203 | 204 | return ref_slab, reactive_site 205 | 206 | elif self.surface_coverage[0] == "oh": 207 | ref_slab = self.slab.copy() 208 | # Orig magmoms for the adslabs 209 | ref_slab.add_site_property( 210 | "magmom", self.slab_orig.site_properties["magmom"] 211 | ) 212 | ads_indices_oxygen = [ 213 | site[0] for site in self.termination_info if site[1] == Element("O") 214 | ] 215 | ads_indices_hyd = [ 216 | site for site in self.termination_info if site[1] == Element("H") 217 | ] 218 | reactive_site_oxygen = np.random.choice(ads_indices_oxygen) 219 | hyd_site = self._find_nearest_hydrogen( 220 | reactive_site_oxygen, ads_indices_hyd 221 | ) 222 | reactive_site = [reactive_site_oxygen, hyd_site] 223 | ref_slab.remove_sites(indices=reactive_site) 224 | 225 | return ref_slab, reactive_site_oxygen 226 | else: # clean termination? 227 | ref_slab = self.slab_clean.copy() 228 | # Orig magmoms for the adslabs 229 | ref_slab.add_site_property( 230 | "magmom", self.slab_orig.site_properties["magmom"] 231 | ) 232 | reactive_site = np.random.choice([x[1] for x in self.ads_indices]) 233 | return ref_slab, reactive_site 234 | 235 | def _mxidegen(self, repeat=[1, 1, 1], verbose=False): 236 | """Returns the MXide Method for the ref_slab""" 237 | mxidegen = MXideAdsorbateGenerator( 238 | self.slab_clean, 239 | repeat=repeat, 240 | verbose=verbose, 241 | positions=["MX_adsites"], 242 | relax_tol=0.025, 243 | ) 244 | return mxidegen 245 | 246 | def _get_shifted_bulk_like_sites(self): 247 | """Get Perturbed bulk-like sites""" 248 | # Bondlength and X-specie from mxide method 249 | _, X = self.mxidegen.bondlengths_dict, self.mxidegen.X 250 | 251 | # Perturb pristine bulk_like sites {idx: np.array([x,y,z])} 252 | bulk_like_shifted_dict = self._bulk_like_adsites_perturbation_oxygens( 253 | self.slab_orig, self.slab, X=X 254 | ) 255 | 256 | return bulk_like_shifted_dict 257 | 258 | def _bulk_like_adsites_perturbation( 259 | self, slab_ref, slab, bulk_like_sites, bondlength, X 260 | ): 261 | """Let's perturb bulk_like_sites with delta (xyz)""" 262 | slab_ref_coords = slab_ref.cart_coords 263 | slab_coords = slab.cart_coords 264 | 265 | delta_coords = slab_coords - slab_ref_coords 266 | 267 | metal_idx = [] 268 | for bulk_like_site in bulk_like_sites: 269 | for idx, site in enumerate(slab_ref): 270 | if ( 271 | site.specie != Element(X) 272 | and site.coords[2] > slab_ref.center_of_mass[2] 273 | ): 274 | dist = np.linalg.norm(bulk_like_site - site.coords) 275 | if dist < bondlength: 276 | metal_idx.append(idx) 277 | 278 | bulk_like_deltas = [delta_coords[i] for i in metal_idx] 279 | return [n + m for n, m in zip(bulk_like_sites, bulk_like_deltas)] 280 | 281 | def _bulk_like_adsites_perturbation_oxygens(self, slab_ref, slab, X): 282 | """peturbation on oxygens""" 283 | slab_ref_coords = slab_ref.cart_coords # input 284 | slab_coords = slab.cart_coords # output 285 | end_idx = np.where(slab_ref.frac_coords[:, 2] >= slab_ref.center_of_mass[2])[0][ 286 | -1 287 | ] 288 | 289 | delta_coords = slab_coords - slab_ref_coords 290 | 291 | ox_idx = [] 292 | for bulk_like_site in self.bulk_like_sites: 293 | min_dist = np.inf # initialize the min_dist register 294 | min_ox_idx = 0 # initialize the min_ox_idx 295 | for idx, site in enumerate(slab_ref): 296 | if ( 297 | site.specie == Element(X) 298 | and site.frac_coords[2] > slab_ref.center_of_mass[2] 299 | ): 300 | dist = np.linalg.norm(bulk_like_site - site.coords) 301 | if dist < min_dist: 302 | min_dist = dist # update the dist register 303 | min_ox_idx = idx # update the idx register 304 | if idx == end_idx: # make sure that len(bulk_like_sites) == len(ox_idx) 305 | ox_idx.append(min_ox_idx) 306 | 307 | bulk_like_deltas = [delta_coords[i] for i in ox_idx] 308 | bulk_like_shifted = [ 309 | n + m for n, m in zip(self.bulk_like_sites, bulk_like_deltas) 310 | ] 311 | return {k: [v] for (k, v) in zip(ox_idx, bulk_like_shifted)} 312 | 313 | def _get_clean_slab(self): 314 | """Remove all the adsorbates""" 315 | clean_slab = self.slab_orig.copy() 316 | clean_slab.remove_sites(indices=self.all_ads_indices) 317 | return clean_slab 318 | 319 | def _get_oer_intermediates( 320 | self, adsorbate, suffix=None, axis_rotation=[0, 0, 1], n_rotations=4 321 | ): 322 | """Returns OH/Ox/OOH intermediates""" 323 | # Adsorbate manipulation 324 | adsorbate = adsorbate.copy() 325 | adsorbate_label = "".join([site.species_string for site in adsorbate]) 326 | adsorbate_angles = self._get_angles(n_rotations=n_rotations) 327 | adsorbate_rotations = self.mxidegen.get_transformed_molecule( 328 | adsorbate, axis=axis_rotation, angles_list=adsorbate_angles 329 | ) 330 | # Intermediates generation 331 | intermediates_dict = {} 332 | for ads_rot_idx in range(len(adsorbate_rotations)): 333 | ads_slab = self.ref_slab.copy() 334 | ads_slab = self._add_adsorbates( 335 | ads_slab, self.selected_site, adsorbate_rotations[ads_rot_idx] 336 | ) 337 | if suffix: 338 | intermediates_dict.update( 339 | {f"{adsorbate_label}_{suffix}_{ads_rot_idx}": ads_slab.as_dict()} 340 | ) 341 | else: 342 | intermediates_dict.update( 343 | {f"{adsorbate_label}_{ads_rot_idx}": ads_slab.as_dict()} 344 | ) 345 | 346 | return intermediates_dict 347 | 348 | def _get_angles(self, n_rotations=4): 349 | """Returns the list of angles depeding on the n of rotations""" 350 | angles = [] 351 | for i in range(n_rotations): 352 | deg = (2 * np.pi / n_rotations) * i 353 | angles.append(deg) 354 | return angles 355 | 356 | def _add_adsorbates(self, adslab, ads_coords, molecule, z_offset=[0, 0, 0.15]): 357 | """Add molecule in the open coordination site""" 358 | translated_molecule = molecule.copy() 359 | for ads_site in ads_coords: 360 | for mol_site in translated_molecule: 361 | new_coord = ads_site + (mol_site.coords - z_offset) 362 | adslab.append( 363 | mol_site.specie, 364 | new_coord, 365 | coords_are_cartesian=True, 366 | properties=mol_site.properties, 367 | ) 368 | return adslab 369 | 370 | def generate_oer_intermediates(self, suffix=None): 371 | """General method to get OER-WNA (single site) intermediantes""" 372 | # Get Reference slab (*) 373 | reference_slab = self.ref_slab.as_dict() 374 | reference_dict = {"reference": reference_slab} 375 | 376 | # Generate intermediantes depeding on Termination 377 | if self.surface_coverage[0] == "oxo": 378 | oh_intermediates = self._get_oer_intermediates(self.adsorbates["OH"]) 379 | ooh_up = self._get_oer_intermediates(self.adsorbates["OOH_up"], suffix="up") 380 | ooh_down = self._get_oer_intermediates( 381 | self.adsorbates["OOH_down"], suffix="down" 382 | ) 383 | oer_intermediates = { 384 | **reference_dict, 385 | **oh_intermediates, 386 | **ooh_up, 387 | **ooh_down, 388 | } 389 | return oer_intermediates 390 | 391 | elif self.surface_coverage[0] == "oh": 392 | ox_intermediates = self._get_oer_intermediates( 393 | self.adsorbates["Ox"], n_rotations=1 394 | ) 395 | ooh_up = self._get_oer_intermediates(self.adsorbates["OOH_up"], suffix="up") 396 | ooh_down = self._get_oer_intermediates( 397 | self.adsorbates["OOH_down"], suffix="down" 398 | ) 399 | oer_intermediates = { 400 | **reference_dict, 401 | **ox_intermediates, 402 | **ooh_up, 403 | **ooh_down, 404 | } 405 | else: # clean termination 406 | ox_intermediates = self._get_oer_intermediates( 407 | self.adsorbates["Ox"], n_rotations=1 408 | ) 409 | oh_intermediates = self._get_oer_intermediates(self.adsorbates["OH"]) 410 | ooh_up = self._get_oer_intermediates(self.adsorbates["OOH_up"], suffix="up") 411 | ooh_down = self._get_oer_intermediates( 412 | self.adsorbates["OOH_down"], suffix="down" 413 | ) 414 | oer_intermediates = { 415 | **reference_dict, 416 | **ox_intermediates, 417 | **oh_intermediates, 418 | **ooh_up, 419 | **ooh_down, 420 | } 421 | 422 | return oer_intermediates 423 | -------------------------------------------------------------------------------- /WhereWulff/tests/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | import os 9 | import json 10 | import shutil 11 | import warnings 12 | import unittest 13 | 14 | import numpy as np 15 | 16 | from pymatgen.core.structure import Structure 17 | from WhereWulff.workflows.catflows import CatFlows 18 | 19 | warnings.filterwarnings("ignore") 20 | 21 | 22 | class CatFlowsTest(unittest.TestCase): 23 | """Unittest for the General Workflow""" 24 | 25 | def setUp(self): 26 | """Automatically called for every single test""" 27 | self.cif_file = "./RuO2_136.cif" 28 | self.catflows = CatFlows(self.cif_file) 29 | 30 | self.bulk_formula = self.catflows.bulk_formula 31 | self.max_index = self.catflows.max_index 32 | return 33 | 34 | def test_magmoms(self): 35 | """Check magnetic moments""" 36 | struct_mag = self.catflows._get_bulk_magmoms() 37 | magmoms = struct_mag.site_properties["magmom"] 38 | return self.assertEqual(magmoms, [2.4, 2.4, 0.6, 0.6, 0.6, 0.6]) 39 | 40 | def test_bulk_formula(self): 41 | """Check Bulk reduced formula""" 42 | bulk_formula = self.catflows._get_bulk_formula() 43 | return self.assertEqual(bulk_formula, "RuO2") 44 | 45 | def test_miller_indices(self): 46 | """Check miller indices list""" 47 | miller_indices = self.catflows._get_miller_indices() 48 | max_index = np.unique([np.max(hkl) for hkl in miller_indices])[0] 49 | return self.assertEqual(max_index, self.max_index) 50 | 51 | def test_slab_structures(self): 52 | """Check Slab structures""" 53 | slab_structures = self.catflows._get_slab_structures() 54 | for slab in slab_structures: 55 | slab_formula = slab.composition.reduced_formula 56 | if ( 57 | not slab.is_polar() 58 | and slab.is_symmetric() 59 | and slab_formula == self.bulk_formula 60 | ): 61 | filter = True 62 | return self.assertTrue(filter) 63 | 64 | 65 | # Execute Testing 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /WhereWulff/workflows/bulk_stability.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from fireworks import Firework, Workflow 11 | from atomate.vasp.config import DB_FILE 12 | 13 | from WhereWulff.analysis.bulk_stability import BulkStabilityAnalysis 14 | 15 | 16 | def StabilityBulk_WF(bulk_structure, parents=None, db_file=DB_FILE): 17 | """ 18 | Wrap-up workflow to do the Stability Analysis for each magnetic ordering. 19 | 20 | Args: 21 | bulk_structure : PMG structure object from EOS fitting, transformation and static calc. 22 | parents (default: None) : Previous tasks or worklfows. 23 | db_file : Connects directly to db.json file. 24 | 25 | Returns: 26 | Workflow as terminal node for bulk stability after EOS transformation 27 | and the single point calc. 28 | 29 | """ 30 | # Bulk structure formula 31 | bulk_formula = bulk_structure.composition.reduced_formula 32 | 33 | # BulkStabilityAnalsysis for NM, FM and AFM Single-points 34 | bulk_stability_fw = Firework( 35 | BulkStabilityAnalysis(reduced_formula=bulk_formula, db_file=db_file), 36 | name=f"{bulk_formula} Bulk Stability Analysis", 37 | parents=parents, 38 | ) 39 | 40 | all_fws = [bulk_stability_fw] 41 | if parents is not None: 42 | all_fws.extend(parents) 43 | stab_wf = Workflow(all_fws, name=f"{bulk_formula} Bulk Stability Analysis") 44 | return stab_wf 45 | -------------------------------------------------------------------------------- /WhereWulff/workflows/eos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | import numpy as np 11 | 12 | from pymatgen.core.structure import Structure 13 | from pymatgen.analysis.elasticity.strain import Deformation 14 | 15 | from fireworks import Firework, Workflow 16 | from atomate.vasp.config import VASP_CMD, DB_FILE 17 | from atomate.vasp.fireworks.core import TransmuterFW 18 | 19 | from WhereWulff.dft_settings.settings import MOSurfaceSet 20 | from WhereWulff.fireworks.optimize import Bulk_FW 21 | from WhereWulff.analysis.equation_of_states import FitEquationOfStateFW 22 | import uuid 23 | 24 | 25 | def EOS_WF( 26 | bulk_structures_dict, 27 | deformations=None, 28 | n_deformations=21, 29 | vasp_input_set=None, 30 | vasp_cmd=VASP_CMD, 31 | db_file=DB_FILE, 32 | ): 33 | """ 34 | Equation of state workflow that handles optimization + Deformation + EOS_FIT 35 | 36 | Args: 37 | bulk_structures_dict : Dictionary of PMG structure objects as dict. 38 | deformations (default: None) : Specific way to build the deformations, if none linspace of 15%. 39 | n_deformations (default: 21) : Number of deformations per magnetic configuration. 40 | vasp_input_set (default: None) : To add specific vasp inputs. 41 | vasp_cmd : VASP execution command (configured in my_fworker.py file) 42 | db_file : Directs to db.json file for mongodb database configuration. 43 | 44 | Return: 45 | Workflow, which consist in optimization + deformations + EOS_FIT 46 | """ 47 | fws, fws_all = [], [] 48 | 49 | # Bulk-optimization settings 50 | bulk_structure = Structure.from_dict(bulk_structures_dict["NM"]) 51 | vasp_opt = MOSurfaceSet(bulk_structure, user_incar_settings={"ISIF": 3}, bulk=True) 52 | 53 | # Bulk structure optimization 54 | opt_name = f"{bulk_structure.composition.reduced_formula}_bulk_optimization" 55 | fws.append( 56 | Bulk_FW( 57 | bulk_structure, 58 | name=opt_name, 59 | vasp_input_set=vasp_opt, 60 | vasp_cmd=vasp_cmd, 61 | db_file=db_file, 62 | ) 63 | ) 64 | opt_parent = fws[0] 65 | fws_all.append(fws[0]) 66 | 67 | # Deformations 68 | if not deformations: 69 | deformations = [ 70 | (np.identity(3) * (1 + x)).tolist() 71 | for x in np.linspace(-0.157, 0.157, n_deformations) 72 | ] 73 | deformations = [Deformation(defo_mat) for defo_mat in deformations] 74 | # breakpoint() 75 | for counter, (mag_ordering, bulk_struct) in enumerate(bulk_structures_dict.items()): 76 | bulk_struct = Structure.from_dict(bulk_struct) 77 | vasp_static = MOSurfaceSet( 78 | bulk_struct, user_incar_settings={"NSW": 0}, bulk=True 79 | ) 80 | if counter != 0: 81 | fws = [opt_parent] 82 | for n, deformation in enumerate(deformations): 83 | # Create unique uuid for each deformation 84 | deform_uuid = uuid.uuid4() 85 | name_deformation = f"{bulk_structure.composition.reduced_formula}_{mag_ordering}_deformation_{n}" 86 | fw = TransmuterFW( 87 | copy_vasp_outputs=False, # default is True 88 | name=name_deformation, 89 | structure=bulk_struct, 90 | transformations=["DeformStructureTransformation"], 91 | transformation_params=[{"deformation": deformation.tolist()}], 92 | vasp_input_set=vasp_static, 93 | parents=opt_parent, 94 | vasp_cmd=vasp_cmd, 95 | db_file=db_file, 96 | ) 97 | # Add deform_uuid to the task doc in the tasks collection 98 | fw.tasks[3]["additional_fields"].update({"deform_uuid": deform_uuid}) 99 | # Send the deform_uuid to the corresponding EOS FW 100 | fw.tasks[3].update( 101 | {"task_fields_to_push": {f"deformation_uuid_{n}": "deform_uuid"}} 102 | ) 103 | fws.append(fw) 104 | fws_all.append(fw) 105 | 106 | # Fit EOS task 107 | fit_parents = fws[1:] 108 | name_fit_eos = f"{bulk_structure.composition.reduced_formula}_{mag_ordering}_eos_fitting_analysis" 109 | fw_analysis = Firework( 110 | FitEquationOfStateFW(magnetic_ordering=mag_ordering, db_file=db_file), 111 | name=name_fit_eos, 112 | parents=fit_parents, 113 | ) 114 | # fws.append(fw_analysis) 115 | fws_all.append(fw_analysis) 116 | 117 | # breakpoint() 118 | # Create Workflow 119 | wf_eos = Workflow(fws_all) 120 | wf_eos.name = f"{bulk_structure.composition.reduced_formula}_eos_fitting_analysis" 121 | return wf_eos, fws_all 122 | 123 | 124 | def BulkOptimize_WF( 125 | bulk_structure, vasp_input_set=None, vasp_cmd=VASP_CMD, db_file=DB_FILE 126 | ): 127 | """ 128 | Bulk optimization workflow. 129 | 130 | Args: 131 | 132 | Return: 133 | Workflow, which consist in bulk optimization. 134 | """ 135 | fws = [] 136 | 137 | # Bulk-optimization 138 | vasp_opt = MOSurfaceSet(bulk_structure, user_incar_settings={"ISIF": 3}, bulk=True) 139 | 140 | # Bulk structure optimization 141 | name_bulk = f"{bulk_structure.composition.reduced_formula}_bulk_optimization" 142 | fws.append( 143 | Bulk_FW( 144 | bulk_structure, 145 | name=name_bulk, 146 | vasp_input_set=vasp_opt, 147 | vasp_cmd=vasp_cmd, 148 | db_file=db_file, 149 | ) 150 | ) 151 | 152 | # Create Workflow 153 | wf_opt = Workflow(fws) 154 | wf_opt.name = f"{bulk_structure.composition.reduced_formula}_OPT_WF" 155 | return wf_opt, fws 156 | 157 | 158 | def EOS_WF_2( 159 | bulk_structure, 160 | deformations=None, 161 | magnetic_ordering="NM", 162 | vasp_input_set=None, 163 | parents=None, 164 | vasp_cmd=VASP_CMD, 165 | db_file=DB_FILE, 166 | ): 167 | """ 168 | Bulk Deformation workflow that handles Deformation + EOS_Fit. 169 | 170 | Args: 171 | 172 | Return: 173 | Workflow, which consist in Deformation + EOS Fitting 174 | """ 175 | # fws = [parents[0]] 176 | fws = [] 177 | 178 | # Linspace deformations 179 | if not deformations: 180 | deformations = [ 181 | (np.identity(3) * (1 + x)).tolist() for x in np.linspace(-0.157, 0.157, 6) 182 | ] 183 | 184 | # Deformations 185 | vasp_static = MOSurfaceSet( 186 | bulk_structure, user_incar_settings={"NSW": 0}, bulk=True 187 | ) 188 | deformations = [Deformation(defo_mat) for defo_mat in deformations] 189 | for n, deformation in enumerate(deformations): 190 | name_deformation = f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_deformation_{n}" 191 | fw = TransmuterFW( 192 | name=name_deformation, 193 | structure=bulk_structure, 194 | transformations=["DeformStructureTransformation"], 195 | transformation_params=[{"deformation": deformation.tolist()}], 196 | vasp_input_set=vasp_static, 197 | parents=parents[0], 198 | vasp_cmd=vasp_cmd, 199 | db_file=db_file, 200 | ) 201 | fws.append(fw) 202 | 203 | # Fit EOS task 204 | # parents = fws[1:] 205 | name_fit_eos = f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_eos_fitting_analysis" 206 | fw_analysis = Firework( 207 | FitEquationOfStateFW(magnetic_ordering=magnetic_ordering, db_file=db_file), 208 | name=name_fit_eos, 209 | parents=parents, 210 | ) 211 | fws.append(fw_analysis) 212 | 213 | # Include parents 214 | # if parents is not None: 215 | # fws.extend(parents) 216 | 217 | # breakpoint() 218 | 219 | # Create workflow 220 | wf_eos = Workflow(fws) 221 | wf_eos.name( 222 | f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_EOS_WF" 223 | ) 224 | return wf_eos 225 | 226 | 227 | def EOS_WF_OLD( 228 | bulk_structure, 229 | deformations=None, 230 | magnetic_ordering="NM", 231 | vasp_input_set=None, 232 | vasp_cmd=VASP_CMD, 233 | db_file=DB_FILE, 234 | ): 235 | """ 236 | Equation of state workflow that handles optimization + Deformation + EOS_FIT 237 | 238 | Args: 239 | bulk_structure (Structure): Pymatgen bulk structure with magnetic moments and ordering. 240 | deformations : List of scaling factors that tunes the cell volume. 241 | magnetic_ordering : Depeding if the magnetic configuration is ["NM", "AFM", "FM"]. 242 | vasp_input_set : Alternative DFT method. 243 | vasp_cmd : Environment variable for vasp execution. 244 | db_file : To connect to the database. 245 | 246 | Return: 247 | Workflow, which consist in optimization + Deformation + EOS_Fit 248 | """ 249 | fws, parents = [], [] 250 | 251 | # Bulk-optimization 252 | vasp_opt = MOSurfaceSet(bulk_structure, user_incar_settings={"ISIF": 3}, bulk=True) 253 | 254 | # linspace deformations 255 | if not deformations: 256 | deformations = [ 257 | (np.identity(3) * (1 + x)).tolist() for x in np.linspace(-0.157, 0.157, 6) 258 | ] 259 | 260 | # Bulk structure optimization 261 | name_bulk = f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_bulk_optimization" 262 | fws.append( 263 | Bulk_FW( 264 | bulk_structure, 265 | name=name_bulk, 266 | vasp_input_set=vasp_opt, 267 | vasp_cmd=vasp_cmd, 268 | db_file=db_file, 269 | ) 270 | ) 271 | parents = fws[0] 272 | 273 | # Deformations 274 | vasp_static = MOSurfaceSet( 275 | bulk_structure, user_incar_settings={"NSW": 0}, bulk=True 276 | ) 277 | deformations = [Deformation(defo_mat) for defo_mat in deformations] 278 | for n, deformation in enumerate(deformations): 279 | name_deformation = f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_deformation_{n}" 280 | fw = TransmuterFW( 281 | name=name_deformation, 282 | structure=bulk_structure, 283 | transformations=["DeformStructureTransformation"], 284 | transformation_params=[{"deformation": deformation.tolist()}], 285 | vasp_input_set=vasp_static, 286 | parents=parents, 287 | vasp_cmd=vasp_cmd, 288 | db_file=db_file, 289 | ) 290 | fws.append(fw) 291 | 292 | # Fit EOS task 293 | parents = fws[1:] 294 | name_fit_eos = f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_eos_fitting_analysis" 295 | fw_analysis = Firework( 296 | FitEquationOfStateFW(magnetic_ordering=magnetic_ordering, db_file=db_file), 297 | name=name_fit_eos, 298 | parents=parents, 299 | ) 300 | fws.append(fw_analysis) 301 | 302 | # Create workflow 303 | wf_eos = Workflow(fws) 304 | wf_eos.name = ( 305 | f"{bulk_structure.composition.reduced_formula}_{magnetic_ordering}_EOS_WF" 306 | ) 307 | return wf_eos 308 | -------------------------------------------------------------------------------- /WhereWulff/workflows/oer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from fireworks import Firework, Workflow 11 | from atomate.vasp.config import VASP_CMD, DB_FILE 12 | 13 | from WhereWulff.firetasks.oer_single_site import OERSingleSiteFireTask 14 | 15 | 16 | def OER_WF( 17 | bulk_structure, 18 | miller_index, 19 | slab_orig, 20 | bulk_like_sites, 21 | ads_dict_orig, 22 | metal_site, 23 | applied_potential=1.60, 24 | applied_pH=0, 25 | parents=None, 26 | vasp_cmd=VASP_CMD, 27 | db_file=DB_FILE, 28 | surface_pbx_uuid="", 29 | ): 30 | """ 31 | Wrap-up workflow to do the OER Single site WNA after SurfacePBX. 32 | Args: 33 | bulk_structure (Structure): Bulk structure to refer the wulff shape 34 | miller_index (String) : Crystallographic orientation (h,k,l). 35 | applied_potential (float) : Potential at which the surface performs OER. 36 | applied_pH (int) : pH conditions for either acidic or alkaline OER. 37 | parents (list) : fw_ids for previous FireTasks. 38 | vasp_cmd : VASP executable 39 | db_file : DB file. 40 | Returns: 41 | OER Workflow to generate/optimize OER intermediates and reactivity Analysis. 42 | """ 43 | # Bulk structure formula 44 | bulk_formula = bulk_structure.composition.reduced_formula 45 | 46 | # WulffShape Analysis 47 | oer_fw = Firework( 48 | OERSingleSiteFireTask( 49 | reduced_formula=bulk_formula, 50 | miller_index=miller_index, 51 | slab_orig=slab_orig, 52 | bulk_like_sites=bulk_like_sites, 53 | ads_dict_orig=ads_dict_orig, 54 | metal_site=metal_site, 55 | applied_potential=applied_potential, 56 | applied_pH=applied_pH, 57 | db_file=db_file, 58 | vasp_cmd=vasp_cmd, 59 | surface_pbx_uuid=surface_pbx_uuid, 60 | ), 61 | name=f"{bulk_formula}-{miller_index} OER Single Site WNA", 62 | parents=parents, 63 | ) 64 | 65 | return oer_fw 66 | -------------------------------------------------------------------------------- /WhereWulff/workflows/oer_single_site.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | import uuid 10 | import numpy as np 11 | 12 | from fireworks import Workflow 13 | from atomate.vasp.config import VASP_CMD, DB_FILE 14 | 15 | from pymatgen.core.surface import Slab 16 | 17 | from WhereWulff.fireworks.optimize import AdsSlab_FW 18 | from WhereWulff.fireworks.oer_single_site import OER_SingleSiteAnalyzer_FW 19 | 20 | 21 | def OERSingleSite_WF( 22 | oer_dict, 23 | slab, 24 | metal_site, 25 | slab_uuid, 26 | oriented_uuid, 27 | surface_termination, 28 | vasp_cmd=VASP_CMD, 29 | db_file=DB_FILE, 30 | surface_pbx_uuid="", 31 | ): 32 | """ 33 | Wrap-up workflow for OER single site (wna) + Reactivity Analysis 34 | 35 | Args: 36 | 37 | Returns: 38 | something 39 | """ 40 | # Empty lists 41 | oer_fws, oer_uuids = [], [] 42 | 43 | # Reduced formula 44 | general_reduced_formula = slab.composition.reduced_formula 45 | miller_index = "".join(list(map(str, slab.miller_index))) 46 | 47 | # Loop over OER intermediates 48 | for oer_inter_label, oer_inter in oer_dict.items(): 49 | oer_intermediate = Slab.from_dict(oer_inter) 50 | # reduced_formula = oer_intermediate.composition.reduced_formula 51 | name = ( 52 | f"{general_reduced_formula}-{miller_index}-{metal_site}-{oer_inter_label}" 53 | ) 54 | oer_inter_uuid = uuid.uuid4() 55 | oer_inter_fw = AdsSlab_FW( 56 | oer_intermediate, 57 | name=name, 58 | oriented_uuid=oriented_uuid, 59 | slab_uuid=slab_uuid, 60 | ads_slab_uuid=oer_inter_uuid, 61 | vasp_cmd=vasp_cmd, 62 | ) 63 | oer_fws.append(oer_inter_fw) 64 | oer_uuids.append(oer_inter_uuid) 65 | 66 | # Reactivity Analysis 67 | oer_fw = OER_SingleSiteAnalyzer_FW( 68 | reduced_formula=str(general_reduced_formula), 69 | miller_index=miller_index, 70 | metal_site=metal_site, 71 | name=f"{general_reduced_formula}-{miller_index}-{metal_site}-OER-Analysis", 72 | slab_uuid=slab_uuid, 73 | ads_slab_uuids=oer_uuids, 74 | surface_termination=surface_termination, 75 | parents=oer_fws, 76 | db_file=db_file, 77 | surface_pbx_uuid=surface_pbx_uuid, 78 | ) 79 | 80 | # Create the workflow 81 | all_fws = oer_fws + [oer_fw] 82 | oer_single_site = Workflow( 83 | all_fws, 84 | name=f"{general_reduced_formula}-{miller_index}-{metal_site}-OER SingleSite", 85 | ) 86 | return oer_single_site 87 | -------------------------------------------------------------------------------- /WhereWulff/workflows/slab_ads.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from fireworks import Firework, Workflow 11 | from atomate.vasp.config import VASP_CMD, DB_FILE 12 | 13 | from WhereWulff.firetasks.slab_ads import SlabAdsFireTask 14 | 15 | 16 | def SlabAds_WF( 17 | bulk_structure, 18 | adsorbates, 19 | parents=None, 20 | vasp_cmd=VASP_CMD, 21 | db_file=DB_FILE, 22 | metal_site="", 23 | applied_potential=1.60, 24 | applied_pH=0, 25 | ): 26 | """ 27 | Wrap-up workflow to do the Wulff Shape Analysis after MO_SLABS_WF. 28 | 29 | Args: 30 | bulk_structure (Structure): Bulk structure to refer the wulff shape 31 | vasp_cmd: vasp executable 32 | db_file: database file. 33 | 34 | Returns: 35 | JSON file with Wulff Analysis. 36 | """ 37 | # Bulk structure formula 38 | bulk_formula = bulk_structure.composition.reduced_formula 39 | 40 | # WulffShape Analysis 41 | ads_slab_fw = Firework( 42 | SlabAdsFireTask( 43 | bulk_structure=bulk_structure, 44 | reduced_formula=bulk_formula, 45 | adsorbates=adsorbates, 46 | slabs=None, 47 | vasp_cmd=vasp_cmd, 48 | db_file=db_file, 49 | metal_site=metal_site, 50 | applied_potential=applied_potential, 51 | applied_pH=applied_pH, 52 | ), 53 | name=f"{bulk_formula} Ads_slab optimization", 54 | parents=parents, 55 | ) 56 | 57 | all_fws = [ads_slab_fw] 58 | if parents is not None: 59 | all_fws.extend(parents) 60 | ads_wf = Workflow(all_fws, name="{} Ads_slab optimizations".format(bulk_formula)) 61 | return ads_wf, all_fws 62 | -------------------------------------------------------------------------------- /WhereWulff/workflows/static_bulk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from fireworks import Firework, Workflow 11 | from atomate.vasp.config import VASP_CMD, DB_FILE 12 | 13 | from WhereWulff.firetasks.static_bulk import StaticBulkFireTask 14 | from WhereWulff.analysis.bulk_stability import BulkStabilityAnalysis 15 | 16 | 17 | def StaticBulk_WF(bulk_structure, parents=None, vasp_cmd=VASP_CMD, db_file=DB_FILE): 18 | """ 19 | Wrap-up workflow to do the Static DFT calculation after EOS Fitting. 20 | 21 | Args: 22 | 23 | Returns: 24 | 25 | """ 26 | # Bulk structure formula 27 | bulk_formula = bulk_structure.composition.reduced_formula 28 | 29 | # StaticBulk for NM, FM and AFM fittings 30 | bulk_static_fw = Firework( 31 | StaticBulkFireTask( 32 | reduced_formula=bulk_formula, bulks=None, vasp_cmd=vasp_cmd, db_file=db_file 33 | ), 34 | name=f"{bulk_formula} Static_Bulk DFT Energy", 35 | parents=parents, 36 | ) 37 | 38 | all_fws = [bulk_static_fw] 39 | if parents is not None: 40 | all_fws.extend(parents) 41 | ads_wf = Workflow(all_fws, name=f"{bulk_formula} Static_Bulk DFT Energy") 42 | return ads_wf, all_fws 43 | -------------------------------------------------------------------------------- /WhereWulff/workflows/surface_energy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from pymatgen.core.surface import Slab 11 | 12 | from fireworks import Firework, Workflow 13 | from atomate.vasp.config import VASP_CMD, DB_FILE 14 | 15 | from WhereWulff.dft_settings.settings import MOSurfaceSet 16 | from WhereWulff.firetasks.surface_energy import SurfaceEnergyFireTask 17 | from WhereWulff.fireworks.optimize import Bulk_FW, Slab_FW 18 | 19 | 20 | def SurfaceEnergy_WF( 21 | slab, include_bulk_opt=True, vasp_input_set=None, vasp_cmd=VASP_CMD, db_file=DB_FILE 22 | ): 23 | """ 24 | Gets a workflow corresponding to a slab optimization calculation. 25 | 26 | Args: 27 | slab (Slab or Structures): Slab model to calculate. 28 | include_bulk_opt (default: True): Oriented bulk for surface energy calculation. 29 | vasp_input_set (default: MOSurfaceSet): User settings instead of default. 30 | vasp_cmd: vasp executable. 31 | db_file: database file. 32 | 33 | Return: 34 | Worflow, which consist in oriented bulk + slab model. 35 | """ 36 | fws, parents = [], [] 37 | miller_index = "".join([str(x) for x in slab.miller_index]) 38 | 39 | # Add bulk opt firework if specified 40 | if include_bulk_opt: 41 | oriented_bulk = slab.oriented_unit_cell 42 | name_bulk = "{}_{} bulk optimization".format( 43 | oriented_bulk.composition.reduced_formula, miller_index 44 | ) 45 | fws.append( 46 | Bulk_FW(oriented_bulk, name=name_bulk, vasp_cmd=vasp_cmd, db_file=db_file) 47 | ) 48 | parents = fws[0] 49 | 50 | # Slab model Optimization 51 | name_slab = "{}_{} slab optimization".format( 52 | slab.composition.reduced_formula, miller_index 53 | ) 54 | slab_fw = Slab_FW( 55 | slab, 56 | name=name_slab, 57 | parents=parents, 58 | vasp_cmd=vasp_cmd, 59 | db_file=db_file, 60 | add_slab_metadata=True, 61 | ) 62 | 63 | fws.append(slab_fw) 64 | 65 | # Surface Energy Calculation 66 | parents = fws[1:] 67 | name_gamma = "{}_{} surface energy".format( 68 | slab.composition.reduced_formula, miller_index 69 | ) 70 | gamma_hkl = Firework( 71 | SurfaceEnergyFireTask( 72 | slab_formula=slab.composition.reduced_formula, 73 | miller_index=miller_index, 74 | db_file=db_file, 75 | to_db=True, 76 | ), 77 | name=name_gamma, 78 | parents=parents, 79 | ) 80 | fws.append(gamma_hkl) 81 | 82 | # WF name for bulk/slab optimization 83 | if isinstance(slab, Slab): 84 | name_wf = "{}_{} slab workflow".format( 85 | slab.composition.reduced_formula, miller_index 86 | ) 87 | else: 88 | name_wf = "{} slab workflow".format(slab.composition.reduced_formula) 89 | 90 | wf = Workflow(fws, name=name_wf) 91 | 92 | return wf 93 | -------------------------------------------------------------------------------- /WhereWulff/workflows/surface_pourbaix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | import uuid 10 | import numpy as np 11 | 12 | from pymatgen.core.periodic_table import Element 13 | 14 | from fireworks import Workflow 15 | from atomate.vasp.config import VASP_CMD, DB_FILE 16 | 17 | from WhereWulff.fireworks.optimize import AdsSlab_FW 18 | from WhereWulff.fireworks.surface_pourbaix import SurfacePBX_FW 19 | from WhereWulff.adsorption.MXide_adsorption import MXideAdsorbateGenerator 20 | from WhereWulff.workflows.oer import OER_WF 21 | 22 | 23 | # Angles list 24 | def get_angles(n_rotations=4): 25 | """Get angles like in the past""" 26 | angles = [] 27 | for i in range(n_rotations): 28 | deg = (2 * np.pi / n_rotations) * i 29 | angles.append(deg) 30 | return angles 31 | 32 | 33 | def add_adsorbates(adslab, ads_coords, molecule, z_offset=[0, 0, 0.15]): 34 | """Add molecule in all ads_coords once""" 35 | translated_molecule = molecule.copy() 36 | for ads_site in ads_coords: 37 | for mol_site in translated_molecule: 38 | new_coord = ads_site + (mol_site.coords - z_offset) 39 | adslab.append( 40 | mol_site.specie, 41 | new_coord, 42 | coords_are_cartesian=True, 43 | properties=mol_site.properties, 44 | ) 45 | return adslab 46 | 47 | 48 | # Try the clockwise thing again... 49 | def get_clockwise_rotations(slab_ref, slab, molecule): 50 | """We need to rush function...""" 51 | # This will be a inner method 52 | mxidegen = MXideAdsorbateGenerator( 53 | slab_ref, 54 | repeat=[1, 1, 1], 55 | verbose=False, 56 | positions=["MX_adsites"], 57 | relax_tol=0.025, 58 | ) 59 | 60 | # Getting the bulk-like adsites on the original slab 61 | bulk_like, _ = mxidegen.get_bulk_like_adsites() 62 | # bulk_like_sites = mxidegen._filter_clashed_sites(bulk_like) # is needed? 63 | 64 | # Bondlength and X 65 | # bondlength, X = mxidegen.bondlength, mxidegen.X 66 | # bulk_like_shifted = _bulk_like_adsites_perturbation( 67 | # slab_ref, slab, bulk_like_sites, bondlength=bondlength, X=X 68 | # ) 69 | 70 | _, X = mxidegen.bondlengths_dict, mxidegen.X 71 | bulk_like_shifted = _bulk_like_adsites_perturbation(slab_ref, slab, bulk_like, X=X) 72 | 73 | # set n_rotations to 1 if mono-atomic 74 | n = len(molecule[0]) if type(molecule).__name__ == "list" else len(molecule) 75 | n_rotations = 1 if n == 1 else 4 76 | 77 | # Angles 78 | angles = get_angles(n_rotations=n_rotations) 79 | 80 | # Molecule formula 81 | molecule_comp = molecule.composition.as_dict() 82 | molecule_formula = "".join(molecule_comp.keys()) 83 | 84 | # rotate OH 85 | molecule_rotations = mxidegen.get_transformed_molecule_MXides( 86 | molecule, axis=[0, 0, 1], angles_list=angles 87 | ) 88 | 89 | # placement 90 | adslab_dict = {} 91 | for rot_idx in range(len(molecule_rotations)): 92 | slab_ads = slab.copy() 93 | slab_ads = add_adsorbates( 94 | slab_ads, bulk_like_shifted, molecule_rotations[rot_idx] 95 | ) 96 | slab_ads.sort() # Sorting for input/output consistency 97 | adslab_dict.update({"{}_{}".format(molecule_formula, rot_idx + 1): slab_ads}) 98 | 99 | return adslab_dict, bulk_like_shifted 100 | 101 | 102 | def _bulk_like_adsites_perturbation_bondlength( 103 | slab_ref, slab, bulk_like_sites, bondlength, X 104 | ): 105 | """Let's perturb bulk_like_sites with delta (x,y,z) comparing input and output""" 106 | slab_ref_coords = slab_ref.cart_coords 107 | slab_coords = slab.cart_coords 108 | 109 | delta_coords = slab_coords - slab_ref_coords 110 | 111 | metal_idx = [] 112 | for bulk_like_site in bulk_like_sites: 113 | for idx, site in enumerate(slab_ref): 114 | if ( 115 | site.specie != Element(X) 116 | and site.coords[2] > slab_ref.center_of_mass[2] 117 | ): 118 | dist = np.linalg.norm(bulk_like_site - site.coords) 119 | if dist < bondlength: 120 | metal_idx.append(idx) 121 | 122 | bulk_like_deltas = [delta_coords[i] for i in metal_idx] 123 | return [n + m for n, m in zip(bulk_like_sites, bulk_like_deltas)] 124 | 125 | 126 | def _bulk_like_adsites_perturbation(slab_ref, slab, bulk_like_sites, X): 127 | """Let's perturb bulk_like_sites with delta (x,y,z) comparing input and output""" 128 | slab_ref_coords = slab_ref.cart_coords 129 | slab_coords = slab.cart_coords 130 | 131 | delta_coords = slab_coords - slab_ref_coords 132 | 133 | metal_idx = [] 134 | for bulk_like_site in bulk_like_sites: 135 | min_dist = np.inf # intialize min_dist register 136 | min_metal_idx = 0 137 | end_idx = np.where(slab_ref.frac_coords[:, 2] >= slab_ref.center_of_mass[2])[0][ 138 | -1 139 | ] 140 | # FIXME: I think we can make this faster by replacing with a while loop 141 | # and only looping over the top half 142 | for idx, site in enumerate(slab_ref): 143 | if ( 144 | site.specie != Element(X) 145 | and site.frac_coords[2] 146 | > slab_ref.center_of_mass[2] # go over the top half of slab 147 | ): 148 | dist = np.linalg.norm(bulk_like_site - site.coords) 149 | 150 | if dist < min_dist: 151 | min_dist = dist 152 | min_metal_idx = idx 153 | 154 | if idx == end_idx: 155 | metal_idx.append(min_metal_idx) 156 | 157 | bulk_like_deltas = [delta_coords[i] for i in metal_idx] 158 | return [n + m for n, m in zip(bulk_like_sites, bulk_like_deltas)] 159 | 160 | 161 | def SurfacePBX_WF( 162 | bulk_structure, 163 | slab, 164 | slab_orig, 165 | slab_uuid, 166 | oriented_uuid, 167 | adsorbates, 168 | vasp_cmd=VASP_CMD, 169 | db_file=DB_FILE, 170 | metal_site="", 171 | applied_potential=1.60, 172 | applied_pH=0.0, 173 | ): 174 | """ 175 | Wrap-up Workflow for surface-OH/Ox terminated + SurfacePBX Analysis. 176 | 177 | Args: 178 | 179 | Retruns: 180 | something 181 | """ 182 | # Empty list of fws 183 | hkl_fws, hkl_uuids = [], [] 184 | 185 | # Reduced formula and Miller_index 186 | reduced_formula = slab.composition.reduced_formula 187 | slab_miller_index = "".join(list(map(str, slab.miller_index))) 188 | 189 | # Generate a set of OptimizeFW additons that will relax all the adslab in parallel 190 | ads_slab_orig = {} 191 | for adsorbate in adsorbates: 192 | adslabs, bulk_like_shifted = get_clockwise_rotations(slab_orig, slab, adsorbate) 193 | for adslab_label, adslab in adslabs.items(): 194 | name = ( 195 | f"{slab.composition.reduced_formula}-{slab_miller_index}-{adslab_label}" 196 | ) 197 | ads_slab_uuid = str(uuid.uuid4()) 198 | ads_slab_fw = AdsSlab_FW( 199 | adslab, 200 | name=name, 201 | oriented_uuid=oriented_uuid, 202 | slab_uuid=slab_uuid, 203 | ads_slab_uuid=ads_slab_uuid, 204 | vasp_cmd=vasp_cmd, 205 | db_file=db_file, 206 | ) 207 | ads_slab_orig.update({adslab_label: adslab}) 208 | hkl_fws.append(ads_slab_fw) 209 | hkl_uuids.append(ads_slab_uuid) 210 | 211 | # Surface PBX Diagram for each surface orientation 212 | surface_pbx_uuid = str(uuid.uuid4()) 213 | pbx_name = f"Surface-PBX-{slab.composition.reduced_formula}-{slab_miller_index}" 214 | pbx_fw = SurfacePBX_FW( 215 | reduced_formula=reduced_formula, 216 | name=pbx_name, 217 | miller_index=slab_miller_index, 218 | slab_uuid=slab_uuid, 219 | oriented_uuid=oriented_uuid, 220 | ads_slab_uuids=hkl_uuids, 221 | parents=hkl_fws, 222 | surface_pbx_uuid=surface_pbx_uuid, 223 | ) 224 | 225 | # OER workflow 226 | oer_fw = OER_WF( 227 | bulk_structure=bulk_structure, 228 | miller_index=slab_miller_index, 229 | slab_orig=slab_orig, 230 | bulk_like_sites=bulk_like_shifted, 231 | ads_dict_orig=ads_slab_orig, 232 | metal_site=metal_site, 233 | applied_potential=applied_potential, 234 | applied_pH=applied_pH, 235 | parents=[pbx_fw], 236 | vasp_cmd=VASP_CMD, 237 | db_file=DB_FILE, 238 | surface_pbx_uuid=surface_pbx_uuid, 239 | ) 240 | 241 | # Create the workflow 242 | all_fws = hkl_fws + [pbx_fw] + [oer_fw] 243 | oer_wf = Workflow( 244 | all_fws, 245 | name=f"{slab.composition.reduced_formula}-{slab_miller_index}-PBX Workflow", 246 | ) 247 | return oer_wf 248 | -------------------------------------------------------------------------------- /WhereWulff/workflows/wulff_shape.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from __future__ import absolute_import, division, print_function, unicode_literals 9 | 10 | from fireworks import Firework, Workflow 11 | from atomate.vasp.config import VASP_CMD, DB_FILE 12 | 13 | from WhereWulff.analysis.wulff_shape import WulffShapeFW 14 | 15 | 16 | def WulffShape_WF(bulk_structure, parents=None, vasp_cmd=VASP_CMD, db_file=DB_FILE): 17 | """ 18 | Wrap-up workflow to do the Wulff Shape Analysis after MO_SLABS_WF. 19 | 20 | Args: 21 | bulk_structure (Structure): Bulk structure to refer the wulff shape 22 | vasp_cmd: vasp executable 23 | db_file: database file. 24 | 25 | Returns: 26 | JSON file with Wulff Analysis. 27 | """ 28 | # Bulk structure formula 29 | bulk_formula = bulk_structure.composition.reduced_formula 30 | 31 | # WulffShape Analysis 32 | wulff_fw = Firework( 33 | WulffShapeFW(bulk_structure=bulk_structure, db_file=db_file), 34 | name="{} wulff shape Task".format(bulk_formula), 35 | parents=parents, 36 | ) 37 | 38 | all_fws = [wulff_fw] 39 | if parents is not None: 40 | all_fws.extend(parents) 41 | wulff_wf = Workflow(all_fws, name="{} wulff shape analysis".format(bulk_formula)) 42 | return wulff_wf, all_fws 43 | -------------------------------------------------------------------------------- /img/wherewulff_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulissigroup/wherewulff/b58b15a011aec8a1d6cdc96128751add4973b18c/img/wherewulff_img.png -------------------------------------------------------------------------------- /img/wherewulff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulissigroup/wherewulff/b58b15a011aec8a1d6cdc96128751add4973b18c/img/wherewulff_logo.png -------------------------------------------------------------------------------- /main_bulk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from WhereWulff.launchers.bulkflows import BulkFlows 9 | 10 | # Import CIF file 11 | cif_file = "<>" 12 | 13 | # BulkFlow method and config 14 | bulk_flow = BulkFlows(cif_file) 15 | 16 | # Get Launchpad 17 | launchpad = bulk_flow.submit( 18 | hostname="localhost", 19 | db_name="<>", 20 | port="<>", 21 | username="<>", 22 | password="<>", 23 | ) 24 | -------------------------------------------------------------------------------- /main_slab.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from WhereWulff.launchers.slabflows import SlabFlows 9 | 10 | 11 | # Import CIF file 12 | cif_file = "<>" 13 | 14 | # CatFlows method and config 15 | cat_flows = SlabFlows(cif_file, exclude_hkl=[(1, 0, 0), (1, 1, 1), (0, 0, 1)]) 16 | 17 | # Get Launchpad 18 | launchpad = cat_flows.submit( 19 | hostname="localhost", 20 | db_name="<>", 21 | port="<>", 22 | username="<>", 23 | password="<>", 24 | ) 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 Carnegie Mellon University. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | """ 7 | 8 | from setuptools import find_packages, setup 9 | 10 | setup( 11 | name="wherewulff", 12 | version="0.0.1", 13 | description="WhereWulff: An Automated Workflow to Democratize and Scale Complex Material Discovery for Electrocatalysis.", 14 | url="https://github.com/ulissigroup/wherewulff", 15 | packages=find_packages(), 16 | include_package_data=True, 17 | license="MIT", 18 | ) 19 | -------------------------------------------------------------------------------- /wherewulff_env.yml: -------------------------------------------------------------------------------- 1 | name: wherewulff 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.8 7 | - numpy 8 | - pandas 9 | - matplotlib 10 | - pymatgen 11 | - enumlib 12 | - phonopy 13 | - fireworks 14 | - custodian 15 | - atomate 16 | - mongodb 17 | - pymongo==3.12.0 18 | - magma 19 | - pymatgen-db 20 | --------------------------------------------------------------------------------