├── LICENSE.rst ├── README.md ├── interface_stability ├── __init__.py ├── pseudobinary.py ├── scripts │ ├── __init__.py │ ├── __init__.pyc │ ├── phase_stability.py │ └── pseudo_binary.py ├── singlephase.py └── tests │ ├── __init__.py │ └── test_interface_stability.py └── setup.py /LICENSE.rst: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright (c) 2018 UMD 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 9 | the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 12 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 14 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 15 | SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## interface_stability 2 | 3 | This package and associated scripts are designed to analyze the interface stability. 4 | 5 | ## Citing 6 | 7 | If you use this library, please consider citing the following papers: 8 | 9 | Zhu, Yizhou, Xingfeng He, and Yifei Mo. "Origin of outstanding stability in the lithium solid electrolyte materials: insights from thermodynamic analyses based on first-principles calculations." ACS applied materials & interfaces 7.42 (2015): 23685-23693. 10 | 11 | DOI: 10.1021/acsami.5b07517 12 | 13 | Zhu, Yizhou, Xingfeng He, and Yifei Mo. "First principles study on electrochemical and chemical stability of solid electrolyte–electrode interfaces in all-solid-state Li-ion batteries." Journal of Materials Chemistry A 4.9 (2016): 3253-3266. 14 | 15 | DOI: 10.1039/C5TA08574H 16 | 17 | Han, Fudong, et al. "Electrochemical stability of Li10GeP2S12 and Li7La3Zr2O12 solid electrolytes." Advanced Energy Materials 6.8 (2016). 18 | 19 | DOI: 10.1002/aenm.201501590 20 | 21 | Ping Ong, Shyue, et al. "Li− Fe− P− O2 phase diagram from first principles calculations." Chemistry of Materials 20.5 (2008): 1798-1807. 22 | 23 | DOI: 10.1021/cm702327g 24 | 25 | Mo, Yifei, Shyue Ping Ong, and Gerbrand Ceder. "First principles study of the Li10GeP2S12 lithium super ionic conductor material." Chemistry of Materials 24.1 (2011): 15-17. 26 | 27 | DOI: 10.1021/cm203303y 28 | 29 | Jain, Anubhav, et al. "Commentary: The Materials Project: A materials genome approach to accelerating materials innovation." Apl Materials 1.1 (2013): 011002. 30 | 31 | DOI:10.1063/1.4812323 32 | 33 | ## Install 34 | 35 | This package works with both Python 2.7+ and Python 3.x. However, it is suggested to use Python 3.x, as the dependent package Pymatgen will be py3k only in the future. 36 | 37 | 38 | 1. Install all dependency packages 39 | 40 | Use your favorite way to install [pymatgen](http://pymatgen.org/) first. 41 | 42 | 2. install this interface_stability package. 43 | 44 | clone this package from github 45 | 46 | ```bash 47 | $ git clone https://github.com/mogroupumd/interface_stability.git 48 | ``` 49 | 50 | install the package 51 | ```bash 52 | $ python setup.py install --user 53 | ``` 54 | 55 | 3. Try to import python classes in your python console 56 | 57 | ```bash 58 | $ python 59 | >>> from interface_stability import singlephase 60 | ``` 61 | 62 | 4. The setup.py will automatically create an executable file phase_stability and pseudo_binary 63 | into your PATH. Try to call it from terminal and read the documentations: 64 | 65 | ```bash 66 | $ phase_stability -h 67 | $ pseudo_binary -h 68 | ``` 69 | 70 | 5. Setup Materials Project API key. This enables you to fetch data from the Materials Project database. 71 | 72 | The documentation is at 73 | https://materialsproject.org/open 74 | 75 | Your API key is at (login required) 76 | https://materialsproject.org/dashboard 77 | 78 | You need to put the API key in ~/.pmgrc.yaml file. (Create this file if not exist) 79 | ```bash 80 | PMG_MAPI_KEY: [Your API key goes here] 81 | ``` 82 | 83 | ## Usage 84 | 85 | There are two executable python scripts, both work with a few sub-commands options. 86 | You can always use -h to see the help information. 87 | For example, 88 | 89 | ```bash 90 | $ phase_stability -h 91 | $ phase_stability evolution -h 92 | ``` 93 | 94 | ### 1. scripts/phase_stability.py 95 | 96 | **phase_stability stability composition** 97 | 98 | This gives the phase equilibria of a given composition. 99 | 100 | ```bash 101 | $ phase_stability stability Li10GeP2S12 102 | ------------------------------------------------------------ 103 | Reduced formula of the given composition: Li10Ge(PS6)2 104 | Calculated phase equilibria: Li4GeS4 Li3PS4 105 | Li10Ge(PS6)2 -> 2 Li3PS4 + Li4GeS4 106 | ------------------------------------------------------------ 107 | ``` 108 | 109 | **phase_stability mu composition open_element chemical_potential** 110 | 111 | This gives the phase equilibria of a given composition under given chemical potential. 112 | 113 | Note: Chemical potential is always referenced to elementary phases and in eV units. 114 | 115 | ```bash 116 | $ phase_stability mu Li3PS4 Li -5 117 | ------------------------------------------------------------ 118 | Reduced formula of the given composition: Li3PS4 119 | Open element : Li 120 | Chemical potential: -5 eV referenced to pure phase 121 | ------------------------------------------------------------ 122 | Reaction:Li3PS4 -> 3 Li + 0.5 S + 0.5 P2S7 123 | Reaction energy: -13.465 eV per Li3PS4 124 | ------------------------------------------------------------ 125 | ``` 126 | **phase_stability evolution [-posmu] composition open_element** 127 | 128 | This gives the evolution profile with changing chemical potential of an open element. 129 | 130 | A figure of reaction energy will also be generated 131 | 132 | ```bash 133 | $ phase_stability evolution Li3PS4 Li 134 | ------------------------------------------------------------ 135 | Reduced formula of the given composition: Li3PS4 136 | 137 | === Evolution Profile === 138 | mu_high (eV) mu_low (eV) d(n_Li) Phase equilibria Reaction 139 | 0.00 -0.87 8.00 Li2S, Li3P Li3PS4 + 8 Li -> Li3P + 4 Li2S 140 | -0.87 -0.93 6.00 Li2S, LiP Li3PS4 + 6 Li -> LiP + 4 Li2S 141 | -0.93 -1.17 5.43 Li2S, Li3P7 Li3PS4 + 5.429 Li -> 0.1429 Li3P7 + 4 Li2S 142 | -1.17 -1.30 5.14 Li2S, LiP7 Li3PS4 + 5.143 Li -> 0.1429 LiP7 + 4 Li2S 143 | -1.30 -1.72 5.00 Li2S, P Li3PS4 + 5 Li -> 4 Li2S + P 144 | -1.72 -2.36 0.00 Li3PS4 Li3PS4 -> Li3PS4 145 | -2.36 -3.74 -2.88 LiS4, P2S7 Li3PS4 -> 2.875 Li + 0.125 LiS4 + 0.5 P2S7 146 | -3.74 -inf -3.00 P2S7, S Li3PS4 -> 3 Li + 0.5 S + 0.5 P2S7 147 | 148 | === Reaction energy === 149 | miu_Li (eV) Rxn energy (eV/atom) 150 | 0.00 -1.42 151 | -0.87 -0.55 152 | -0.93 -0.50 153 | -1.17 -0.35 154 | -1.30 -0.26 155 | -1.72 0.00 156 | -2.36 -0.00 157 | -3.74 -0.50 158 | -3.94 -0.57 159 | Note: 160 | Chemical potential referenced to element phase. 161 | Reaction energy is normalized to per atom of the given composition. 162 | ------------------------------------------------------------ 163 | ``` 164 | **phase_stability plotvc [-posmu] [-v VALENCE] composition open_element** 165 | 166 | Generate a figure of voltage profile (and display all raw data). 167 | 168 | ```bash 169 | $ phase_stability plotvc Li3PS4 Li 170 | d n(Li) Voltage ref. to Li (V) 171 | -3.00 3.74 172 | -2.88 3.74 173 | -2.88 2.36 174 | 0.00 2.36 175 | 0.00 1.72 176 | 5.00 1.72 177 | 5.00 1.30 178 | 5.14 1.30 179 | 5.14 1.17 180 | 5.43 1.17 181 | 5.43 0.93 182 | 6.00 0.93 183 | 6.00 0.87 184 | 8.00 0.87 185 | ``` 186 | 187 | ### 2. scripts/pseudo_binary.py 188 | 189 | **pseudo_binary pd composition_1 composition_2** 190 | 191 | This is used to calculate the chemical stability of two phases. 192 | 193 | The minimum point is marked in the comment column 194 | 195 | ```bash 196 | $ pseudo_binary pd LiCoO2 Li3PS4 197 | ---------------------------------------------------------------------------------------------------- 198 | The starting phases compositions are LiCoO2 and Li3PS4 199 | All mixing ratio based on all formula already normalized to ONE atom per fu! 200 | 201 | === Pseudo-binary evolution profile === 202 | x(Li3PS4) x(LiCoO2) Rxn. E. (meV/atom) Mutual Rxn. E. (meV/atom) Phase Equilibria Comment 203 | 1.00 0.00 -0.00 0.00 Li3PS4 204 | 0.50 0.50 -402.55 -402.55 CoS2, Co3S4, Li2S, Li3PO4 205 | 0.48 0.52 -405.94 -405.94 Co3S4, Li2S, Li2SO4, Li3PO4 206 | 0.41 0.59 -406.03 -406.03 Li2S, Li3PO4, Li2SO4, Co9S8 Minimum 207 | 0.34 0.66 -368.44 -368.44 Li3PO4, Li2O, Li2SO4, Co9S8 208 | 0.16 0.84 -237.56 -237.56 Co, Li2O, Li2SO4, Li3PO4 209 | 0.15 0.85 -233.60 -233.60 Co, Li2SO4, Li6CoO4, Li3PO4 210 | 0.06 0.94 -90.55 -90.55 CoO, Li3PO4, Li2SO4, Li6CoO4 211 | 0.00 1.00 0.00 0.00 LiCoO2 212 | ``` 213 | 214 | **pseudo_binary gppd composition_1 composition_2** 215 | 216 | This is used to calculate the chemical stability of two phases. 217 | 218 | The minimum point is marked in the comment column 219 | 220 | ```bash 221 | $ pseudo_binary pd LiCoO2 Li3PS4 222 | ---------------------------------------------------------------------------------------------------- 223 | The starting phases compositions are LiCoO2 and Li3PS4 224 | All mixing ratio based on all formula already normalized to ONE atom per fu! 225 | 226 | === Pseudo-binary evolution profile === 227 | x(Li3PS4) x(LiCoO2) Rxn. E. (meV/atom) Mutual Rxn. E. (meV/atom) Phase Equilibria Comment 228 | 1.00 0.00 -0.00 0.00 Li3PS4 229 | 0.50 0.50 -402.55 -402.55 CoS2, Co3S4, Li2S, Li3PO4 230 | 0.48 0.52 -405.94 -405.94 Co3S4, Li2S, Li2SO4, Li3PO4 231 | 0.41 0.59 -406.03 -406.03 Li2S, Li3PO4, Li2SO4, Co9S8 Minimum 232 | 0.34 0.66 -368.44 -368.44 Li3PO4, Li2O, Li2SO4, Co9S8 233 | 0.16 0.84 -237.56 -237.56 Co, Li2O, Li2SO4, Li3PO4 234 | 0.15 0.85 -233.60 -233.60 Co, Li2SO4, Li6CoO4, Li3PO4 235 | 0.06 0.94 -90.55 -90.55 CoO, Li3PO4, Li2SO4, Li6CoO4 236 | 0.00 1.00 0.00 0.00 LiCoO2 237 | (py3k) Yizhous-MBP:interface_stability yizhou$ pseudo_binary gppd LiCoO2 Li3PS4 Li -5 238 | ---------------------------------------------------------------------------------------------------- 239 | The starting phases compositions are LiCoO2 and Li3PS4 240 | All mixing ratio based on all formula already normalized to ONE atom per fu! 241 | Chemical potential is miu_Li = -5.0, using elementary phase as reference. 242 | ------------------------------------------------------------ 243 | 244 | === Pseudo-binary evolution profile === 245 | x(Li3PS4) x(LiCoO2) Rxn. E. (meV/atom) Mutual Rxn. E. (meV/atom) Phase Equilibria Comment 246 | 1.00 -0.00 -1,547.69 0.00 P2S7, S 247 | 0.99 0.01 -1,601.10 -68.74 CoS2, P2S7, S8O 248 | 0.58 0.42 -1,679.16 -606.19 CoS2, CoP4O11, S8O 249 | 0.55 0.45 -1,679.82 -631.65 Co(PO3)2, CoS2, S8O Rxn. E. Min. 250 | 0.41 0.59 -1,568.49 -677.13 Co(PO3)2, CoS2, CoSO4 251 | 0.33 0.67 -1,496.84 -695.56 CoS2, CoSO4, Co3(PO4)2 252 | 0.29 0.71 -1,458.93 -704.30 Co3S4, CoSO4, Co3(PO4)2 Mutual Rxn. E. Min. 253 | 0.26 0.74 -1,420.64 -704.18 CoSO4, Co3(PO4)2, Co9S8 254 | 0.12 0.88 -1,182.46 -618.68 CoO, CoSO4, Co3(PO4)2 255 | 0.10 0.90 -1,104.35 -569.65 Co3(PO4)2, CoSO4, Co3O4 256 | 0.09 0.91 -1,075.28 -545.43 CoSO4, Co3O4, CoPO4 257 | 0.00 1.00 -428.07 0.00 CoO2 258 | ``` 259 | 260 | ## License 261 | 262 | 263 | Python library interface_stability is released under the MIT License. The terms of the license are as 264 | follows: 265 | 266 | The MIT License (MIT) Copyright (c) 2018 UMD 267 | 268 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 269 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 270 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 271 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 272 | 273 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 274 | the Software. 275 | 276 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 277 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 278 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 279 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 280 | SOFTWARE. 281 | -------------------------------------------------------------------------------- /interface_stability/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogroupumd/interface_stability/2729cdf7bc6fea579c7aa95bae71c450bd6a7455/interface_stability/__init__.py -------------------------------------------------------------------------------- /interface_stability/pseudobinary.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) Mogroup @ University of Maryland, College Park 3 | # Distributed under the terms of the MIT License. 4 | 5 | import pandas 6 | from pymatgen import Composition, Element 7 | from pymatgen.analysis.phase_diagram import PhaseDiagram, GrandPotentialPhaseDiagram, GrandPotPDEntry 8 | from pymatgen.analysis.reaction_calculator import ComputedReaction, ReactionError 9 | from interface_stability.singlephase import VirtualEntry 10 | 11 | 12 | __author__ = "Yizhou Zhu" 13 | __copyright__ = "" 14 | __version__ = "2.2" 15 | __maintainer__ = "Yizhou Zhu" 16 | __email__ = "yizhou.zhu@gmail.com" 17 | __status__ = "Production" 18 | __date__ = "Jun 10, 2018" 19 | 20 | 21 | class PseudoBinary(object): 22 | """ 23 | A class for performing analyses on pseudo-binary stability calculations. 24 | 25 | The algorithm is based on the work in the following paper: 26 | 27 | Yizhou Zhu, Xingfeng He, Yifei Mo*, “First Principles Study on Electrochemical and Chemical Stability of the 28 | Solid Electrolyte-Electrode Interfaces in All-Solid-State Li-ion Batteries”, Journal of Materials Chemistry A, 4, 29 | 3253-3266 (2016) 30 | DOI: 10.1039/c5ta08574h 31 | """ 32 | 33 | def __init__(self, entry1, entry2, entries=None, sup_el=None): 34 | comp1 = entry1.composition 35 | comp2 = entry2.composition 36 | norm1 = 1.0 / entry1.composition.num_atoms 37 | norm2 = 1.0 / entry2.composition.num_atoms 38 | 39 | self.entry1 = VirtualEntry.from_composition(entry1.composition * norm1, energy=entry1.energy * norm1, 40 | name=comp1.reduced_formula) 41 | self.entry2 = VirtualEntry.from_composition(entry2.composition * norm2, energy=entry2.energy * norm2, 42 | name=comp2.reduced_formula) 43 | 44 | if not entries: 45 | entry_mix = VirtualEntry.from_composition(comp1 + comp2) 46 | entries = entry_mix.get_PD_entries(sup_el=sup_el) 47 | entries += [entry1, entry2] 48 | self.PDEntries = entries 49 | self.PD = PhaseDiagram(entries) 50 | 51 | def __eq__(self, other): 52 | return self.__dict__ == other.__dict__ 53 | 54 | def pd_mixing(self): 55 | """ 56 | This function give the phase equilibria of a pseudo-binary in a closed system (PD). 57 | It will give a complete evolution profile for mixing ratio x change from 0 to 1. 58 | x is the ratio (both entry norm. to 1 atom/fu) or each entry 59 | """ 60 | profile = get_full_evolution_profile(self.PD, self.entry1, self.entry2, 0.0, 1.0) 61 | cleaned = clean_profile(profile) 62 | return cleaned 63 | 64 | def get_printable_pd_profile(self): 65 | return self.get_printed_profile(self.pd_mixing()) 66 | 67 | def get_printable_gppd_profile(self, chempots, gppd_entries=None): 68 | return self.get_printed_profile(self.gppd_mixing(chempots, gppd_entries=gppd_entries)) 69 | 70 | def get_printed_profile(self, profile): 71 | """ 72 | A general function to generate printable table strings for pseudo-binary mixing results 73 | """ 74 | output = ['\n === Pseudo-binary evolution profile === '] 75 | df = pandas.DataFrame() 76 | rxn_e = [] 77 | mutual_rxn_e = [] 78 | E0 = -profile[0][1][1] 79 | E1 = -profile[-1][1][1] 80 | 81 | x1s, x2s, es, mes, pes = [], [], [], [], [] 82 | 83 | for item in profile: 84 | ratio, (decomp, e) = item 85 | x1s.append(1-ratio) 86 | x2s.append(ratio) 87 | es.append(-e*1000) 88 | mes.append((-e - ratio * E1 - (1 - ratio) * E0) * 1000) 89 | pes.append(", ".join([x.name for x in decomp])) 90 | rxn_e.append(-e) 91 | mutual_rxn_e.append(((-e - ratio * E1 - (1 - ratio) * E0) * 1000)) 92 | df["x({})".format(self.entry2.name)] = x1s 93 | df["x({})".format(self.entry1.name)] = x2s 94 | df["Rxn. E. (meV/atom)"] = es 95 | df["Mutual Rxn. E. (meV/atom)"] = mes 96 | df["Phase Equilibria"] = pes 97 | 98 | comments = ["" for _ in range(len(profile))] 99 | min_loc = list(df[df.columns[2:4]].idxmin()) 100 | if min_loc[0] == min_loc[1]: 101 | comments[min_loc[0]] = 'Minimum' 102 | else: 103 | comments[min_loc[0]] = 'Rxn. E. Min.' 104 | comments[min_loc[1]] = 'Mutual Rxn. E. Min.' 105 | df["Comment"] = comments 106 | 107 | print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 108 | output.append(print_df) 109 | string = '\n'.join(output) 110 | return string 111 | 112 | def gppd_mixing(self, chempots, gppd_entries=None): 113 | """ 114 | This function give the phase equilibria of a pseudo-binary in a open system (GPPD). 115 | It will give a complete evolution profile for mixing ratio x change from 0 to 1. 116 | x is the ratio (both entry norm. to 1 atom/fu(w/o open element) ) or each entry 117 | """ 118 | open_el = list(chempots.keys())[0] 119 | el_ref = VirtualEntry.get_mp_entry(open_el) 120 | chempots[open_el] = chempots[open_el] + el_ref.energy_per_atom 121 | gppd_entry1 = GrandPotPDEntry(self.entry1, {Element[_]: chempots[_] for _ in chempots}) 122 | gppd_entry2 = GrandPotPDEntry(self.entry2, {Element[_]: chempots[_] for _ in chempots}) 123 | if not gppd_entries: 124 | gppd_entries = self.get_gppd_entries(open_el) 125 | gppd = GrandPotentialPhaseDiagram(gppd_entries, chempots) 126 | profile = get_full_evolution_profile(gppd, gppd_entry1, gppd_entry2, 0, 1) 127 | cleaned = clean_profile(profile) 128 | return cleaned 129 | 130 | def get_gppd_entries(self, open_el): 131 | if open_el in (self.entry1.composition + self.entry2.composition).keys(): 132 | gppd_entries = self.PDEntries 133 | else: 134 | comp = self.entry1.composition + self.entry2.composition + Composition(open_el.symbol) 135 | gppd_entries = VirtualEntry.from_composition(comp).get_GPPD_entries(open_el) 136 | return gppd_entries 137 | 138 | def get_gppd_transition_chempots(self, open_el, gppd_entries=None): 139 | """ 140 | This is to get all possible transition chemical potentials from PD (rather than GPPD) 141 | Still use pure element ref. 142 | # May consider supporting negative miu in the future 143 | """ 144 | if not gppd_entries: 145 | gppd_entries = self.get_gppd_entries(open_el) 146 | pd = PhaseDiagram(gppd_entries) 147 | vaspref_mius = pd.get_transition_chempots(Element(open_el)) 148 | el_ref = VirtualEntry.get_mp_entry(open_el) 149 | 150 | elref_mius = [miu - el_ref.energy_per_atom for miu in vaspref_mius] 151 | return elref_mius 152 | 153 | def gppd_scanning(self, open_el, mu_hi, mu_lo, gppd_entries=None, verbose=False): 154 | """ 155 | This function is to do a (slightly smarter) screening of GPPD pseudo-binary in a given miu range 156 | This is a very tedious function, but mainly because GPPD screening itself is very tedious. 157 | 158 | :param open_el: open element 159 | :param mu_hi: chemical potential upper bound 160 | :param mu_lo: chemical potential lower bound 161 | :param gppd_entries: Supply GPPD entries manually. If you supply this, I assume you know what you are doing 162 | :param verbose: whether to prune the PE result table 163 | :return: a printable string of screening results 164 | """ 165 | mu_lo, mu_hi = sorted([mu_lo, mu_hi]) 166 | miu_E_candidates = [miu for miu in self.get_gppd_transition_chempots(open_el) if 167 | (miu - mu_lo) * (miu - mu_hi) <= 0] 168 | miu_E_candidates = [mu_hi] + miu_E_candidates + [mu_lo] 169 | duplicate_index = [] 170 | if not gppd_entries: 171 | gppd_entries = self.get_gppd_entries(open_el) 172 | 173 | for i in range(1, len(miu_E_candidates) - 1): 174 | miu_left = (miu_E_candidates[i] + miu_E_candidates[i - 1]) / 2.0 175 | miu_right = (miu_E_candidates[i] + miu_E_candidates[i + 1]) / 2.0 176 | profile_left = self.gppd_mixing({open_el: miu_left}, gppd_entries) 177 | profile_right = self.gppd_mixing({open_el: miu_right}, gppd_entries) 178 | if judge_same_decomp(profile_left, profile_right): 179 | duplicate_index.append(i) 180 | miu_E_candidates = [miu_E_candidates[i] for i in range(len(miu_E_candidates)) if i not in duplicate_index] 181 | 182 | mu_hi, miu_low, PE = [], [], [] 183 | mu_list, E_mutual_list, E_total_list = [], [], [] 184 | 185 | for i in range(1, len(miu_E_candidates)): 186 | miu = (miu_E_candidates[i] + miu_E_candidates[i - 1]) / 2.0 187 | profile = self.gppd_mixing({open_el: miu}, gppd_entries) 188 | E0 = -profile[0][1][1] 189 | E1 = -profile[-1][1][1] 190 | min_mutual = min(profile, key=lambda step: (-step[1][1] - step[0] * E1 - (1 - step[0]) * E0)) 191 | mu_hi.append(miu_E_candidates[i - 1]) 192 | miu_low.append(miu_E_candidates[i]) 193 | PE.append(", ".join(sorted([x.name for x in min_mutual[1][0]]))) 194 | for i in range(len(miu_E_candidates)): 195 | profile_transition = self.gppd_mixing({open_el: miu_E_candidates[i]}, gppd_entries) 196 | E0 = -profile_transition[0][1][1] 197 | E1 = -profile_transition[-1][1][1] 198 | min_mutual_transition = min(profile_transition, 199 | key=lambda step: (-step[1][1] - step[0] * E1 - (1 - step[0]) * E0)) 200 | mu_list.append(miu_E_candidates[i]) 201 | 202 | E_mutual_list.append( 203 | (-min_mutual_transition[1][1] - min_mutual_transition[0] * E1 - (1 - min_mutual_transition[0]) * E0)) 204 | E_total_list.append(-min_mutual_transition[1][1]) 205 | 206 | to_be_hidden = [] 207 | if not verbose: 208 | for i in range(1, len(PE)): 209 | if PE[i] == PE[i - 1]: 210 | to_be_hidden.append(i) 211 | 212 | mu_hi_display_list = [mu_hi[k] for k in range(len(mu_hi)) if k not in to_be_hidden] 213 | mu_low_display_list = mu_hi_display_list[1:] + [mu_lo] 214 | PE_display_list = [PE[k] for k in range(len(PE)) if k not in to_be_hidden] 215 | 216 | df1 = pandas.DataFrame() 217 | df2 = pandas.DataFrame() 218 | 219 | df1['mu_low'] = mu_hi_display_list 220 | df1['mu_high'] = mu_low_display_list 221 | df1['phase equilibria'] = PE_display_list 222 | 223 | df2['mu'] = mu_list 224 | df2['E_mutual(eV/atom)'] = E_mutual_list 225 | df2['E_total(eV/atom)'] = E_total_list 226 | 227 | print_df1 = df1.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 228 | print_df2 = df2.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 229 | 230 | output = [' == Phase Equilibria at min E_mutual == ', print_df1,'\n', ' == Reaction Energy ==', 231 | print_df2, 'Note: if E_mutual = 0, E_total is at x = 1 or 0'] 232 | string = "\n".join(output) 233 | return string 234 | 235 | 236 | """ 237 | The following functions are auxiliary functions. 238 | Most of them are used to solve or clean the mixing PE profile. 239 | """ 240 | 241 | 242 | def judge_same_decomp(profile1, profile2): 243 | """ 244 | Judge whether two profiles have identical decomposition products 245 | """ 246 | if len(profile1) != len(profile2): 247 | return False 248 | for step in range(len(profile1)): 249 | ratio1, (decomp1, e1) = profile1[step] 250 | ratio2, (decomp2, e1) = profile2[step] 251 | if abs(ratio1 - ratio2) > 1e-8: 252 | return False 253 | names1 = sorted([x.name for x in decomp1]) 254 | names2 = sorted([x.name for x in decomp2]) 255 | if names1 != names2: 256 | return False 257 | return True 258 | 259 | 260 | def get_full_evolution_profile(pd, entry1, entry2, x1, x2): 261 | """ 262 | This function is used to solve the transition points along a path on convex hull. 263 | The essence is to use binary search, which is more accurate and faster than brutal force screening 264 | This is a recursive function. 265 | :param pd: PhaseDiagram of GrandPotentialPhaseDiagram 266 | :param entry1 & entry2: mixing entry1/entry2, PDEntry for pd_mixing, GrandPotEntry for gppd_mixing 267 | :param x1 & x2: The mixing ratio range for binary search. 268 | :return: An uncleaned but complete profile with all transition points. 269 | """ 270 | evolution_profile = {} 271 | entry_left = get_mix_entry({entry1: x1, entry2: 1 - x1}) 272 | entry_right = get_mix_entry({entry1: x2, entry2: 1 - x2}) 273 | (decomp1, h1) = pd.get_decomp_and_e_above_hull(entry_left) 274 | (decomp2, h2) = pd.get_decomp_and_e_above_hull(entry_right) 275 | decomp1 = set(decomp1.keys()) 276 | decomp2 = set(decomp2.keys()) 277 | evolution_profile[x1] = (decomp1, h1) 278 | evolution_profile[x2] = (decomp2, h2) 279 | 280 | if decomp1 == decomp2: 281 | return evolution_profile 282 | 283 | intersect = decomp1 & decomp2 284 | if len(intersect) > 0: 285 | # This is try to catch a single transition point 286 | try: 287 | rxn = ComputedReaction([entry_left, entry_right], list(intersect)) 288 | if not {entry_left, entry_right} < set(rxn.all_entries): 289 | return evolution_profile 290 | 291 | c1 = rxn.coeffs[rxn.all_entries.index(entry_left)] 292 | c2 = rxn.coeffs[rxn.all_entries.index( 293 | entry_right)] # I know this is tedious but this is the only way I found that works.. 294 | x = (c1 * x1 + c2 * x2) / (c1 + c2) 295 | if c1 * c2 == 0: 296 | return evolution_profile 297 | entry_mid = VirtualEntry.from_mixing({entry_left: c1 / (c1 + c2), entry_right: c2 / (c1 + c2)}) 298 | h_mid = pd.get_decomp_and_e_above_hull(entry_mid)[1] 299 | evolution_profile[x] = (intersect, h_mid) 300 | return evolution_profile 301 | except ReactionError: 302 | pass 303 | 304 | x_mid = (x1 + x2) / 2.0 305 | entry_mid = get_mix_entry({entry1: 0.5, entry2: 0.5}) 306 | (decomp_mid, h_mid) = pd.get_decomp_and_e_above_hull(entry_mid) 307 | decomp_mid = set(decomp_mid.keys()) 308 | evolution_profile[x_mid] = (decomp_mid, h_mid) 309 | part1 = get_full_evolution_profile(pd, entry1, entry2, x1, x_mid) 310 | part2 = get_full_evolution_profile(pd, entry1, entry2, x_mid, x2) 311 | evolution_profile.update(part1) 312 | evolution_profile.update(part2) 313 | return evolution_profile 314 | 315 | 316 | def clean_profile(evolution_profile): 317 | """ 318 | This function is to clean the calculated profile from binary search. Redundant trial results are pruned out, 319 | with only transition points left. 320 | """ 321 | raw_data = list(evolution_profile.items()) 322 | raw_data.sort() 323 | clean_set = [raw_data[0]] 324 | for i in range(1, len(raw_data)): 325 | x, (decomp, h) = raw_data[i] 326 | x_cpr, (decomp_cpr, h_cpr) = clean_set[-1] 327 | if set(decomp_cpr) <= set(decomp): 328 | continue 329 | else: 330 | clean_set.append(raw_data[i]) 331 | return clean_set 332 | 333 | 334 | def get_mix_entry(mix_dict): 335 | """ 336 | Mixing PDEntry or GrandPotEntry for the binary search algorithm. 337 | """ 338 | entry1, entry2 = mix_dict.keys() 339 | x1, x2 = mix_dict[entry1], mix_dict[entry2] 340 | if type(entry1) == GrandPotPDEntry: 341 | mid_ori_entry = VirtualEntry.from_mixing({entry1.original_entry: x1, entry2.original_entry: x2}) 342 | return GrandPotPDEntry(mid_ori_entry, entry1.chempots) 343 | else: 344 | return VirtualEntry.from_mixing({entry1: x1, entry2: x2}) 345 | -------------------------------------------------------------------------------- /interface_stability/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogroupumd/interface_stability/2729cdf7bc6fea579c7aa95bae71c450bd6a7455/interface_stability/scripts/__init__.py -------------------------------------------------------------------------------- /interface_stability/scripts/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogroupumd/interface_stability/2729cdf7bc6fea579c7aa95bae71c450bd6a7455/interface_stability/scripts/__init__.pyc -------------------------------------------------------------------------------- /interface_stability/scripts/phase_stability.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pymatgen import Composition 3 | from interface_stability.singlephase import VirtualEntry 4 | 5 | 6 | def get_phase_equilibria_from_composition(args): 7 | """ 8 | Provides the phase equilibria of a phase with given composition 9 | """ 10 | comp = Composition(args.composition) 11 | entry = VirtualEntry.from_composition(comp) 12 | print(entry.get_printable_PE_data_in_pd()) 13 | return 0 14 | 15 | 16 | def get_phase_equilibria_and_decomposition_energy_under_mu_from_composition(args): 17 | """ 18 | Provide the phase equilibria and decomposition energy when open to one element with given miu 19 | Chemical potential is referenced to pure phase of open element. 20 | """ 21 | comp = Composition(args.composition) 22 | chempot = {args.open_element: args.chemical_potential} 23 | entry = VirtualEntry.from_composition(comp) 24 | entry.stabilize() 25 | print(entry.get_printable_PE_and_decomposition_in_gppd(chempot, entries=None)) 26 | return 0 27 | 28 | 29 | def get_phase_evolution_profile(args): 30 | """ 31 | Provides the phase equilibria and decomposition energy evolution process of a phase when open to a specific element 32 | Chemical potential is referenced to pure phase of open element. 33 | """ 34 | comp = Composition(args.composition) 35 | entry = VirtualEntry.from_composition(comp) 36 | oe = args.open_element 37 | entry.stabilize() 38 | print(entry.get_printable_evolution_profile(oe, allowpmu=args.posmu)) 39 | return 0 40 | 41 | 42 | def plot_vc(args): 43 | """ 44 | Get the plot data of voltage profile. 45 | """ 46 | comp = Composition(args.composition) 47 | entry = VirtualEntry.from_composition(comp) 48 | oe = args.open_element 49 | entry.stabilize() 50 | common_working_ions = dict(Li=1, Na=1, K=1, Mg=2, Ca=2, Al=3) 51 | valence = args.valence if args.valence else common_working_ions[oe] 52 | oe_list, v_list = entry.get_vc_plot_data(oe, valence=valence, allowpmu=args.posmu) 53 | print(entry.get_printable_vc_plot_data(oe, oe_list, v_list)) 54 | entry.get_voltage_profile_plot(oe, oe_list, v_list, valence).show() 55 | 56 | 57 | 58 | def main(): 59 | parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=""" 60 | --BRIEF INTRO-- 61 | This script will analyze the stability of a phase with any given input composition 62 | Either in a closed system (phase diagram) or in a system with an open element(grand potential phase diagram) 63 | This script works based on several sub-commands with their own options. 64 | To see the options for the sub-commands, use "python phase_stability.py sub-command -h". 65 | """, epilog=""" 66 | --REMINDER-- 67 | To use this script, you need to set following variable in ~/.pmgrc.yaml: 68 | PMG_MAPI_KEY :[Mandatory] the API key for MP to fetch data from MP website. 69 | PMG_PD_PRELOAD_PATH : [Optional] the local directory for saved cached data. 70 | """) 71 | 72 | parent_comp_mp = argparse.ArgumentParser(add_help=False) 73 | parent_comp_mp.add_argument("composition", type=str, help="The composition for analysis") 74 | parent_oe = argparse.ArgumentParser(add_help=False) 75 | parent_oe.add_argument("open_element", type=str, help="The open element") 76 | parent_mu = argparse.ArgumentParser(add_help=False) 77 | parent_mu.add_argument("chemical_potential", type=float, help="The chemical potential of open element." 78 | "Referenced to pure phase") 79 | 80 | parent_posmu = argparse.ArgumentParser(add_help=False) 81 | parent_posmu.add_argument("-posmu", action='store_true', default=False, 82 | help="Allow mu range to go beyond 0 and become positive") 83 | 84 | subparsers = parser.add_subparsers() 85 | 86 | parser_stability = subparsers.add_parser("stability", parents=[parent_comp_mp], 87 | help="Obtain the phase equilibria of a phase with given composition") 88 | parser_stability.set_defaults(func=get_phase_equilibria_from_composition) 89 | 90 | parser_evolution = subparsers.add_parser("evolution", parents=[parent_comp_mp, parent_oe, parent_posmu], 91 | help="Obtain the evolution profile at a given composition when open to an element") 92 | 93 | parser_evolution.set_defaults(func=get_phase_evolution_profile) 94 | 95 | parser_mu = subparsers.add_parser("mu", parents=[parent_comp_mp, parent_oe, parent_mu], 96 | help="Obtain the phase equilibria & decomposition energy of a phase with given composition when open to an element") 97 | parser_mu.set_defaults(func=get_phase_equilibria_and_decomposition_energy_under_mu_from_composition) 98 | 99 | # parser_plot_gppd = subparsers.add_parser("plotgppd", parents=[parent_comp_mp, parent_oe, parent_miu], 100 | # help="Obtain the grand potential phase diagram of a given material system under certain chemical potential") 101 | # parser_plot_gppd.set_defaults(func=plot_gppd) 102 | 103 | parser_plot_vc = subparsers.add_parser("plotvc", parents=[parent_comp_mp, parent_oe, parent_posmu], 104 | help="Plot the voltage profile of at a given composition") 105 | parser_plot_vc.add_argument('-v', '--valence', type=int, default=None, help='Valence of Working ion') 106 | 107 | parser_plot_vc.set_defaults(func=plot_vc) 108 | 109 | args = parser.parse_args() 110 | 111 | 112 | if hasattr(args, "func"): 113 | args.func(args) 114 | else: 115 | parser.print_help() 116 | 117 | 118 | if __name__ == "__main__": 119 | main() -------------------------------------------------------------------------------- /interface_stability/scripts/pseudo_binary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from pymatgen import Composition 5 | from interface_stability.pseudobinary import PseudoBinary 6 | from interface_stability.singlephase import VirtualEntry 7 | 8 | 9 | def input_handling(args): 10 | comp1 = Composition(args.composition_1) 11 | comp2 = Composition(args.composition_2) 12 | entry1 = VirtualEntry.from_composition(comp1) 13 | entry2 = VirtualEntry.from_composition(comp2) 14 | entry1.stabilize() 15 | entry2.stabilize() 16 | entry1.energy_correction(args.e1) 17 | entry2.energy_correction(args.e2) 18 | return entry1, entry2 19 | 20 | 21 | def chemical_stability(args): 22 | entry1, entry2 = input_handling(args) 23 | print("-" * 100, "\nThe starting phases compositions are ", entry1.name, 'and', entry2.name) 24 | print("All mixing ratio based on all formula already normalized to ONE atom per fu!") 25 | pb = PseudoBinary(entry1, entry2) 26 | print(pb.get_printable_pd_profile()) 27 | return 0 28 | 29 | 30 | def electrochemical_stability(args): 31 | entry1, entry2 = input_handling(args) 32 | oe = args.open_element 33 | mu = args.chemical_potential 34 | chempots = {oe: mu} 35 | print("-" * 100, "\nThe starting phases compositions are ", entry1.name, 'and', entry2.name) 36 | print("All mixing ratio based on all formula already normalized to ONE atom per fu!") 37 | 38 | print("Chemical potential is miu_{} = {}, using elementary phase as reference.".format(oe, mu)) 39 | print('-' * 60) 40 | pb = PseudoBinary(entry1, entry2) 41 | print(pb.get_printable_gppd_profile(chempots)) 42 | return 0 43 | 44 | 45 | def electrochemical_stability_screening(args): 46 | entry1, entry2 = input_handling(args) 47 | oe = args.open_element 48 | miu_low = args.miu_low 49 | miu_high = args.miu_high 50 | pb = PseudoBinary(entry1, entry2) 51 | print(pb.gppd_scanning(oe, miu_high, miu_low)) 52 | 53 | 54 | def main(): 55 | parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=""" 56 | --BRIEF INTRO-- 57 | This script will calculate the phase equilibria of a pseudo binary (linear combination of two entries) 58 | Either in a closed system (pd) or a system with an open element (gppd) 59 | They reflect the chemical/electrochemical stability of the pseudo binary, respectively. 60 | This script works based on several sub-commands with their own options. 61 | To see the options for the sub-commands, use "pseudo_stability.py sub-command -h". 62 | """, epilog=""" 63 | --REMINDER-- 64 | To use this script, you need to set following variable in ~/.pmgrc.yaml: 65 | PMG_MAPI_KEY : the API key for MP to fetch data from MP website. 66 | PMG_PD_PRELOAD_PATH : the local path for saved pickle files. 67 | """) 68 | subparsers = parser.add_subparsers() 69 | parent_comp_mp = argparse.ArgumentParser(add_help=False) 70 | parent_comp_mp.add_argument("composition_1", type=str, help="The first phase composition of the pseudo-binary") 71 | parent_comp_mp.add_argument("composition_2", type=str, help="The second phase composition of the pseudo-binary") 72 | 73 | parent_comp_mp.add_argument("-e1", type=float, default=0.0, help="The energy correction of entry1 ref. to hull ") 74 | parent_comp_mp.add_argument("-e2", type=float, default=0.0, help="The energy correction of entry2 ref. to hull ") 75 | 76 | parent_oe = argparse.ArgumentParser(add_help=False) 77 | parent_oe.add_argument("open_element", type=str, help="The open element") 78 | 79 | parent_miu = argparse.ArgumentParser(add_help=False) 80 | parent_miu.add_argument("chemical_potential", type=float, help="The chemical potential of open element." 81 | "Default referenced to pure phase, " 82 | "ref can be changed with -vaspref") 83 | 84 | parser_pd = subparsers.add_parser("pd", parents=[parent_comp_mp], 85 | help="The chemical stability / phase equilibria info of the pseudo-binary, " 86 | "calculated in PD") 87 | parser_pd.set_defaults(func=chemical_stability) 88 | 89 | parser_gppd = subparsers.add_parser("gppd", parents=[parent_comp_mp, parent_oe, parent_miu], 90 | help="The electrochemical stability / phase equilibria info of the pseudo-" 91 | "binary, calculated in GPPD") 92 | parser_gppd.set_defaults(func=electrochemical_stability) 93 | 94 | parser_gppd_screen = subparsers.add_parser("gppd_screen", parents=[parent_comp_mp, parent_oe], 95 | help="The electrochemical stability in a given chemical potential range") 96 | parser_gppd_screen.add_argument("miu_low", type=float, help="lower chemical potential for gppd screening") 97 | parser_gppd_screen.add_argument("miu_high", type=float, help="upper chemical potential for gppd screening") 98 | parser_gppd_screen.set_defaults(func=electrochemical_stability_screening) 99 | 100 | args = parser.parse_args() 101 | if hasattr(args, "func"): 102 | args.func(args) 103 | else: 104 | parser.print_help() 105 | 106 | 107 | if __name__ == "__main__": 108 | main() 109 | -------------------------------------------------------------------------------- /interface_stability/singlephase.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) Yifei Mo Group @ University of Maryland, College Park 3 | # Distributed under the terms of the MIT License. 4 | 5 | 6 | import os 7 | import json 8 | import re 9 | import warnings 10 | import pandas 11 | 12 | import matplotlib.pyplot as plt 13 | from matplotlib import rc 14 | from monty.json import MontyDecoder, MontyEncoder 15 | from pymatgen import Composition, SETTINGS, Element, MPRester 16 | from pymatgen.analysis.phase_diagram import PhaseDiagram, GrandPotentialPhaseDiagram 17 | from pymatgen.analysis.reaction_calculator import ComputedReaction 18 | from pymatgen.entries.computed_entries import ComputedEntry 19 | 20 | __author__ = "Yizhou Zhu" 21 | __copyright__ = "" 22 | __version__ = "2.2" 23 | __maintainer__ = "Yizhou Zhu" 24 | __email__ = "yizhou.zhu@gmail.com" 25 | __status__ = "Production" 26 | __date__ = "Jun 10, 2018" 27 | 28 | PD_PRELOAD_PATH = SETTINGS.get("PMG_PD_PRELOAD_PATH") 29 | # if PD_PRELOAD_PATH is None: 30 | # trypreload = False 31 | 32 | plt.rcParams['mathtext.default'] = 'regular' 33 | rc('font', **{'family': 'sans-serif', 'sans-serif': ['Helvetica'], 'size': 15}) 34 | 35 | 36 | class VirtualEntry(ComputedEntry): 37 | def __init__(self, composition, energy, name=None): 38 | super(VirtualEntry, self).__init__(Composition(composition), energy) 39 | if name: 40 | self.name = name 41 | 42 | @classmethod 43 | def from_composition(cls, comp, energy=0, name=None): 44 | return cls(Composition(comp), energy, name=name) 45 | 46 | @classmethod 47 | def from_mixing(cls, mixing_dict): 48 | comp = Composition("") 49 | energy = 0 50 | for i in mixing_dict.keys(): 51 | comp += Composition({el: i.composition[el] * mixing_dict[i] for el in i.composition.keys()}) 52 | energy += mixing_dict[i] * i.energy 53 | return cls(Composition(comp), energy) 54 | 55 | @classmethod 56 | def from_mp(cls, criteria): 57 | entry = cls.get_mp_entry(criteria) 58 | return cls(entry.composition, energy=entry.energy, name=entry.name) 59 | 60 | @staticmethod 61 | def get_mp_entry(criteria): 62 | """ 63 | Here always return the lowest energy among all polymorphs. 64 | Criteria can be a formula or an mp-id 65 | """ 66 | with MPRester() as m: 67 | entries = m.get_entries(criteria) 68 | entries = sorted(entries, key=lambda e: e.energy_per_atom) 69 | if len(entries) == 0: 70 | raise ValueError("MP doesn't have any entry that matches the given formula/MP-id!") 71 | return entries[0] 72 | 73 | @property 74 | def chemsys(self): 75 | return [_.symbol for _ in self.composition.elements] 76 | 77 | def get_PD_entries(self, sup_el=None, exclusions=None, trypreload=False): 78 | """ 79 | :param sup_el: a list for extra element dimension, using str format 80 | :param exclusions: a list of manually exclusion entries, can use entry name or mp_id 81 | :param trypreload: If try to reload from cached search results. 82 | Warning: if set to True, the return result may not be consistent with the updated MP database. 83 | :return: all related entries to construct phase diagram. 84 | """ 85 | 86 | chemsys = self.chemsys + sup_el if sup_el else self.chemsys 87 | chemsys = list(set(chemsys)) 88 | 89 | if trypreload: 90 | entries = self.get_PD_entries_from_preload_file(chemsys) 91 | else: 92 | entries = self.get_PD_entries_from_MP(chemsys) 93 | entries.append(self) 94 | if exclusions: 95 | entries = [e for e in entries if e.name not in exclusions] 96 | entries = [e for e in entries if e.entry_id not in exclusions] 97 | return entries 98 | 99 | @staticmethod 100 | def get_PD_entries_from_MP(chemsys): 101 | with MPRester() as m: 102 | entries = m.get_entries_in_chemsys(chemsys) 103 | 104 | return entries 105 | 106 | @staticmethod 107 | def get_PD_entries_from_preload_file(chemsys): 108 | """ 109 | If you use this method, the results may be incompatible with the most updated MP database. 110 | """ 111 | if PD_PRELOAD_PATH is None: 112 | warnings.warn("\nYou are trying load locally cached entries. " 113 | "\nYou should set up a valid folder for local cache, " 114 | "\nand put the path as PMG_PD_PRELOAD_PATH in ~/.pmgrc.yaml") 115 | if not os.path.isdir(PD_PRELOAD_PATH): 116 | warnings.warn("\nPMG_PD_PRELOAD_PATH is not a valid folder path." 117 | "\nPlease reset PMG_PD_PRELOAD_PATH in ~/.pmgrc.yaml") 118 | 119 | el_list = [x.symbol for x in sorted(set(chemsys))] 120 | load_path = os.path.join(PD_PRELOAD_PATH, "_".join(el_list) + "_Entries.json") 121 | try: 122 | with open(load_path) as f: 123 | entries = json.load(f, cls=MontyDecoder) 124 | except (IOError, EOFError): 125 | entries = VirtualEntry.get_PD_entries_from_MP(chemsys) 126 | with open(load_path, 'w') as f: 127 | json.dump(entries, f, cls=MontyEncoder) 128 | return entries 129 | 130 | def get_decomp_entries_and_e_above_hull(self, entries=None, exclusions=None, trypreload=None): 131 | if not entries: 132 | entries = self.get_PD_entries(exclusions=exclusions, trypreload=trypreload) 133 | pd = PhaseDiagram(entries) 134 | decomp_entries, hull_energy = pd.get_decomp_and_e_above_hull(self) 135 | return decomp_entries, hull_energy 136 | 137 | def stabilize(self, entries=None): 138 | """ 139 | Stabilize an entry by putting it on the convex hull 140 | """ 141 | decomp_entries, hull_energy = self.get_decomp_entries_and_e_above_hull(entries=entries) 142 | self.correction -= (hull_energy * self.composition.num_atoms + 1e-8) 143 | return None 144 | 145 | def energy_correction(self, e): 146 | """ 147 | Correction term is applied by per atom. 148 | """ 149 | self.correction += e * self.composition.num_atoms 150 | return None 151 | 152 | def get_printable_PE_data_in_pd(self, entries=None): 153 | decomp, hull_e = self.get_decomp_entries_and_e_above_hull(entries=entries) 154 | output = ['-' * 60] 155 | PE = list(decomp.keys()) 156 | output.append("Reduced formula of the given composition: " + self.composition.reduced_formula) 157 | output.append("Calculated phase equilibria: " + "\t".join(i.name for i in PE)) 158 | rxn = ComputedReaction([self], PE) 159 | rxn.normalize_to(self.composition.reduced_composition) 160 | output.append(str(rxn)) 161 | output.append('-' * 60) 162 | string = '\n'.join(output) 163 | return string 164 | 165 | def GPComp(self, chempot): 166 | """ 167 | Non-open element composition, which excluded the open element part. 168 | """ 169 | GPComp = Composition({el: amt for el, amt in self.composition.items() if el.symbol not in chempot.keys()}) 170 | return GPComp 171 | 172 | def get_gppd_entries(self, chempot, exclusions=None, trypreload=False): 173 | return self.get_PD_entries(sup_el=list(chempot.keys()), exclusions=exclusions, trypreload=trypreload) 174 | 175 | def get_decomposition_in_gppd(self, chempot, entries=None, exclusions=None, trypreload=False): 176 | gppd_entries = entries if entries \ 177 | else self.get_gppd_entries(chempot, exclusions=exclusions, trypreload=trypreload) 178 | pd = PhaseDiagram(gppd_entries) 179 | gppd_entries = pd.stable_entries 180 | open_el_entries = [_ for _ in gppd_entries if 181 | _.is_element and _.composition.elements[0].symbol in chempot.keys()] 182 | el_ref = {_.composition.elements[0].symbol: _.energy_per_atom for _ in open_el_entries} 183 | chempot_vaspref = {_: chempot[_] + el_ref[_] for _ in chempot} 184 | for open_entry in open_el_entries: 185 | open_entry.correction += chempot_vaspref[open_entry.composition.elements[0].symbol] 186 | 187 | GPPD = GrandPotentialPhaseDiagram(gppd_entries, chempot_vaspref) 188 | GPComp = self.GPComp(chempot) 189 | decomp_GP_entries = GPPD.get_decomposition(GPComp) 190 | decomp_entries = [gpe.original_entry for gpe in decomp_GP_entries] 191 | rxn = ComputedReaction([self] + open_el_entries, decomp_entries) 192 | rxn.normalize_to(self.composition) 193 | return decomp_entries, rxn 194 | 195 | def get_printable_PE_and_decomposition_in_gppd(self, chempot, entries=None, exclusions=None, trypreload=False): 196 | oes = list(chempot.keys()) 197 | output = ['-' * 60, "Reduced formula of the given composition: " + self.composition.reduced_formula] 198 | for oe in oes: 199 | output.append("Open element : " + oe) 200 | output.append("Chemical potential: {:.5g} eV referenced to pure phase".format(chempot[oe])) 201 | output.append('-' * 60) 202 | decomp_entries, rxn = self.get_decomposition_in_gppd(chempot, entries=entries, exclusions=exclusions, 203 | trypreload=trypreload) 204 | formula = self.composition.reduced_composition 205 | rxn.normalize_to(formula) 206 | rxn_e = round(rxn.calculated_reaction_energy, 5) 207 | output.append("Reaction:" + str(rxn)) 208 | output.append("Reaction energy: {:.5g} eV per {}".format(rxn_e, formula.reduced_formula)) 209 | output.append('-' * 60) 210 | string = '\n'.join(output) 211 | return string 212 | 213 | def get_phase_evolution_profile(self, oe, allowpmu=False, entries=None,exclusions=None): 214 | pd_entries = entries if entries else self.get_PD_entries(sup_el=[oe],exclusions=exclusions) 215 | offset = 30 if allowpmu else 0 216 | for e in pd_entries: 217 | if e.composition.is_element and oe in e.composition.keys(): 218 | e.correction += offset * e.composition.num_atoms 219 | pd = PhaseDiagram(pd_entries) 220 | evolution_profile = pd.get_element_profile(oe, self.composition.reduced_composition) 221 | el_ref = evolution_profile[0]['element_reference'] 222 | el_ref.correction -= el_ref.composition.num_atoms * offset 223 | evolution_profile[0]['chempot'] -= offset 224 | return evolution_profile 225 | 226 | 227 | def get_stability_window(self,oe,allowpmu=False, entries=None): 228 | profile = self.get_phase_evolution_profile(oe=oe,allowpmu=allowpmu,entries=entries) 229 | chempots = [_['chempot'] for _ in profile] 230 | evolutions = [_['evolution'] for _ in profile] 231 | index = evolutions.index(sorted(evolutions,key=lambda x: abs(x))[0]) 232 | 233 | if abs(evolutions[index]) < 1e-8: 234 | ref = profile[0]['element_reference'].energy_per_atom 235 | if index < len(profile)-1: 236 | return (chempots[index]-ref,chempots[index+1]-ref) 237 | else: 238 | return (chempots[index]-ref,None) 239 | else: 240 | return (None, None) 241 | 242 | 243 | 244 | #index = chempots.index([evolution]) 245 | # print (evolutions[index],chempots[index]) 246 | 247 | #return 248 | 249 | 250 | 251 | def get_evolution_phases_table_string(self, open_el, pure_el_ref, PE_list, oe_amt_list, mu_trans_list, allowpmu): 252 | if not allowpmu: 253 | mu_h_list = [0] + mu_trans_list 254 | mu_l_list = mu_h_list[1:] + ['-inf'] 255 | df = pandas.DataFrame() 256 | df['mu_high (eV)'] = mu_h_list 257 | df['mu_low (eV)'] = mu_l_list 258 | df['d(n_{})'.format(open_el)] = oe_amt_list 259 | PE_names = [] 260 | rxns = [] 261 | for PE in PE_list: 262 | rxn = ComputedReaction([self, pure_el_ref], PE) 263 | rxn.normalize_to(self.composition.reduced_composition) 264 | PE_names.append(', '.join(sorted([_.name for _ in PE]))) 265 | rxns.append(str(rxn)) 266 | df['Phase equilibria'] = PE_names 267 | df['Reaction'] = rxns 268 | print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 269 | return print_df 270 | 271 | def get_rxn_e_table_string(self, pure_el_ref, open_el, PE_list, oe_amt_list, mu_trans_list, plot_rxn_e): 272 | neg_flag = (max(mu_trans_list) > 1e-6) 273 | rxn_trans_list = [mu for mu in mu_trans_list] 274 | rxn_e_list = [] 275 | ext = 0.2 276 | rxn_trans_list = [rxn_trans_list[0] + ext] + rxn_trans_list if neg_flag else [0] + rxn_trans_list 277 | for data in zip(oe_amt_list, PE_list, rxn_trans_list): 278 | oe_amt, PE, ext_miu = data 279 | rxn = ComputedReaction([self, pure_el_ref], PE) 280 | rxn.normalize_to(self.composition.reduced_composition) 281 | rxn_e_list.append(rxn.calculated_reaction_energy - oe_amt * ext_miu) 282 | rxn_trans_list = rxn_trans_list + [rxn_trans_list[-1] - ext] 283 | rxn_e_list = rxn_e_list + [rxn_e_list[-1] + ext * oe_amt_list[-1]] 284 | rxn_e_list = [e / self.composition.num_atoms for e in rxn_e_list] 285 | df = pandas.DataFrame() 286 | df["miu_{} (eV)".format(open_el)] = rxn_trans_list 287 | df["Rxn energy (eV/atom)"] = rxn_e_list 288 | 289 | if plot_rxn_e: 290 | plt.figure(figsize=(8, 6)) 291 | ax = plt.gca() 292 | ax.invert_xaxis() 293 | ax.axvline(0, linestyle='--', color='k', linewidth=0.5, zorder=1) 294 | ax.plot(rxn_trans_list, rxn_e_list, '-', linewidth=1.5, color='cornflowerblue', zorder=3) 295 | ax.scatter(rxn_trans_list[1:-1], rxn_e_list[1:-1], edgecolors='cornflowerblue', facecolors='w', 296 | linewidth=1.5, s=50, zorder=4) 297 | ax.set_xlabel('Chemical potential ref. to {}'.format(open_el)) 298 | ax.set_ylabel('Reaction energy (eV/atom)') 299 | ax.set_xlim([float(rxn_trans_list[0]), float(rxn_trans_list[-1])]) 300 | plt.show() 301 | print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 302 | 303 | return print_df 304 | 305 | def get_printable_evolution_profile(self, open_el, entries=None, plot_rxn_e=True, allowpmu=False): 306 | evolution_profile = self.get_phase_evolution_profile(open_el, entries=entries, allowpmu=allowpmu) 307 | 308 | PE_list = [list(stage['entries']) for stage in evolution_profile] 309 | oe_amt_list = [stage['evolution'] for stage in evolution_profile] 310 | pure_el_ref = evolution_profile[0]['element_reference'] 311 | 312 | miu_trans_list = [stage['chempot'] for stage in evolution_profile][1:] # The first chempot is always useless 313 | miu_trans_list = sorted(miu_trans_list, reverse=True) 314 | miu_trans_list = [miu - pure_el_ref.energy_per_atom for miu in miu_trans_list] 315 | 316 | table1 = self.get_evolution_phases_table_string(open_el, pure_el_ref, PE_list, oe_amt_list, miu_trans_list, 317 | allowpmu) 318 | table2 = self.get_rxn_e_table_string(pure_el_ref, open_el, PE_list, oe_amt_list, miu_trans_list, plot_rxn_e) 319 | 320 | output = ['-' * 60, "Reduced formula of the given composition: " + self.composition.reduced_formula, 321 | '\n === Evolution Profile ===', str(table1), '\n === Reaction energy ===', str(table2), 322 | 'Note:\nChemical potential referenced to element phase.', 323 | 'Reaction energy is normalized to per atom of the given composition.'] 324 | string = '\n'.join(output) 325 | return string 326 | 327 | def get_vc_plot_data(self, open_el, valence=None, entries=None, allowpmu=True): 328 | common_working_ion = {Element('Li'): 1, Element('Na'): 1, Element('K'): 1, Element('Mg'): 2, Element('Ca'): 2, 329 | Element('Zn'): 2, Element('Al'): 3} 330 | if valence: 331 | ioncharge = valence 332 | else: 333 | if open_el not in common_working_ion.keys(): 334 | raise ValueError('Working ion {} not supported. You can provide charge manually'.format(open_el.symbol)) 335 | else: 336 | ioncharge = common_working_ion[open_el] 337 | 338 | evolution_profile = self.get_phase_evolution_profile(open_el, entries=entries, allowpmu=allowpmu) 339 | oe_list = [] 340 | v_list = [] 341 | for i in range(len(evolution_profile)): 342 | step = evolution_profile[-i - 1] 343 | oe_content = step['evolution'] 344 | miu_vasp = step['chempot'] 345 | oe_list.append(oe_content) 346 | v_list.append(miu_vasp) 347 | v_ref = v_list[-1] 348 | v_list = [-i + v_ref for i in v_list] 349 | v_list = [v / ioncharge for v in v_list] 350 | 351 | return oe_list, v_list 352 | 353 | def get_printable_vc_plot_data(self, open_el, oe_list, v_list): 354 | df = pandas.DataFrame() 355 | oes, vs = [], [] 356 | for i in range(len(oe_list) - 1): 357 | oes.append(oe_list[i]) 358 | oes.append(oe_list[i + 1]) 359 | vs.append(v_list[i]) 360 | vs.append(v_list[i]) 361 | df["d n({})".format(open_el)] = oes 362 | df["Voltage ref. to {} (V)".format(open_el)] = vs 363 | print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') 364 | return print_df 365 | 366 | def get_voltage_profile_plot(self, open_el, oe_list, v_list, valence): 367 | X, Y = [], [] 368 | for i in range(len(oe_list) - 1): 369 | X += [oe_list[i], oe_list[i + 1]] 370 | Y += [v_list[i], v_list[i]] 371 | fig, ax = plt.subplots(1, 1) 372 | plt.plot(X, Y) 373 | ylabel = 'Potential ref. to {} / '.format(open_el, open_el, valence) 374 | sup = r'${}^{{{}+}}$'.format(open_el.symbol, valence) if valence > 1 else r'${}^{{+}}$'.format(open_el) 375 | ax.set_ylabel(ylabel + sup) 376 | s1 = re.sub("([0-9]+)", "_{\\1}", self.name) 377 | formula = '$\mathregular{' + s1 + '}$' 378 | ax.set_xlabel('$\Delta$n({}) per {}'.format(open_el, formula)) 379 | ax.legend([formula]) 380 | if min(ax.get_ylim()) < 0: 381 | ax.axhline(0, linestyle='--', color='k', linewidth=0.5, zorder=1) 382 | else: 383 | ax.set_ylim(bottom=0) 384 | return plt 385 | -------------------------------------------------------------------------------- /interface_stability/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogroupumd/interface_stability/2729cdf7bc6fea579c7aa95bae71c450bd6a7455/interface_stability/tests/__init__.py -------------------------------------------------------------------------------- /interface_stability/tests/test_interface_stability.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pymatgen import MPRester, SETTINGS 3 | 4 | #@unittest.skipIf(not SETTINGS.get("PMG_MAPI_KEY"), "PMG_MAPI_KEY environment variable not set.") 5 | 6 | class InterfaceStabilityTest(unittest.TestCase): 7 | def test_get_api_key(self): 8 | self.assertTrue(SETTINGS.get("PMG_MAPI_KEY")) 9 | 10 | 11 | def test_get_structure_by_material_id(self): 12 | with MPRester() as m: 13 | s1 = m.get_structure_by_material_id("mp-1") 14 | self.assertEqual(s1.formula, "Cs1") 15 | 16 | if __name__ == "__main__": 17 | unittest.main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from setuptools import setup, find_packages 3 | from codecs import open 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='interface_stability', 13 | version='0.1', 14 | description='Python project for analyze interface stability', 15 | long_description=long_description, 16 | url='https://github.com/mogroupumd/interface_stability', 17 | author='Yifei Mo Research Group at UMD', 18 | author_email='yizhou.zhu@gmail.com', 19 | classifiers=[ 20 | 'License :: OSI Approved :: MIT License', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.6' 23 | ], 24 | keywords=[], 25 | #packages=['interface_stability','scripts'], 26 | packages=find_packages(), 27 | #install_requires=['pymatgen','argparse'], 28 | install_requires=['argparse'], 29 | extras_require={}, 30 | package_data={}, 31 | data_files=[], 32 | entry_points={ 33 | 'console_scripts': [ 34 | 'phase_stability=interface_stability.scripts.phase_stability:main', 35 | 'pseudo_binary=interface_stability.scripts.pseudo_binary:main' 36 | ], 37 | }, 38 | project_urls={ 39 | 'Bug Reports': 'https://github.com/mogroupumd/interface_stability/issues', 40 | 'Source': 'https://github.com/mogroupumd/interface_stability', 41 | }, 42 | ) 43 | --------------------------------------------------------------------------------