├── .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 | [](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 |
--------------------------------------------------------------------------------