├── .gitignore ├── LICENSE ├── README.md └── micki ├── __init__.py ├── analysis.py ├── db.py ├── eref.py ├── fortran.py ├── io.py ├── lattice.py ├── masses.py ├── model.py ├── reactants.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### micki 2 | 3 | A modular, extensible, robust object-oriented microkinetic modeling package 4 | written in Python. 5 | 6 | ### DEPENDENCIES: 7 | * lapack (MKL by default "-lmkl_rt"; can specify alternative LAPACK library via MICKI_LAPACK environmental variable) 8 | * ase 9 | * numpy 10 | * sympy 11 | * sundials (C library) - Tested with version 4.0. Compile Sundials with the following flags to cmake: -DFCMIX_ENABLE=ON -DCMAKE_C_FLAGS="-fPIC" -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_SIZE=32 12 | 13 | ### Detailed installation instructions: 14 | #install ASE (which includes numpy as a dependancy) and sympy
15 | pip3 install --user ase
16 | pip3 install --user sympy
17 | 18 | #install sundials
19 | wget https://github.com/LLNL/sundials/releases/download/v4.0.2/sundials-4.0.2.tar.gz
20 | tar zxvf sundials-4.0.2.tar.gz
21 | cd sundials-4.0.2
22 | mkdir build
23 | cd build
24 | #ensure that MKL is found!
25 | . /opt/intel/mkl/bin/mklvars.sh intel64
26 | cmake .. -DFCMIX_ENABLE=ON -DCMAKE_C_FLAGS="-fPIC" -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_SIZE=32
27 | #make sure MKL BLAS/LAPACK were found!
28 | make
29 | make install
30 | 31 | #fetch Micki itself
32 | git clone https://github.com/jrschmidt2/micki.git 33 | -------------------------------------------------------------------------------- /micki/__init__.py: -------------------------------------------------------------------------------- 1 | from micki.reactants import Liquid, Gas, Adsorbate, Electron 2 | from micki.model import Reaction, Model 3 | from micki.eref import EnergyReference 4 | from micki.analysis import ModelAnalysis 5 | from micki.lattice import Lattice 6 | -------------------------------------------------------------------------------- /micki/analysis.py: -------------------------------------------------------------------------------- 1 | """Module for doing sensitivity analysis of microkinetic model""" 2 | 3 | import collections 4 | 5 | import numpy as np 6 | import sympy as sym 7 | 8 | from ase.units import kB 9 | 10 | from micki.reactants import Adsorbate, _Fluid 11 | from micki.model import Model 12 | 13 | 14 | class ModelAnalysis(object): 15 | def __init__(self, model, product_reaction, Uequil, tol=1e-3, dt=3600): 16 | self.model = model 17 | self.reaction_name = product_reaction 18 | self.product_reaction = model.reactions[product_reaction] 19 | self.Uequil = Uequil 20 | self.tol = tol 21 | self.dt = dt 22 | 23 | self.model.set_initial_conditions(self.Uequil) 24 | 25 | # self.U, self.r = self.model.solve(self.dt, 100) 26 | t, self.U, self.r = self.model.find_steady_state() 27 | model.finalize() 28 | 29 | # self.check_converged(self.U, self.r) 30 | self.species_symbols = [] 31 | for species in self.model._species: 32 | if species.symbol is not None: 33 | self.species_symbols.append(species) 34 | self.rmid = self.r[self.reaction_name] 35 | 36 | def campbell_rate_control(self, rxn_name, scale=0.001): 37 | reaction = self.model.reactions[rxn_name] 38 | 39 | keq = reaction.get_keq(self.model.T, 40 | self.model.Asite, 41 | self.model.z) 42 | 43 | subs = {} 44 | for species in self.species_symbols: 45 | subs[species.symbol] = self.U[species.label] 46 | 47 | kmid = reaction.get_kfor(self.model.T, 48 | self.model.Asite, 49 | self.model.z) 50 | 51 | if isinstance(kmid, sym.Basic): 52 | kmid = kmid.subs(subs) 53 | 54 | reaction.set_scale('kfor', 1.0 - scale) 55 | reaction.set_scale('krev', 1.0 - scale) 56 | reaction.update(self.model.T, 57 | self.model.Asite, 58 | self.model.z, 59 | force=True) 60 | klow = reaction.get_kfor() 61 | model = self.model.copy() 62 | 63 | try: 64 | t1, U1, r1 = model.find_steady_state() 65 | # U1, r1 = model.solve(self.dt, 100) 66 | finally: 67 | reaction.set_scale('kfor', 1.0) 68 | reaction.set_scale('krev', 1.0) 69 | 70 | model.finalize() 71 | # self.check_converged(U1, r1) 72 | rlow = r1[self.reaction_name] 73 | if isinstance(klow, sym.Basic): 74 | subs = {} 75 | for species in self.species_symbols: 76 | subs[species.symbol] = U1[species.label] 77 | klow = klow.subs(subs) 78 | 79 | reaction.set_scale('kfor', 1.0 + scale) 80 | reaction.set_scale('krev', 1.0 + scale) 81 | reaction.update(self.model.T, 82 | self.model.Asite, 83 | self.model.z, 84 | force=True) 85 | khigh = reaction.get_kfor() 86 | model = self.model.copy() 87 | 88 | try: 89 | t2, U2, r2 = model.find_steady_state() 90 | # U2, r2 = model.solve(self.dt, 100) 91 | finally: 92 | reaction.set_scale('kfor', 1.0) 93 | reaction.set_scale('krev', 1.0) 94 | 95 | model.finalize() 96 | # self.check_converged(U2, r2) 97 | rhigh = r2[self.reaction_name] 98 | if isinstance(khigh, sym.Basic): 99 | subs = {} 100 | for species in self.species_symbols: 101 | subs[species.symbol] = U2[species.label] 102 | khigh = khigh.subs(subs) 103 | reaction.set_scale('kfor', 1.0) 104 | reaction.set_scale('krev', 1.0) 105 | 106 | return kmid * (rhigh - rlow) / (self.rmid * (khigh - klow)) 107 | 108 | def thermodynamic_rate_control(self, names, dg=None): 109 | T = self.model.T 110 | if dg is None: 111 | dg = 0.001 * kB * T 112 | 113 | if not isinstance(names, (list, tuple)): 114 | species = [self.model.species[names]] 115 | else: 116 | species = [self.model.species[name] for name in names] 117 | 118 | gmid = species[0].get_G(T) 119 | gmid = species[0].get_H(T) - T * species[0].get_S(T) 120 | if isinstance(gmid, sym.Basic): 121 | subs = {} 122 | for sp in self.species_symbols: 123 | subs[sp.symbol] = self.U[sp.label] 124 | gmid = gmid.subs(subs) 125 | 126 | for sp in species: 127 | sp.dE -= dg 128 | 129 | for reaction in self.model._reactions: 130 | oldalpha = reaction.alpha 131 | reaction.update(T=self.model.T, 132 | Asite=self.model.Asite, 133 | L=self.model.z, 134 | force=True) 135 | newalpha = reaction.alpha 136 | 137 | model = self.model.copy(initialize=False) 138 | model.set_initial_conditions(self.U) 139 | 140 | try: 141 | t1, U1, r1 = model.find_steady_state() 142 | # U1, r1 = model.solve(self.dt, 100) 143 | finally: 144 | for sp in species: 145 | sp.dE += dg 146 | 147 | model.finalize() 148 | # self.check_converged(U1, r1) 149 | rlow = r1[self.reaction_name] 150 | 151 | for sp in species: 152 | sp.dE += dg 153 | 154 | for reaction in self.model._reactions: 155 | reaction.update(T=self.model.T, 156 | Asite=self.model.Asite, 157 | L=self.model.z, 158 | force=True) 159 | 160 | model = self.model.copy(initialize=False) 161 | model.set_initial_conditions(self.U) 162 | 163 | try: 164 | t2, U2, r2 = model.find_steady_state() 165 | # U2, r2 = model.solve(self.dt, 100) 166 | finally: 167 | for sp in species: 168 | sp.dE -= dg 169 | 170 | for reaction in self.model._reactions: 171 | reaction.update(T=self.model.T, 172 | Asite=self.model.Asite, 173 | L=self.model.z, 174 | force=True) 175 | 176 | model.finalize() 177 | # self.check_converged(U2, r2) 178 | rhigh = r2[self.reaction_name] 179 | 180 | return (rlow - rhigh) * kB * T / (self.rmid * dg) 181 | 182 | def activation_barrier(self, dT=0.01): 183 | T = self.model.T 184 | 185 | model = self.model.copy(initialize=False) 186 | model.T = T - dT 187 | model.set_initial_conditions(self.Uequil) 188 | t1, U1, r1 = model.find_steady_state() 189 | # U1, r1 = model.solve(self.dt, 100) 190 | model.finalize() 191 | # self.check_converged(U1, r1) 192 | 193 | rlow = r1[self.reaction_name] 194 | 195 | model = self.model.copy(initialize=False) 196 | model.T = T + dT 197 | model.set_initial_conditions(self.Uequil) 198 | t2, U2, r2 = model.find_steady_state() 199 | # U2, r2 = model.solve(self.dt, 100) 200 | model.finalize() 201 | # self.check_converged(U2, r2) 202 | 203 | rhigh = r2[self.reaction_name] 204 | 205 | return kB * T**2 * (rhigh - rlow) / (self.rmid * 2 * dT) 206 | 207 | def rate_order(self, name, drho=0.05): 208 | species = self.model.species[name] 209 | 210 | rhomid = self.Uequil[species.label] 211 | assert rhomid > 0 212 | 213 | U0 = self.Uequil.copy() 214 | rholow = rhomid * (1.0 - drho) 215 | U0[species.label] = rholow 216 | model = self.model.copy(initialize=False) 217 | model.set_initial_conditions(U0) 218 | t1, U1, r1 = model.find_steady_state() 219 | # U1, r1 = model.solve(self.dt, 100) 220 | model.finalize() 221 | # self.check_converged(U1, r1) 222 | rlow = r1[self.reaction_name] 223 | 224 | rhohigh = rhomid * (1.0 + drho) 225 | U0[species.label] = rhohigh 226 | model.set_initial_conditions(U0) 227 | t2, U2, r2 = model.find_steady_state() 228 | # U2, r2 = model.solve(self.dt, 100) 229 | model.finalize() 230 | # self.check_converged(U2, r2) 231 | rhigh = r2[self.reaction_name] 232 | 233 | return (rhomid / self.rmid) * (rhigh - rlow) / (rhohigh - rholow) 234 | 235 | def drate_order_dg(self, fluid, adsorbates, rho_scale=0.01, g_scale=0.01): 236 | assert isinstance(fluid, _Fluid) 237 | 238 | if not isinstance(adsorbates, collections.Iterable): 239 | adsorbates = [adsorbates] 240 | 241 | assert isinstance(adsorbates[0], Adsorbate) 242 | 243 | rhomid = self.U[fluid] 244 | assert rhomid > 0 245 | rmid = self.r[self.reaction_name] 246 | gmid = adsorbates[0].get_G(self.model.T) 247 | if isinstance(gmid, sym.Basic): 248 | trans = {} 249 | for species in self.model._species: 250 | if isinstance(species, Adsorbate) \ 251 | and species.symbol is not None: 252 | trans[species.symbol] = self.U[species.label] 253 | gmid = gmid.subs(trans) 254 | dg = np.abs(gmid * g_scale * 2) 255 | drho = rhomid * rho_scale * 2 256 | 257 | dr = 0 258 | 259 | def set_dg(species, dg): 260 | species.dE += dg 261 | 262 | for i in [-1, 1]: 263 | for adsorbate in adsorbates: 264 | set_dg(adsorbate, i * dg) 265 | 266 | for reaction in self.model._reactions: 267 | reaction.update(T=self.model.T, 268 | Asite=self.model.Asite, 269 | L=self.model.z, 270 | force=True) 271 | 272 | for j in [-1, 1]: 273 | U0 = self.Uequil.copy() 274 | U0[fluid] = rhomid + j * drho 275 | 276 | model = self.model.copy(initialize=False) 277 | model.set_initial_conditions(U0) 278 | 279 | # Ui, ri = model.solve(self.dt, 100) 280 | ti, Ui, ri = model.find_steady_state() 281 | dr += i * j * ri[self.product_reaction] 282 | 283 | for adsorbate in adsorbates: 284 | set_dg(adsorbate, -i * dg) 285 | 286 | for reaction in self.model._reactions: 287 | reaction.update(T=self.model.T, 288 | Asite=self.model.Asite, 289 | L=self.model.z, 290 | force=True) 291 | 292 | return (rhomid / rmid) * dr / (dg * drho) 293 | 294 | def check_converged(self, *vals): 295 | for val in vals: 296 | for i, key in enumerate(val[0]): 297 | if np.abs(val[-1][key] - val[-2][key]) > self.tol: 298 | print(key, val[-1][key], val[-1][key] - val[-2][key]) 299 | raise ValueError("Calculation not converged! Increase " 300 | "dt or use better initial guess.") 301 | -------------------------------------------------------------------------------- /micki/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | 5 | from ase.db import connect 6 | from ase.db.core import Database 7 | 8 | from micki.reactants import Adsorbate, Gas, Liquid 9 | from micki.eref import EnergyReference 10 | 11 | class MickiDBReadError(ValueError): 12 | pass 13 | 14 | # Attempts to parse attribute 'name' from dictionary 'data' and raises a 15 | # parse error if it cannot be found. 16 | def get_data(row, param): 17 | if param not in row.data: 18 | raise MickiDBReadError("DB row named {} does not have '{}' entry!" 19 | "".format(row.name, param)) 20 | return row.data[param] 21 | 22 | # Converts a single ASE DB row to a Micki Thermo object. 23 | def row_to_thermo(row): 24 | name = row.name 25 | freqs = get_data(row, 'freqs') 26 | thermo = get_data(row, 'thermo') 27 | sites = get_data(row, 'sites') 28 | rhoref = get_data(row, 'rhoref') 29 | dE = get_data(row, 'dE') 30 | symm = get_data(row, 'symm') 31 | ts = get_data(row, 'ts') 32 | spin = get_data(row, 'spin') 33 | D = get_data(row, 'D') 34 | S = get_data(row, 'S') 35 | 36 | if thermo == 'Adsorbate': 37 | return Adsorbate(row.toatoms(), name, freqs, 38 | ts=ts, sites=sites, dE=dE, symm=symm) 39 | elif thermo == 'Gas': 40 | return Gas(row.toatoms(), name, freqs, 41 | symm=symm, spin=spin, rhoref=rhoref, dE=dE) 42 | elif thermo == 'Liquid': 43 | return Liquid(row.toatoms(), name, freqs, 44 | symm=symm, spin=spin, D=D, S=S, rhoref=rhoref, dE=dE) 45 | else: 46 | raise ValueError('Unknown Thermo type {}!'.format(thermo)) 47 | 48 | # Creates a dictionary of Thermo objects from a properly-formatted ASE DB file. 49 | def read_from_db(db, names=None, eref=None): 50 | if isinstance(db, str): 51 | db = connect(db) 52 | elif not isinstance(db, Database): 53 | raise ValueError("Must pass active ASE DB connection, " 54 | "or name of ASE DB file!") 55 | 56 | species = {} 57 | 58 | for row in db.select(): 59 | name = row.name 60 | try: 61 | species[name] = row_to_thermo(row) 62 | except MickiDBReadError: 63 | print("Could not parse row {}, skipping.".format(name)) 64 | 65 | for name, sp in species.items(): 66 | newsites = [] 67 | for site in sp.sites: 68 | if site in species: 69 | newsites.append(species[site]) 70 | else: 71 | raise ValueError("Unknown site named {}!".format(site)) 72 | sp.sites = newsites 73 | 74 | if eref is not None: 75 | reference = EnergyReference([species[name] for name in eref]) 76 | for name, sp in species.items(): 77 | sp.eref = reference 78 | 79 | if names is not None: 80 | return {name: species[name] for name in names} 81 | return species 82 | -------------------------------------------------------------------------------- /micki/eref.py: -------------------------------------------------------------------------------- 1 | """Reference atoms object""" 2 | 3 | import numpy as np 4 | 5 | from ase import Atoms 6 | from ase.io import read 7 | from ase.data import chemical_symbols 8 | from ase.db.row import AtomsRow 9 | 10 | from micki.reactants import _Thermo 11 | 12 | 13 | class EnergyReference(dict): 14 | """Construct an atomic energy reference. 15 | 16 | This routine accepts an iterable containing N paths to ASE-readable 17 | geometry files with N unique elements between them, and returns a dict-like 18 | containing those unique elements as keys and their reference energy as 19 | values. 20 | 21 | By default, use the same geometry files as those which contain the 22 | vibrational frequencies for the microkinetic model. 23 | """ 24 | def __init__(self, species, index=0): 25 | dict.__init__(self) 26 | symbols = [] 27 | energies = [] 28 | elements = set() 29 | 30 | self.initialized = False 31 | 32 | for sp in species: 33 | if isinstance(sp, Atoms): 34 | conf = sp 35 | if isinstance(sp, AtomsRow): 36 | conf = sp.toatoms() 37 | elif isinstance(sp, _Thermo): 38 | conf = sp.atoms 39 | else: 40 | conf = read(sp, index=index) 41 | symbols.append(conf.get_chemical_symbols()) 42 | elements = elements.union(symbols[-1]) 43 | energies.append(conf.get_potential_energy()) 44 | 45 | size = len(elements) 46 | if len(energies) < size: 47 | raise ValueError("System is underdetermined!") 48 | elif len(energies) > size: 49 | raise ValueError("System is overdetermined!") 50 | 51 | coeff = np.zeros((size, size), dtype=float) 52 | 53 | for i in range(size): 54 | for j, symbol in enumerate(elements): 55 | coeff[i, j] = symbols[i].count(symbol) 56 | eref = np.linalg.solve(coeff, energies) 57 | 58 | for i, symbol in enumerate(elements): 59 | self[symbol] = eref[i] 60 | 61 | self.initialized = True 62 | 63 | def __setitem__(self, key, value): 64 | if self.initialized: 65 | raise NotImplementedError 66 | else: 67 | super(EnergyReference, self).__setitem__(key, value) 68 | 69 | def __delitem__(self, key): 70 | raise NotImplementedError 71 | 72 | def __getitem__(self, key): 73 | if isinstance(key, str): 74 | key = key.capitalize() 75 | elif isinstance(key, int): 76 | key = chemical_symbols[key] 77 | return super(EnergyReference, self).__getitem__(key) 78 | 79 | def copy(self): 80 | new = EnergyReference.__new__(EnergyReference) 81 | new.initialized = False 82 | for key in self: 83 | new[key] = self[key] 84 | new.initialized = True 85 | return new 86 | -------------------------------------------------------------------------------- /micki/fortran.py: -------------------------------------------------------------------------------- 1 | f90_template = """module solve_ida 2 | 3 | implicit none 4 | 5 | integer :: neq = {neq} 6 | integer :: iout(50) 7 | real*8 :: rout(50) 8 | real*8 :: y0({neq}), yp0({neq}) 9 | real*8 :: diff({neq}), mas({neq}, {neq}) 10 | real*8 :: jac({neq}, {neq}) 11 | real*8 :: rates({nrates}) 12 | real*8 :: dypdr({neq}, {nrates}) 13 | integer :: dvacdy({nvac}, {neq}) 14 | 15 | end module solve_ida 16 | 17 | subroutine initialize(neqin, y0in, rtol, atol, ipar, rpar, id_vec) 18 | 19 | use solve_ida, only: neq, iout, rout, y0, yp0, mas, diff, dypdr, dvacdy 20 | 21 | implicit none 22 | 23 | integer, intent(in) :: neqin, ipar(*) 24 | real*8, intent(in) :: y0in(neqin), rtol, atol(*) 25 | real*8, intent(in) :: rpar(*) 26 | real*8, intent(in) :: id_vec(neqin) 27 | real*8 :: constr_vec(neqin) 28 | real*8 :: t0, yptmp(neqin) 29 | integer :: nthreads, iatol, ier 30 | integer :: i 31 | integer :: meth, itmeth 32 | integer :: myid 33 | 34 | dypdr = 0 35 | {dypdrcalc} 36 | 37 | dvacdy = 0 38 | {dvacdycalc} 39 | 40 | iatol = 2 41 | constr_vec = 1.d0 42 | 43 | y0 = y0in 44 | yp0 = 0 45 | yptmp = 0 46 | diff = id_vec 47 | mas = 0 48 | t0 = 0 49 | meth = 2 ! 1 = Adams (nonstiff), 2 = BDF (stiff) 50 | itmeth = 2 ! 1 = functional iteration, 2 = Newton iteration 51 | 52 | do i = 1, neq 53 | mas(i, i) = id_vec(i) 54 | enddo 55 | 56 | ! ! Calculate yp 57 | call fidaresfun(0.d0, y0, yptmp, yp0, ipar, rpar, ier) 58 | 59 | ! initialize Sundials 60 | call fnvinits(2, neq, ier) 61 | ! allocate memory 62 | call fidamalloc(t0, y0, yp0, iatol, rtol, atol, iout, rout, ipar, rpar, ier) 63 | ! set maximum number of steps (default = 500) 64 | call fidasetiin('MAX_NSTEPS', 50000, ier) 65 | ! set algebraic variables 66 | call fidasetvin('ID_VEC', id_vec, ier) 67 | ! set constraints (all yi >= 0.) 68 | call fidasetvin('CONSTR_VEC', constr_vec, ier) 69 | 70 | !Uncomment the following lines for Sundials 2.X 71 | ! call fidalapackdense(neq, ier) 72 | ! call fidalapackdensesetjac(1, ier) 73 | 74 | !Uncomment these lines for Sundials 4.X (and comment about the above) 75 | call FSUNDenseMatInit(2, neq, neq, ier) 76 | call FSUNLAPACKDENSEINIT(2, ier) 77 | call FIDALSINIT(ier) 78 | 79 | end subroutine initialize 80 | 81 | subroutine find_steady_state(neqin, nrates, dt, maxiter, epsilon, t1, u1, du1, r1) 82 | 83 | use solve_ida, only: y0, yp0, iout, rout, rates, dypdr 84 | 85 | implicit none 86 | 87 | integer, intent(in) :: neqin, nrates, maxiter 88 | real*8, intent(in) :: dt, epsilon 89 | 90 | real*8 :: rpar(1) 91 | integer :: ipar(1) 92 | 93 | real*8, intent(out) :: t1, u1(neqin), du1(neqin), r1(nrates) 94 | 95 | real*8 :: tout, epsilon2 96 | real*8 :: dutmp(neqin), du0(neqin) 97 | integer :: itask, ier 98 | integer :: i 99 | 100 | logical :: converged = .FALSE. 101 | 102 | epsilon2 = epsilon**2 103 | i = 0 104 | itask = 1 105 | tout = 0.0d0 106 | u1 = y0 107 | du1 = yp0 108 | t1 = 0.d0 109 | du0 = 0.d0 110 | 111 | call fidacalcic(1, dt, ier) 112 | 113 | do while (.not. converged) 114 | if (tout - t1 < dt * 0.01) then 115 | tout = tout + dt 116 | end if 117 | 118 | call fidasolve(tout, t1, u1, du1, itask, ier) 119 | 120 | i = i + 1 121 | 122 | call fidaresfun(tout, u1, du0, dutmp, ipar, rpar, ier) 123 | 124 | if (maxval(dutmp**2) < epsilon2) then 125 | converged = .TRUE. 126 | end if 127 | if (i >= maxiter) then 128 | print *, "ODE NOT CONVERGED!" 129 | exit 130 | end if 131 | end do 132 | 133 | call ratecalc({neq}, u1) 134 | r1 = rates 135 | 136 | end subroutine find_steady_state 137 | 138 | subroutine solve(neqin, nrates, nt, tfinal, t1, u1, du1, r1) 139 | 140 | use solve_ida, only: y0, yp0, iout, rout, rates 141 | 142 | implicit none 143 | 144 | integer, intent(in) :: neqin, nt, nrates 145 | real*8, intent(in) :: tfinal 146 | 147 | real*8 :: rpar(1) 148 | integer :: ipar(1) 149 | 150 | real*8, intent(out) :: t1(nt) 151 | real*8, intent(out) :: u1(neqin, nt), du1(neqin, nt) 152 | real*8, intent(out) :: r1(nrates, nt) 153 | 154 | real*8 :: dt, tout 155 | integer :: itask, ier 156 | integer :: i 157 | 158 | itask = 1 159 | dt = tfinal / (nt - 1) 160 | tout = 0.0d0 161 | u1 = 0 162 | du1 = 0 163 | u1(:, 1) = y0 164 | du1(:, 1) = yp0 165 | t1(1) = 0.d0 166 | call ratecalc({neq}, u1(:, 1)) 167 | r1(:, 1) = rates 168 | 169 | 170 | ! call fidacalcic(1, dt, ier) 171 | 172 | do i = 2, nt 173 | tout = tout + dt 174 | do while (tout - t1(i) > dt * 0.01) 175 | call fidasolve(tout, t1(i), u1(:, i), du1(:, i), itask, ier) 176 | end do 177 | r1(:, i) = rates 178 | end do 179 | 180 | end subroutine solve 181 | 182 | subroutine finalize 183 | 184 | implicit none 185 | 186 | call fidafree 187 | 188 | end subroutine finalize 189 | 190 | subroutine fidaresfun(tres, yin, ypin, res, ipar, rpar, reserr) 191 | 192 | use solve_ida, only: neq, diff, dypdr, rates 193 | 194 | implicit none 195 | 196 | integer, intent(in) :: ipar(*) 197 | integer, intent(out) :: reserr 198 | real*8, intent(in) :: tres, rpar(*) 199 | real*8, intent(in) :: yin(neq), ypin(neq) 200 | real*8, intent(out) :: res(neq) 201 | real*8 :: y(neq) 202 | 203 | integer :: i 204 | 205 | reserr = 0 206 | 207 | y = yin 208 | res = 0 209 | 210 | 211 | do i = 1, neq 212 | if (y(i) < -1d-10) then 213 | ! y(i) = 0.d0 214 | reserr = 1 215 | endif 216 | enddo 217 | 218 | call ratecalc({neq}, y) 219 | 220 | res = matmul(dypdr, rates) - diff * ypin 221 | 222 | end subroutine fidaresfun 223 | 224 | subroutine fidadjac(neqin, t, yin, ypin, r, jac, cj, ewt, h, ipar, rpar, wk1, wk2, wk3, djacerr) 225 | 226 | use solve_ida, only: mas, dypdr, dvacdy 227 | 228 | implicit none 229 | 230 | integer :: neqin, ipar(*) 231 | integer :: djacerr 232 | real*8 :: t, h, cj, rpar(*) 233 | real*8 :: yin(neqin), ypin(neqin), r(neqin), ewt(*), jac(neqin, neqin) 234 | real*8 :: wk1(*), wk2(*), wk3(*) 235 | real*8 :: y(neqin), drdy({nrates}, {neq}), drdvac({nrates}, {nvac}), vac({nvac}) 236 | 237 | integer :: i 238 | 239 | djacerr = 0 240 | 241 | jac = 0 242 | y = yin 243 | 244 | do i = 1, neqin 245 | if (y(i) < -1d-10) then 246 | y(i) = 0.d0 247 | djacerr = 1 248 | endif 249 | enddo 250 | 251 | vac = 0 252 | {vaccalc} 253 | 254 | do i = 1, {nvac} 255 | if (vac(i) < -1d-10) then 256 | vac(i) = 0.d0 257 | djacerr = 1 258 | endif 259 | enddo 260 | 261 | drdy = 0 262 | {drdycalc} 263 | 264 | drdvac = 0 265 | {drdvaccalc} 266 | 267 | drdy = drdy + matmul(drdvac, dvacdy) 268 | 269 | jac = matmul(dypdr, drdy) - cj * mas 270 | 271 | end subroutine fidadjac 272 | 273 | subroutine ratecalc(neqin, yin) 274 | 275 | use solve_ida, only: rates 276 | 277 | implicit none 278 | 279 | integer, intent(in) :: neqin 280 | real*8, intent(in) :: yin(neqin) 281 | real*8 :: y(neqin) 282 | real*8 :: vac({nvac}) 283 | 284 | integer :: i 285 | 286 | y = yin 287 | 288 | 289 | vac = 0 290 | {vaccalc} 291 | 292 | do i = 1, {nvac} 293 | if (vac(i) < -1d-10) then 294 | vac(i) = 0.d0 295 | endif 296 | enddo 297 | 298 | rates = 0 299 | {ratecalc} 300 | 301 | end subroutine ratecalc 302 | 303 | subroutine fidajtimes(tres, yin, ypin, res, vin, fjv, cj, ewt, h, ipar, rpar, wk1, wk2, ier) 304 | 305 | use solve_ida, only: neq 306 | 307 | implicit none 308 | 309 | real*8, intent(in) :: tres, yin(neq), ypin(neq), res(neq), vin(neq), cj, h 310 | real*8 :: ewt(*), wk1(*), wk2(*), rpar(*), wk3(1) 311 | integer :: ipar(*) 312 | integer :: i 313 | 314 | real*8, intent(out) :: fjv(neq) 315 | integer, intent(out) :: ier 316 | 317 | real*8 :: jac(neq, neq) 318 | 319 | call fidadjac(neq, tres, yin, ypin, res, jac, cj, ewt, h, ipar, rpar, wk1, wk2, wk2, ier) 320 | 321 | fjv = 0.d0 322 | 323 | call dgemv('N', neq, neq, 1.d0, jac, neq, vin, 1, 0.d0, fjv, 1) 324 | 325 | end subroutine fidajtimes 326 | 327 | subroutine fidapsol(tres, yin, ypin, res, rvin, zv, cj, delta, ewt, ipar, rpar, wk1, ier) 328 | 329 | use solve_ida, only: neq, jac 330 | 331 | implicit none 332 | 333 | real*8, intent(in) :: tres, yin(neq), ypin(neq), res(neq), rvin(neq) 334 | real*8, intent(in) :: cj, delta, ewt(*), rpar(*) 335 | integer, intent(in) :: ipar(*) 336 | 337 | real*8 :: wk1(*) 338 | integer :: ier 339 | 340 | real*8, intent(out) :: zv(neq) 341 | 342 | integer :: ipiv(neq) 343 | integer :: i 344 | 345 | zv = rvin 346 | call dgesv(neq, 1, jac, neq, ipiv, zv, neq, ier) 347 | 348 | end subroutine fidapsol 349 | 350 | subroutine fidapset(tres, yin, ypin, res, cj, ewt, h, ipar, rpar, wk1, wk2, wk3, ier) 351 | 352 | use solve_ida, only: neq, jac 353 | 354 | implicit none 355 | 356 | real*8, intent(in) :: tres, yin(neq), ypin(neq), res(neq) 357 | real*8, intent(in) :: cj, ewt(*), h, rpar(*) 358 | real*8 :: wk1(*), wk2(*), wk3(*) 359 | 360 | integer, intent(in) :: ipar(*) 361 | 362 | integer, intent(out) :: ier 363 | 364 | call fidadjac(neq, tres, yin, ypin, res, jac, cj, ewt, h, ipar, rpar, wk1, wk2, wk3, ier) 365 | 366 | end subroutine fidapset 367 | 368 | """ 369 | 370 | 371 | pyf_template = """! -*- f90 -*- 372 | ! Note: the context of this file is case sensitive. 373 | 374 | python module {modname} ! in 375 | interface ! in :{modname} 376 | module solve_ida ! in :{modname}:{modname}.f90 377 | integer dimension(50) :: iout 378 | real*8 dimension(50) :: rout 379 | real*8 dimension({neq}) :: y0 380 | real*8 dimension({neq}) :: yp0 381 | real*8 dimension({neq}) :: diff 382 | real*8 dimension({neq},{neq}) :: mas 383 | real*8 dimension({neq},{neq}) :: jac 384 | real*8 dimension({nrates}) :: rates 385 | real*8 dimension({neq},{nrates}) :: dypdr 386 | integer dimension({nvac},{neq}) :: dvacdy 387 | integer, optional :: neq={neq} 388 | end module solve_ida 389 | subroutine initialize(neqin,y0in,rtol,atol,ipar,rpar,id_vec) ! in :{modname}:{modname}.f90 390 | use solve_ida, only: neq,iout,rout,y0,yp0,mas,diff,dypdr,dvacdy 391 | integer, optional,intent(in),check(len(y0in)>=neqin),depend(y0in) :: neqin=len(y0in) 392 | real*8 dimension(neqin),intent(in) :: y0in 393 | real*8 intent(in) :: rtol 394 | real*8 dimension(*),intent(in) :: atol 395 | integer dimension(*),intent(in) :: ipar 396 | real*8 dimension(*),intent(in) :: rpar 397 | real*8 dimension(neqin),intent(in),depend(neqin) :: id_vec 398 | end subroutine initialize 399 | subroutine find_steady_state(neqin,nrates,dt,maxiter,epsilon,t1,u1,du1,r1) ! in :{modname}:{modname}.f90 400 | use solve_ida, only: y0,yp0,iout,rout,rates,dypdr 401 | integer intent(in) :: neqin 402 | integer intent(in) :: nrates 403 | real*8 intent(in) :: dt 404 | integer intent(in) :: maxiter 405 | real*8 intent(in) :: epsilon 406 | real*8 intent(out) :: t1 407 | real*8 intent(out),dimension(neqin),depend(neqin) :: u1 408 | real*8 intent(out),dimension(neqin),depend(neqin) :: du1 409 | real*8 intent(out),dimension(nrates),depend(nrates) :: r1 410 | end subroutine find_steady_state 411 | subroutine solve(neqin,nrates,nt,tfinal,t1,u1,du1,r1) ! in :{modname}:{modname}.f90 412 | use solve_ida, only: y0,yp0,iout,rout,rates 413 | integer intent(in) :: neqin 414 | integer intent(in) :: nrates 415 | integer intent(in) :: nt 416 | real*8 intent(in) :: tfinal 417 | real*8 intent(out),dimension(nt),depend(nt) :: t1 418 | real*8 intent(out),dimension(neqin,nt),depend(neqin,nt) :: u1 419 | real*8 intent(out),dimension(neqin,nt),depend(neqin,nt) :: du1 420 | real*8 intent(out),dimension(nrates,nt),depend(nrates,nt) :: r1 421 | end subroutine solve 422 | subroutine finalize ! in :{modname}:{modname}.f90 423 | end subroutine finalize 424 | end interface 425 | end python module {modname} 426 | 427 | ! This file was auto-generated with f2py (version:2). 428 | ! See http://cens.ioc.ee/projects/f2py2e/""" 429 | -------------------------------------------------------------------------------- /micki/io.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | 5 | from ase.io import read 6 | from ase.units import _hplanck, J, m, kg 7 | 8 | from micki.masses import masses 9 | 10 | def parse_vasp_out(filename, ignore_atoms=[]): 11 | atoms = read(filename, index=0) 12 | for atom in atoms: 13 | atom.mass = masses[atom.symbol] 14 | if 'OUTCAR' in filename: 15 | # This reads the hessian from the OUTCAR and diagonalizes it 16 | # to find the frequencies, rather than reading the frequencies 17 | # directly from the OUTCAR. This is to ensure we use the same 18 | # unit conversion factors, and also to make sure we use the same 19 | # atom masses for all calculations. Also, allows for the possibility 20 | # of doing partial hessian diagonalization should we want to do that. 21 | hessblock = 0 22 | with open(filename, 'r') as f: 23 | for line in f: 24 | line = line.strip() 25 | if line != '': 26 | if hessblock == 1: 27 | if line.startswith('---'): 28 | hessblock = 2 29 | 30 | elif hessblock == 2: 31 | line = line.split() 32 | dof = len(line) 33 | hess = np.zeros((dof, dof), dtype=float) 34 | index = np.zeros(dof, dtype=int) 35 | cart = np.zeros(dof, dtype=int) 36 | for i, direction in enumerate(line): 37 | index[i] = int(direction[:-1]) - 1 38 | if direction[-1] == 'X': 39 | cart[i] = 0 40 | elif direction[-1] == 'Y': 41 | cart[i] = 1 42 | elif direction[-1] == 'Z': 43 | cart[i] = 2 44 | else: 45 | raise ValueError("Error reading Hessian!") 46 | hessblock = 3 47 | j = 0 48 | 49 | elif hessblock == 3: 50 | line = line.split() 51 | hess[j] = np.array([float(val) for val in line[1:]], 52 | dtype=float) 53 | j += 1 54 | 55 | elif line.startswith('SECOND DERIVATIVES'): 56 | hessblock = 1 57 | 58 | elif hessblock == 3: 59 | break 60 | 61 | hess = -(hess + hess.T) / 2. 62 | elif 'vasprun.xml' in filename: 63 | import xml.etree.ElementTree as ET 64 | 65 | tree = ET.parse(filename) 66 | root = tree.getroot() 67 | 68 | vasp_mass = {} 69 | 70 | for element in root.find("atominfo/array[@name='atomtypes']/set"): 71 | vasp_mass[element[1].text.strip()] = float(element[2].text) 72 | 73 | selective = np.ones((len(atoms), 3), dtype=bool) 74 | constblock = root.find( 75 | 'structure[@name="initialpos"]/varray[@name="selective"]') 76 | if constblock is not None: 77 | for i, v in enumerate(constblock): 78 | for j, fixed in enumerate(v.text.split()): 79 | selective[i, j] = (fixed == 'T') 80 | index = [] 81 | for i, atom in enumerate(atoms): 82 | for direction in selective[i]: 83 | if direction: 84 | index.append(i) 85 | 86 | hess = np.zeros((len(index), len(index)), dtype=float) 87 | 88 | for i, v in enumerate(root.find( 89 | 'calculation/dynmat/varray[@name="hessian"]')): 90 | hess[i] = -np.array([float(val) for val in v.text.split()]) 91 | 92 | vasp_massvec = np.zeros(len(index), dtype=float) 93 | for i, j in enumerate(index): 94 | vasp_massvec[i] = vasp_mass[atoms[j].symbol] 95 | 96 | # VASP uses weird masses, so we un-mass-weight here 97 | hess *= np.sqrt(np.outer(vasp_massvec, vasp_massvec)) 98 | 99 | else: 100 | raise ValueError('Unknown file format {}!'.format(filename)) 101 | mass = np.array([atoms[i].mass for i in index], dtype=float) 102 | hess /= np.sqrt(np.outer(mass, mass)) 103 | 104 | # Temporary work around: My test system OUTCARs include some 105 | # metal atoms in the hessian, this seems to cause some problems 106 | # with the MKM. So, here I'm taking only the non-metal part 107 | # of the hessian and diagonalizing that. 108 | partial = [] 109 | for i, j in enumerate(index): 110 | if (j in ignore_atoms 111 | or atoms[j] in ignore_atoms 112 | or atoms[j].symbol in ignore_atoms): 113 | continue 114 | partial.append(i) 115 | if not partial: 116 | return atoms, np.array([]) 117 | partial_hess = np.zeros((len(partial), len(partial))) 118 | partial_index = np.zeros(len(partial), dtype=int) 119 | for i, a in enumerate(partial): 120 | partial_index[i] = index[a] 121 | for j, b in enumerate(partial): 122 | partial_hess[i, j] = hess[a, b] 123 | 124 | partial_hess *= _hplanck**2 * J * m**2 * kg / (4 * np.pi**2) 125 | v, w = np.linalg.eig(partial_hess) 126 | 127 | # We're taking the square root of an array that could include 128 | # negative numbers, so the result has to be complex. 129 | freq = np.sqrt(np.array(v, dtype=complex)) 130 | freqs = np.zeros_like(freq, dtype=float) 131 | 132 | # We don't want to deal with complex numbers, so we just convert 133 | # imaginary numbers to negative reals. 134 | for i, val in enumerate(freq): 135 | if val.imag == 0: 136 | freqs[i] = val.real 137 | else: 138 | freqs[i] = -val.imag 139 | freqs.sort() 140 | return atoms, freqs 141 | -------------------------------------------------------------------------------- /micki/lattice.py: -------------------------------------------------------------------------------- 1 | """Lattice stuff""" 2 | 3 | from __future__ import division 4 | 5 | from .reactants import _Thermo 6 | import numpy as np 7 | from ase.units import kB 8 | 9 | 10 | class Lattice(object): 11 | def __init__(self, neighborlist): 12 | self.neighborlist = neighborlist 13 | self.sites = [site for site in neighborlist] 14 | if isinstance(self.sites[0], str): 15 | self.string_names = True 16 | elif isinstance(self.sites[0], _Thermo): 17 | self.string_names = False 18 | else: 19 | raise ValueError('All sites must be _Thermo objects or strings!') 20 | 21 | sitetype = str if self.string_names else _Thermo 22 | 23 | for site in self.sites: 24 | if not isinstance(site, sitetype): 25 | raise ValueError('All sites must be _Thermo objects or strings!') 26 | 27 | # Sanity check the input 28 | self.totneighbors = {} 29 | for site, neighbors in neighborlist.items(): 30 | self.totneighbors[site] = 0 31 | for neighbor, val in neighbors.items(): 32 | self.totneighbors[site] += val 33 | if neighbor not in self.sites: 34 | raise ValueError("Neighbor {} is unknown!".format(neighbor)) 35 | for site in self.sites: 36 | if site not in neighbors: 37 | neighbors[site] = 0 38 | 39 | # Create a neighbor list matrix 40 | nsites = len(self.sites) 41 | if nsites == 1: 42 | self.ratio = {self.sites[0]: 1} 43 | return 44 | nmat = np.zeros((nsites, nsites), dtype=float) 45 | for i, a in enumerate(self.sites): 46 | for j, b in enumerate(self.sites): 47 | nmat[i, j] = self.neighborlist[b].get(a, 0) / self.totneighbors[a] 48 | 49 | # Diagonalize to find the element ratio. Only one eigenvector should 50 | # have all positive values. This is the eigenvector that describes 51 | # the element ratio 52 | eigenvals, eigenvecs = np.linalg.eig(nmat) 53 | for i in range(nsites): 54 | if np.all(eigenvecs[:, i] < 0) or np.all(eigenvecs[:, i] > 0): 55 | ratio = np.abs(eigenvecs[:, i]) 56 | ratio /= ratio.sum() 57 | self.ratio = {site: ratio[i] for i, site in enumerate(self.sites)} 58 | break 59 | else: 60 | print("Eigenvectors: {}".format(eigenvecs)) 61 | raise ValueError("Failed to find the element ratio! Please " 62 | "double-check your neighbor count.") 63 | 64 | def update_site_names(self, string_to_thermo): 65 | if not self.string_names: 66 | raise RuntimeError('Sites are already _Thermo objects!') 67 | 68 | for site in self.sites: 69 | if site not in string_to_thermo: 70 | raise ValueError('No _Thermo object for site {}!'.format(site)) 71 | 72 | new_sites = [] 73 | new_neighborlist = {} 74 | 75 | for string, thermo in string_to_thermo.items(): 76 | if string not in self.sites: 77 | raise ValueError('Unknown site name {}!'.format(string)) 78 | new_sites.append(thermo) 79 | new_neighborlist[thermo] = {} 80 | for neighbor, count in self.neighborlist[string].items(): 81 | new_neighborlist[thermo] = string_to_thermo[neighbor] 82 | 83 | self.sites = new_sites 84 | self.neighborlist = new_neighborlist 85 | self.string_names = False 86 | 87 | def get_S_conf(self, sites): 88 | if sites is None or isinstance(sites, _Thermo) or len(sites) == 1: 89 | return 0 90 | nconfs = 1 91 | for i in range(1, len(sites)): 92 | ncount = self.neighborlist[sites[i - 1]][sites[i]] 93 | if ncount == 0: 94 | raise ValueError("This binding geometry is impossible!") 95 | nconfs *= ncount / self.ratio[sites[i]] 96 | return kB * np.log(nconfs) 97 | -------------------------------------------------------------------------------- /micki/masses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Masses from the 3rd edition of the "Green Book" 4 | masses = { 5 | 'H': 1.00782503207, 6 | 'He': 4.00260325415, 7 | 'Li': 7.01600455, 8 | 'Be': 9.0121822, 9 | 'B': 11.0093054, 10 | 'C': 12.0000000, 11 | 'N': 14.0030740048, 12 | 'O': 15.99491461956, 13 | 'F': 18.99840322, 14 | 'Ne': 19.9924401754, 15 | 'Na': 22.9897692809, 16 | 'Mg': 23.985041700, 17 | 'Al': 26.98153863, 18 | 'Si': 27.9769265325, 19 | 'P': 30.97376163, 20 | 'S': 31.97207100, 21 | 'Cl': 34.96885268, 22 | 'Ar': 39.9623831225, 23 | 'K': 38.96370668, 24 | 'Ca': 39.96259098, 25 | 'Sc': 44.9559119, 26 | 'Ti': 47.9479463, 27 | 'V': 50.9439595, 28 | 'Cr': 51.9405075, 29 | 'Mn': 54.9380451, 30 | 'Fe': 55.9349375, 31 | 'Co': 58.9331950, 32 | 'Ni': 57.9353429, 33 | 'Cu': 62.9295975, 34 | 'Zn': 63.9291422, 35 | 'Ga': 68.9255736, 36 | 'Ge': 73.9211778, 37 | 'As': 74.9215965, 38 | 'Se': 79.9165213, 39 | 'Br': 78.9183371, 40 | 'Kr': 83.911507, 41 | 'Rb': 84.911789738, 42 | 'Sr': 87.9056121, 43 | 'Y': 88.9058483, 44 | 'Zr': 89.9047044, 45 | 'Nb': 92.9063781, 46 | 'Mo': 97.9054082, 47 | 'Tc': 97.907216, 48 | 'Ru': 101.9043493, 49 | 'Rh': 102.905504, 50 | 'Pd': 105.903486, 51 | 'Ag': 106.905097, 52 | 'Cd': 113.9033585, 53 | 'In': 114.903878, 54 | 'Sn': 119.9021947, 55 | 'Sb': 120.9038157, 56 | 'Te': 129.9062244, 57 | 'I': 126.904473, 58 | 'Xe': 131.9041535, 59 | 'Cs': 132.905451933, 60 | 'Ba': 137.9052472, 61 | 'La': 138.9063533, 62 | 'Ce': 139.9054387, 63 | 'Pr': 140.9076528, 64 | 'Nd': 141.9077233, 65 | 'Pm': 144.912749, 66 | 'Sm': 151.9197324, 67 | 'Eu': 152.9212303, 68 | 'Gd': 157.9241039, 69 | 'Tb': 158.9253468, 70 | 'Dy': 163.9291748, 71 | 'Ho': 164.9303221, 72 | 'Er': 165.9302931, 73 | 'Tm': 168.9342133, 74 | 'Yb': 173.9388621, 75 | 'Lu': 174.9407718, 76 | 'Hf': 179.9465500, 77 | 'Ta': 180.9479958, 78 | 'W': 183.9509312, 79 | 'Re': 186.9557531, 80 | 'Os': 191.9614807, 81 | 'Ir': 192.9629264, 82 | 'Pt': 194.9647911, 83 | 'Au': 196.9665687, 84 | 'Hg': 201.9706430, 85 | 'Tl': 204.9744275, 86 | 'Pb': 207.9766521, 87 | 'Bi': 208.9803987, 88 | 'Po': 208.9824304, 89 | 'At': 209.987148, 90 | 'Rn': 222.0175777, 91 | 'Fr': 223.0197359, 92 | 'Ra': 226.0254098, 93 | 'Ac': 227.0277521, 94 | 'Th': 232.0380553, 95 | 'Pa': 231.0358840, 96 | 'U': 238.0507882, 97 | 'Np': 237.0481734, 98 | 'Pu': 244.064204, 99 | 'Am': 243.0613811, 100 | 'Cm': 247.070354, 101 | 'Bk': 247.070307, 102 | 'Cf': 251.079587, 103 | 'Es': 252.082980, 104 | 'Fm': 257.095105, 105 | 'Md': 258.098431, 106 | 'No': 259.10103, 107 | 'Lr': 262.10963, 108 | 'Rf': 261.10877, 109 | 'Db': 262.11408, 110 | 'Sg': 263.11832, 111 | 'Bh': 264.1246, 112 | 'Hs': 265.13009, 113 | 'Mt': 268.13873, 114 | 'Ds': 271.14606, 115 | 'Rg': 272.15362, 116 | 'Cn': None, 117 | } 118 | 119 | # Masses as reported in the second edition of the "Green Book" 120 | masses_1993 = { 121 | 'H': 1.007825035, 122 | 'He': 4.00260324, 123 | 'Li': 7.0160030, 124 | 'Be': 9.0121822, 125 | 'B': 11.0093054, 126 | 'C': 12.0, 127 | 'N': 14.003074002, 128 | 'O': 15.99491463, 129 | 'F': 18.99840322, 130 | 'Ne': 19.9924356, 131 | 'Na': 22.9897677, 132 | 'Mg': 23.9850423, 133 | 'Al': 26.9815386, 134 | 'Si': 27.9769271, 135 | 'P': 30.9737620, 136 | 'S': 31.97207070, 137 | 'Cl': 34.968852721, 138 | 'Ar': 39.9623837, 139 | 'K': 38.9637074, 140 | 'Ca': 39.9625906, 141 | 'Sc': 44.9559100, 142 | 'Ti': 47.9479473, 143 | 'V': 50.9439617, 144 | 'Cr': 51.9405098, 145 | 'Mn': 54.9380471, 146 | 'Fe': 55.9349393, 147 | 'Co': 58.9331976, 148 | 'Ni': 57.9353462, 149 | 'Cu': 62.9295989, 150 | 'Zn': 63.9291448, 151 | 'Ga': 68.925580, 152 | 'Ge': 73.9211774, 153 | 'As': 74.9215942, 154 | 'Se': 79.9165196, 155 | 'Br': 78.9183361, 156 | 'Kr': 83.911507, 157 | 'Rb': 84.911794, 158 | 'Sr': 87.9056188, 159 | 'Y': 88.905849, 160 | 'Zr': 89.9047026, 161 | 'Nb': 92.9063772, 162 | 'Mo': 97.9054073, 163 | 'Tc': 97.907215, 164 | 'Ru': 101.9043485, 165 | 'Rh': 102.905500, 166 | 'Pd': 105.903478, 167 | 'Ag': 106.905092, 168 | 'Cd': 113.903357, 169 | 'In': 114.903882, 170 | 'Sn': 119.9021991, 171 | 'Sb': 120.9038212, 172 | 'Te': 129.906229, 173 | 'I': 126.904473, 174 | 'Xe': 131.904144, 175 | 'Cs': 132.905429, 176 | 'Ba': 137.905232, 177 | 'La': 138.906347, 178 | 'Ce': 139.905433, 179 | 'Pr': 140.907647, 180 | 'Nd': 141.907719, 181 | 'Pm': 144.912743, 182 | 'Sm': 151.919728, 183 | 'Eu': 152.921225, 184 | 'Gd': 157.924019, 185 | 'Tb': 158.925342, 186 | 'Dy': 163.929171, 187 | 'Ho': 164.930319, 188 | 'Er': 165.930290, 189 | 'Tm': 168.93421, 190 | 'Yb': 173.938859, 191 | 'Lu': 174.940770, 192 | 'Hf': 179.9465457, 193 | 'Ta': 180.947462, 194 | 'W': 183.950928, 195 | 'Re': 186.955744, 196 | 'Os': 191.961467, 197 | 'Ir': 192.962917, 198 | 'Pt': 194.964766, 199 | 'Au': 196.966543, 200 | 'Hg': 201.970617, 201 | 'Tl': 204.974401, 202 | 'Pb': 207.976627, 203 | 'Bi': 208.980374, 204 | 'Po': 208.982404, 205 | 'At': 209.987126, 206 | 'Rn': 222.017571, 207 | 'Fr': None, 208 | 'Ra': None, 209 | 'Ac': None, 210 | 'Th': 232.0381, 211 | 'Pa': 231.03588, 212 | 'U': 238.0289, 213 | 'Np': None, 214 | 'Pu': None, 215 | 'Am': None, 216 | 'Cm': None, 217 | 'Bk': None, 218 | 'Cf': None, 219 | 'Es': None, 220 | 'Fm': None, 221 | 'Md': None, 222 | 'No': None, 223 | 'Lr': None, 224 | 'Rf': None, 225 | 'Db': None, 226 | 'Sg': None, 227 | 'Bh': None, 228 | 'Hs': None, 229 | 'Mt': None, 230 | 'Ds': None, 231 | 'Rg': None, 232 | 'Cn': None, 233 | } 234 | -------------------------------------------------------------------------------- /micki/model.py: -------------------------------------------------------------------------------- 1 | """Microkinetic modeling objects""" 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import glob 7 | import tempfile 8 | import shutil 9 | import warnings 10 | 11 | from collections import OrderedDict 12 | 13 | import numpy as np 14 | import sympy as sym 15 | 16 | from copy import copy 17 | from ase.units import kB, _hplanck, kg, _k, _Nav, mol 18 | 19 | from micki.reactants import _Thermo, _Fluid, _Reactants, Gas, Liquid, Adsorbate 20 | from micki.reactants import Electron 21 | 22 | from micki.lattice import Lattice 23 | 24 | 25 | class Reaction(object): 26 | def __init__(self, reactants, products, ts=None, method=None, S0=1., 27 | dG_act=None, dground=False, reversible=True): 28 | 29 | # Wrap reactants and products in _Reactants type 30 | if isinstance(reactants, _Thermo): 31 | self.reactants = _Reactants([reactants]) 32 | elif isinstance(reactants, _Reactants): 33 | self.reactants = reactants 34 | else: 35 | raise NotImplementedError 36 | 37 | if isinstance(products, _Thermo): 38 | self.products = _Reactants([products]) 39 | elif isinstance(products, _Reactants): 40 | self.products = products 41 | else: 42 | raise NotImplementedError 43 | 44 | # Determine the number of sites on the LHS and the RHS of the reaction, 45 | # then add "bare" sites as necessary to balance the site number. 46 | vacancies = OrderedDict() 47 | self.species = [] 48 | for species in self.reactants: 49 | if species not in self.species: 50 | self.species.append(species) 51 | if species.sites is None: 52 | continue 53 | for site in species.sites: 54 | if site in vacancies: 55 | vacancies[site] -= 1 56 | else: 57 | vacancies[site] = -1 58 | for species in self.products: 59 | if species not in self.species: 60 | self.species.append(species) 61 | if species.sites is None: 62 | continue 63 | for site in species.sites: 64 | if site in vacancies: 65 | vacancies[site] += 1 66 | else: 67 | vacancies[site] = 1 68 | 69 | # If the user supplied "bare" sites, count them too 70 | for species in self.reactants: 71 | if species in vacancies: 72 | vacancies[species] -= 1 73 | for species in self.products: 74 | if species in vacancies: 75 | vacancies[species] += 1 76 | 77 | for vacancy, nvac in vacancies.items(): 78 | # There are extra sites on the RHS, so add some to the LHS 79 | if nvac > 0: 80 | self.reactants += nvac * vacancy 81 | # There are extra sites on the LHS, so add some to the RHS 82 | elif nvac < 0: 83 | self.products += abs(nvac) * vacancy 84 | 85 | self.ts = None 86 | # The user supplied a transition state species 87 | if ts is not None: 88 | assert dG_act is None, \ 89 | "Cannot specify both barrier height and transition state!" 90 | # Wrap the TS in the _Reactants class 91 | if isinstance(ts, _Thermo): 92 | self.ts = _Reactants([ts]) 93 | elif isinstance(ts, _Reactants): 94 | self.ts = ts 95 | # Fail if the user supplies something other than a _Thermo 96 | # or _Reactants 97 | else: 98 | raise NotImplementedError 99 | # FIXME: Add stoichiometry checking to ensure logical reactions. 100 | # Caveat: Don't fail on unbalanced adsorption sites, since some 101 | # species take up more than one site. 102 | 103 | self.involves_catalyst = False 104 | for species in self.reactants: 105 | if isinstance(species, Adsorbate): 106 | self.involves_catalyst = True 107 | break 108 | if not self.involves_catalyst: 109 | for species in self.products: 110 | if isinstance(species, Adsorbate): 111 | self.involves_catalyst = True 112 | break 113 | 114 | self.method = method 115 | if self.method is None: 116 | if self.ts is not None: 117 | self.method = 'TST' 118 | else: 119 | self.method = 'EQUIL' 120 | 121 | if isinstance(self.method, str): 122 | self.method = self.method.upper() 123 | 124 | self.S0 = S0 125 | self.keq = None 126 | self.kfor = None 127 | self.krev = None 128 | self.T = None 129 | self.Asite = None 130 | self.L = None 131 | self.scale_params = ['dH', 'dS', 'dH_act', 'dS_act', 'kfor', 'krev'] 132 | self.alpha = None 133 | self.reversible = reversible 134 | 135 | # Scaling for sensitivity analysis, defaults to 1 (no scaling) 136 | self.scale = OrderedDict() 137 | for param in self.scale_params: 138 | self.scale[param] = 1.0 139 | self.scale_old = self.scale.copy() 140 | 141 | # If the user supplied a TS, this should be None. 142 | self.dG_act = dG_act 143 | 144 | # If all reactants are Liquid species, then this reaction can occur 145 | # at any point of the diffusion grid, not just near the catalyst 146 | # surface. 147 | self.all_liquid = True 148 | 149 | # Count up the number of Fluid and Adsorbate species on either side 150 | # of the reaction. This is necessary to construct a proper Jacobian 151 | # for the rate of change of Fluid species vs Adsorbate species. 152 | # The Jacobian is related to the concentration in M of catalytic sites 153 | # in the model (defaults to 1 M in the Model class). 154 | self.Nreact_fluid = 0 155 | self.Nreact_ads = 0 156 | for species in self.reactants: 157 | if not isinstance(species, Liquid): 158 | self.all_liquid = False 159 | if isinstance(species, _Fluid): 160 | self.Nreact_fluid += 1 161 | elif isinstance(species, Adsorbate): 162 | self.Nreact_ads += 1 163 | 164 | self.Nprod_fluid = 0 165 | self.Nprod_ads = 0 166 | for species in self.products: 167 | if not isinstance(species, Liquid): 168 | self.all_liquid = False 169 | if isinstance(species, _Fluid): 170 | self.Nprod_fluid += 1 171 | elif isinstance(species, Adsorbate): 172 | self.Nprod_ads += 1 173 | 174 | self.Nfluid = self.Nreact_fluid + self.Nprod_fluid 175 | self.Nads = self.Nreact_ads + self.Nprod_ads 176 | 177 | self.dground = dground 178 | 179 | def get_scale(self, param): 180 | try: 181 | return self.scale[param] 182 | except KeyError: 183 | print("{} is not a valid scaling parameter name!".format(param)) 184 | return None 185 | 186 | def set_scale(self, param, value): 187 | try: 188 | self.scale[param] = value 189 | except KeyError: 190 | print("{} is not a valid scaling parameter name!".format(param)) 191 | 192 | def update(self, T=None, Asite=None, L=None, force=False): 193 | if not force and not self.is_update_needed(T, Asite, L): 194 | return 195 | 196 | for species in self.species: 197 | species.update(T=T, force=True) 198 | 199 | self.T = T 200 | self.Asite = Asite 201 | self.L = L 202 | self.dH = self.products.get_H(T) - self.reactants.get_H(T) 203 | # self.dH *= self.scale['dH'] 204 | self.dS = self.products.get_S(T) - self.reactants.get_S(T) 205 | # self.dS *= self.scale['dS'] 206 | self.dG = self.dH - self.T * self.dS 207 | if self.ts is not None: 208 | for species in self.ts: 209 | species.update(T=T, force=True) 210 | 211 | Gts = self.ts.get_G(T) 212 | Gr = self.reactants.get_G(T) 213 | Gp = self.products.get_G(T) 214 | 215 | dEr = np.sum([species.lateral + species.dE for species in self.reactants]) 216 | dEp = np.sum([species.lateral + species.dE for species in self.products]) 217 | 218 | dGf = Gts - Gr + dEr 219 | dGr = Gts - Gp + dEp 220 | 221 | if dGf < 0: 222 | raise RuntimeError('Reaction {} has negative forwards activation barrier!'.format(self)) 223 | if dGr < 0: 224 | raise RuntimeError('Reaction {} has negative reverse activation barrier!'.format(self)) 225 | 226 | all_symbols = set() 227 | all_symbols.update(sym.sympify(dEr).atoms(sym.Symbol)) 228 | all_symbols.update(sym.sympify(dEp).atoms(sym.Symbol)) 229 | 230 | if sym.sympify(dEp - dEr).subs({symbol: 0 for symbol in all_symbols}) == 0: 231 | self.alpha = dGf / (dGf + dGr) 232 | else: 233 | a1 = (2*dEp - 2*dEr - dGf - dGr - sym.sqrt(8*(dEp-dEr)*dGf + (-2*dEp + 2*dEr + dGf + dGr)**2))/(4*(dEp-dEr)) 234 | a1 = sym.sympify(a1).subs({symbol: 0 for symbol in all_symbols}) 235 | if isinstance(a1, sym.Float) and 0. <= a1 <= 1: 236 | self.alpha = a1 237 | else: 238 | a2 = (2*dEp - 2*dEr - dGf - dGr + sym.sqrt(8*(dEp-dEr)*dGf + (-2*dEp + 2*dEr + dGf + dGr)**2))/(4*(dEp-dEr)) 239 | self.alpha = sym.sympify(a2).subs({symbol: 0 for symbol in all_symbols}) 240 | if not isinstance(self.alpha, sym.Float) or not (0. <= self.alpha <= 1.): 241 | raise RuntimeError("Couldn't find alpha parameter for {}!".format(self)) 242 | 243 | self.dH_act = self.ts.get_H(T) + (1 - self.alpha) * dEr + self.alpha * dEp - self.reactants.get_H(T) 244 | self.dH_act *= self.scale['dH_act'] 245 | self.dS_act = self.ts.get_S(T) - self.reactants.get_S(T) 246 | self.dS_act *= self.scale['dS_act'] 247 | self.dG_act = self.dH_act - self.T * self.dS_act 248 | 249 | # If there is a coverage dependence, assume everything has 250 | # coverage 0 251 | if self.dground: 252 | dG_act = self.dG_act 253 | if isinstance(dG_act, sym.Basic): 254 | subs = {} 255 | for atom in dG_act.atoms(sym.Symbol): 256 | subs[atom] = 0. 257 | dG_act = dG_act.subs(subs) 258 | 259 | if dG_act < 0.: 260 | warnings.warn('Negative activation energy found for {}. ' 261 | 'Rounding to 0.'.format(self), 262 | RuntimeWarning, stacklevel=2) 263 | self.dG_act = 0. 264 | 265 | dG_rev = self.dG_act - self.dG 266 | if isinstance(dG_rev, sym.Basic): 267 | subs = {} 268 | for atom in dG_rev.atoms(sym.Symbol): 269 | subs[atom] = 0. 270 | dG_rev = dG_rev.subs(subs) 271 | 272 | if dG_rev < 0.: 273 | warnings.warn('Negative activation energy found for {}. ' 274 | 'Rounding to {}'.format(self, self.dG), 275 | RuntimeWarning, stacklevel=2) 276 | self.dG_act = self.dG 277 | self._calc_keq() 278 | self._calc_kfor() 279 | self._calc_krev() 280 | self.scale_old = self.scale.copy() 281 | 282 | def is_update_needed(self, T, Asite, L): 283 | for species in self.species: 284 | if species.is_update_needed(T): 285 | return True 286 | if self.keq is None: 287 | return True 288 | if T is not None and T != self.T: 289 | return True 290 | if Asite is not None and Asite != self.Asite: 291 | return True 292 | if L is not None and L != self.L: 293 | return True 294 | for param in self.scale_params: 295 | if self.scale[param] != self.scale_old[param]: 296 | return True 297 | return False 298 | 299 | def get_keq(self, T=None, Asite=None, L=None): 300 | self.update(T, Asite, L) 301 | return self.keq 302 | 303 | def get_kfor(self, T=None, Asite=None, L=None): 304 | self.update(T, Asite, L) 305 | return self.kfor 306 | 307 | def get_krev(self, T=None, Asite=None, L=None): 308 | self.update(T, Asite, L) 309 | return self.krev 310 | 311 | def _calc_keq(self): 312 | self.keq = sym.exp(-self.dG / (kB * self.T)) \ 313 | * self.products.get_reference_state() \ 314 | / self.reactants.get_reference_state() \ 315 | * self.scale['kfor'] / self.scale['krev'] 316 | 317 | def _calc_kfor(self): 318 | barr = 1 319 | if self.dG_act is not None: 320 | barr *= sym.exp(-self.dG_act / (kB * self.T)) \ 321 | / self.reactants.get_reference_state() 322 | # * self.ts.get_reference_state() \ 323 | if self.method == 'EQUIL': 324 | self.kfor = _k * self.T * barr / _hplanck * self.scale['kfor'] 325 | if isinstance(self.keq, sym.Basic): 326 | subs = {} 327 | for atom in self.keq.atoms(sym.Symbol): 328 | subs[atom] = 0. 329 | keq = self.keq.subs(subs) 330 | else: 331 | keq = self.keq 332 | if keq < 1: 333 | self.kfor *= self.keq * self.scale['krev'] / self.scale['kfor'] 334 | elif self.method == 'DIEQUIL': 335 | kfor1 = _k * self.T * barr / _hplanck * self.scale['kfor'] 336 | kfor2 = kfor1 * self.keq * self.scale['krev'] / self.scale['kfor'] 337 | self.kfor = kfor1 * kfor2 / (kfor1 + kfor2) 338 | elif self.method == 'STICK': 339 | # STICK is TST with the transition state being a non-interacting 340 | # 2D ideal gas. 341 | found_fluid = False 342 | for species in self.reactants: 343 | if isinstance(species, _Fluid): 344 | if found_fluid: 345 | raise ValueError("At most one fluid " 346 | "can react with STICK!") 347 | found_fluid = True 348 | fluid = species 349 | Sfluid = fluid.get_S(self.T) 350 | fluid._calc_qtrans2D(self.T, self.Asite) 351 | Strans = Sfluid - fluid.S['elec'] - fluid.S['rot'] - fluid.S['vib'] 352 | Slost = Strans / fluid.S['trans'] 353 | dS = (fluid.S['trans2D'] - fluid.S['trans']) * Slost 354 | dG = fluid.E['trans2D'] - fluid.E['trans'] - self.T * dS 355 | self.kfor = barr * _k * self.T / _hplanck * np.exp(-dG / (kB * self.T)) 356 | self.kfor *= self.scale['kfor'] 357 | elif self.method == 'ER': 358 | # Collision Theory 359 | # kfor = S0 * Asite / (sqrt(2 * pi * m * kB * T)) 360 | m_react = 0. 361 | for species in self.reactants: 362 | if isinstance(species, _Fluid): 363 | m_react += species.atoms.get_masses().sum() 364 | kfor1 = barr * 1000 * self.S0 * _Nav * self.Asite \ 365 | * np.sqrt(_k * self.T * kg / (2 * np.pi * m_react)) \ 366 | * self.scale['kfor'] 367 | 368 | m_prod = 0. 369 | for species in self.products: 370 | if isinstance(species, _Fluid): 371 | m_prod = species.atoms.get_masses().sum() 372 | # FIXME: barr should be different for reverse reaction 373 | krev2 = barr * 1000 * self.S0 * _Nav * self.Asite \ 374 | * np.sqrt(_k * self.T * kg / (2 * np.pi * m_prod)) \ 375 | * self.scale['krev'] 376 | kfor2 = self.keq * krev2 377 | self.kfor = kfor1 * kfor2 / (kfor1 + kfor2) 378 | elif self.method == 'DIFF': 379 | if self.L is None: 380 | raise ValueError("Must provide diffusion length " 381 | "for diffusion reactions!") 382 | found_fluid = False 383 | for species in self.reactants: 384 | if isinstance(species, _Fluid): 385 | if found_fluid: 386 | raise ValueError("Diffusion reaction must " 387 | "have exactly 1 fluid!") 388 | found_fluid = True 389 | D = species.D 390 | sites = 1 391 | for species in self.reactants: 392 | if isinstance(species, Adsorbate): 393 | sites *= species.symbol 394 | for species in self.products: 395 | if not isinstance(species, (Adsorbate, Electron)): 396 | raise ValueError("All products must be adsorbates " 397 | "in diffusion reaction!") 398 | self.kfor = 1000 * D * self.Asite * mol * barr \ 399 | * self.scale['kfor'] / (self.L * sites) 400 | elif self.method == 'DIFF_LIQ': 401 | if len(self.reactants) != 2: 402 | raise ValueError("DIFF_LIQ rate only defined for reactions " 403 | "with exactly two reactants!") 404 | if not self.all_liquid: 405 | raise ValueError("DIFF_LIQ rate only defined for all-liquid " 406 | "phase reactions!") 407 | Rtot = self.reactants[0].R + self.reactants[1].R 408 | Dtot = self.reactants[0].D + self.reactants[1].D 409 | self.kfor = 4 * np.pi * Dtot * Rtot * 1e-10 * 1000 * _Nav 410 | self.kfor *= self.scale['kfor'] 411 | elif self.method == 'TST': 412 | # Transition State Theory 413 | self.kfor = (_k * self.T / _hplanck) * barr * self.scale['kfor'] 414 | else: 415 | raise ValueError("Method {} is not recognized!".format( 416 | self.method)) 417 | 418 | def _calc_krev(self): 419 | self.krev = self.kfor / self.keq 420 | 421 | def __repr__(self): 422 | string = self.reactants.__repr__() + ' <-> ' 423 | if self.ts is not None: 424 | string += self.ts.__repr__() + ' <-> ' 425 | string += self.products.__repr__() 426 | return string 427 | 428 | 429 | class Model(object): 430 | def __init__(self, T, Asite, z=0, lattice=None, reactor='CSTR', rhocat=1): 431 | self.reactions = OrderedDict() 432 | self._reactions = [] 433 | self._species = [] 434 | self.species = OrderedDict() 435 | self.vacancy = [] 436 | self.vacspecies = OrderedDict() 437 | self.solvent = None 438 | self.fixed = [] 439 | self.initialized = False 440 | self.U0 = None 441 | self.rhocat = rhocat 442 | 443 | self.T = T # System temperature 444 | self.Asite = Asite # Area of adsorption site 445 | self._z = z # Diffusion length 446 | self.lattice = lattice 447 | self.reactor = reactor 448 | 449 | def add_reactions(self, reactions): 450 | # Set up list of reactions and species 451 | for name, reaction in reactions.items(): 452 | assert isinstance(reaction, Reaction) 453 | if reaction in self._reactions: 454 | return 455 | self._reactions.append(reaction) 456 | self.reactions[name] = reaction 457 | for species in reaction.species: 458 | self._add_species(species) 459 | if reaction.ts is not None: 460 | for ts in reaction.ts: 461 | ts.lattice = self.lattice 462 | reaction.update(T=self.T, Asite=self.Asite, L=self.z) 463 | 464 | def set_solvent(self, solvent): 465 | # Solvent will not diffuse even in diffusion system 466 | if solvent is not None: 467 | if self.solvent is not None: 468 | warnings.warn('Overriding old solvent {} with {}.' 469 | ''.format(solvent, self.solvent), 470 | RuntimeWarning, stacklevel=2) 471 | if not isinstance(self.species[solvent], Liquid): 472 | raise ValueError("Solvent must be a Liquid!") 473 | self.solvent = solvent 474 | 475 | def set_fixed(self, fixed): 476 | # Fixed species are removed from the differential equations 477 | if isinstance(fixed, str): 478 | fixed = [fixed] 479 | for name in fixed: 480 | if name not in self.fixed: 481 | self.fixed.append(name) 482 | 483 | def _add_species(self, species): 484 | assert isinstance(species, _Thermo) 485 | # Do nothing if we already know about the species 486 | if species in self._species or species in self.vacancy: 487 | return 488 | # Add the species to the list of known species 489 | species.lattice = self.lattice 490 | self.species[species.label] = species 491 | self._species.append(species) 492 | # Add the sites that species occupies to the list of known vacancies. 493 | if species.sites is not None: 494 | for site in species.sites: 495 | if site not in self.vacancy: 496 | if site in self._species: 497 | self._species.remove(site) 498 | self.vacancy.append(site) 499 | self.vacspecies[site] = [species] 500 | else: 501 | self.vacspecies[site].append(species) 502 | 503 | def set_T(self, T): 504 | self._T = T 505 | for reaction in self._reactions: 506 | reaction.update(T=T, Asite=self.Asite) 507 | if self.U0 is not None: 508 | self.set_initial_conditions(self.U0) 509 | 510 | def get_T(self): 511 | return self._T 512 | 513 | T = property(get_T, set_T, doc='Model temperature') 514 | 515 | def set_Asite(self, Asite): 516 | self._Asite = Asite 517 | for reaction in self._reactions: 518 | reaction.update(T=self.T, Asite=Asite) 519 | if self.U0 is not None: 520 | self.set_initial_conditions(self.U0) 521 | 522 | def get_Asite(self): 523 | return self._Asite 524 | 525 | Asite = property(get_Asite, set_Asite, doc='Area of an adsorption site') 526 | 527 | def set_z(self, z): 528 | self._z = z 529 | self.check_diffusion() 530 | for reaction in self._reactions: 531 | reaction.update(L=z) 532 | if self.U0 is not None: 533 | self.set_initial_conditions(self.U0) 534 | 535 | def get_z(self): 536 | return self._z 537 | 538 | z = property(get_z, set_z, doc='Diffusion length') 539 | 540 | def set_lattice(self, lattice): 541 | if isinstance(lattice, Lattice) or lattice is None: 542 | self._lattice = lattice 543 | elif isinstance(lattice, dict): 544 | self._lattice = Lattice(lattice) 545 | else: 546 | raise ValueError('Unable to parse lattice!') 547 | for species in self._species: 548 | species.set_lattice(self.lattice) 549 | if self.U0 is not None: 550 | self.set_initial_conditions(self.U0) 551 | 552 | def get_lattice(self): 553 | return self._lattice 554 | 555 | lattice = property(get_lattice, set_lattice, doc='Model lattice') 556 | 557 | def set_initial_conditions(self, U0): 558 | if self.initialized: 559 | self.finalize() 560 | 561 | # Reorder species such that Liquid -> Gas -> Adsorbate -> Vacancy 562 | # Steady-state species go to the end. 563 | newspecies = [] 564 | for species in self._species: 565 | if isinstance(species, Liquid): 566 | newspecies.append(species) 567 | for species in self._species: 568 | if isinstance(species, (Gas, Electron)): 569 | newspecies.append(species) 570 | for species in self._species: 571 | if isinstance(species, Adsorbate): 572 | newspecies.append(species) 573 | self._species = newspecies 574 | 575 | # Also obtain a list of species that will be variables in the 576 | # differential equations. This excludes fixed species and empty 577 | # sites. 578 | self._variable_species = [] 579 | for species in self._species: 580 | if species.label not in self.fixed + [self.solvent]: 581 | self._variable_species.append(species) 582 | self.nvariables = len(self._variable_species) 583 | 584 | # Start with the incomplete user-provided initial conditions 585 | self.U0 = U0.copy() 586 | 587 | # Initialize counter for vacancies 588 | occsites = {species: 0 for species in self.vacancy} 589 | 590 | for name in self.U0: 591 | try: 592 | species = self.species[name] 593 | except KeyError: 594 | for species in self.vacancy: 595 | if species.label == name: 596 | break 597 | else: 598 | raise ValueError('Species {} is unknown!'.format(name)) 599 | 600 | # Ignore all initial conditions for the number of empty sites 601 | if species in self.vacancy: 602 | warnings.warn('Initial condition for vacancy concentration ' 603 | 'ignored.', RuntimeWarning, stacklevel=2) 604 | continue 605 | 606 | # Throw an error if the user provides the concentration for a 607 | # species we don't know about 608 | if species not in self._species: 609 | raise ValueError("Unknown species {}!".format(species)) 610 | 611 | # If the species occupies a site, add its concentration to the 612 | # occupied sites counter 613 | if species.sites is not None: 614 | for site in species.sites: 615 | occsites[site] += self.U0[name] 616 | 617 | self.dvacdy = np.zeros((len(self.vacancy), self.nvariables), dtype=int) 618 | self.vactot = {} 619 | # Determine what the initial vacancy concentration should be 620 | for i, vac in enumerate(self.vacancy): 621 | name = vac.label 622 | # If a vacancy species is part of the lattice, get its maximum 623 | # concentration from its relative abundance. Otherwise, assume 624 | # it is 1. 625 | if self.lattice is not None and vac in self.lattice.sites: 626 | self.vactot[vac] = self.lattice.ratio[vac] 627 | else: 628 | self.vactot[vac] = 1. 629 | # Make sure there isn't too much stuff occupying each kind of 630 | # site on the surface. 631 | assert occsites[vac] <= self.vactot[vac], \ 632 | "Too many adsorbates on {}!".format(vac) 633 | # Normalize the concentration of empty sites to match the 634 | # appropriate site ratio from the lattice. 635 | self.U0[name] = self.vactot[vac] - occsites[vac] 636 | for j, species in enumerate(self._variable_species): 637 | self.dvacdy[i, j] = -species.sites.count(vac) 638 | 639 | # Populate dictionary of initial conditions for all species 640 | for name, species in self.species.items(): 641 | # Assume concentration of unnamed species is 0 642 | if name not in self.U0: 643 | self.U0[name] = 0. 644 | 645 | # The number of variables that will be in our differential equations 646 | size = len(self._species) 647 | 648 | # This creates a symbol for each species named modelparamX where X 649 | # is a three-digit numerical identifier that corresponds to its 650 | # position in the order of species 651 | self.symbols_all = [] 652 | # symbols_dict a Thermo object and returns its corresponding symbol 653 | self.symbols_dict = OrderedDict() 654 | # symbols ONLY includes species that will be in the differential 655 | # equations. Fixed species are not included in this list 656 | self.symbols = [] 657 | 658 | for species in self._species: 659 | self.symbols_all.append(species.symbol) 660 | self.symbols_dict[species] = species.symbol 661 | 662 | self.symbols = [species.symbol for species in self._variable_species] 663 | 664 | # subs converts a species symbol to either its initial value if 665 | # it is fixed or to a constraint (such as constraining the total 666 | # number of adsorption sites) 667 | subs = {} 668 | 669 | self.vac_sym = np.zeros(len(self.vacancy), dtype=object) 670 | # A vacancy will be represented by the total number of sites 671 | # minus the symbol of each species that occupies one of its sites. 672 | for i, vacancy in enumerate(self.vacancy): 673 | self.vac_sym[i] = self.vactot[vacancy] 674 | for species in self._species: 675 | self.vac_sym[i] -= species.sites.count(vacancy) * species.symbol 676 | 677 | # known_symbols keeps track of user-provided symbols that the 678 | # model has seen, so that symbols referring to species not in 679 | # the model can be later removed. 680 | known_symbols = set() 681 | for species in self._species + self.vacancy: 682 | known_symbols.add(species.symbol) 683 | 684 | # Create the final mass matrix of the proper dimensions 685 | self.M = np.eye(self.nvariables, dtype=int) 686 | 687 | if self.reactor == 'PFR': 688 | for i, species in enumerate(self._variable_species): 689 | if isinstance(species, Adsorbate): 690 | self.M[i, i] = 0 691 | 692 | # algvar tells the solver which variables are differential 693 | # and which are algebraic. It is the diagonal of the mass matrix. 694 | algvar = np.array(self.M.diagonal(), dtype=float) 695 | 696 | # Initialize all rate expressions based on the above symbols 697 | nrxns = len(self._reactions) 698 | # Array of symbolic rate expressions 699 | self.rates = np.zeros(nrxns, dtype=object) 700 | # Array of rate coefficients. 701 | self.dypdr = np.zeros((self.nvariables, nrxns), dtype=float) 702 | 703 | for j, rxn in enumerate(self._reactions): 704 | rate_for = rxn.get_kfor(self.T, self.Asite, self.z) 705 | rate_rev = rxn.get_krev(self.T, self.Asite, self.z) 706 | 707 | for i, species in enumerate(self._variable_species): 708 | rcount = rxn.reactants.species.count(species) 709 | pcount = rxn.products.species.count(species) 710 | self.dypdr[i, j] = -rcount + pcount 711 | if isinstance(species, _Fluid) and rxn.involves_catalyst: 712 | self.dypdr[i, j] *= self.rhocat 713 | 714 | for species in self._species + self.vacancy: 715 | rcount = rxn.reactants.species.count(species) 716 | pcount = rxn.products.species.count(species) 717 | if not isinstance(species, Electron): 718 | rate_for *= species.symbol**rcount 719 | rate_rev *= species.symbol**pcount 720 | 721 | # Overall reaction rate (flux) 722 | self.rates[j] = rate_for 723 | if rxn.reversible: 724 | self.rates[j] -= rate_rev 725 | 726 | 727 | # All symbols referring to unknown species are going to be replaced 728 | # by 0 729 | unknown_symbols = set() 730 | for rate in self.rates: 731 | unknown_symbols.update(rate.atoms(sym.Symbol)) 732 | unknown_symbols -= known_symbols 733 | unknown_symbols -= set(self.symbols_all) 734 | subs.update({symbol: 0 for symbol in unknown_symbols}) 735 | 736 | # Fixed species must have their symbols replaced by their fixed 737 | # initial values. 738 | for species in self._species: 739 | if species.label in self.fixed or species.label == self.solvent: 740 | label = species.label 741 | subs[species.symbol] = self.U0[label] 742 | 743 | # Additionally, fixed species concentrations into rate 744 | # expressions 745 | for i, r in enumerate(self.rates): 746 | self.rates[i] = sym.sympify(r).subs(subs) 747 | 748 | # derivative of rate expressions w.r.t. concentrations and vacancies 749 | self.drdy = np.zeros((nrxns, self.nvariables), dtype=object) 750 | self.drdvac = np.zeros((nrxns, len(self.vacancy)), dtype=object) 751 | for i, rate in enumerate(self.rates): 752 | for j, symbol in enumerate(self.symbols): 753 | self.drdy[i, j] = sym.diff(rate, symbol) 754 | for j, vac in enumerate(self.vacancy): 755 | self.drdvac[i, j] = sym.diff(rate, vac.symbol) 756 | 757 | # Sets up and compiles the Fortran differential equation solving module 758 | self.setup_execs() 759 | 760 | # Convert the dictionary U0 of initial conditions into a list that can 761 | # be used with the Fortran module. 762 | U0 = [] 763 | for symbol in self.symbols: 764 | for species, isymbol in self.symbols_dict.items(): 765 | if symbol == isymbol: 766 | U0.append(self.U0[species.label]) 767 | break 768 | 769 | # Pass initial values to the fortran module 770 | atol = np.array([1e-32] * self.nvariables) 771 | atol += 1e-16 * algvar 772 | self.finitialize(U0, 1e-10, atol, [], [], algvar) 773 | 774 | self.initialized = True 775 | 776 | def setup_execs(self): 777 | from micki.fortran import f90_template, pyf_template 778 | from numpy import f2py 779 | 780 | # y_vec is an array symbol that will represent the species 781 | # concentrations provided by the differential equation solver inside 782 | # the Fortran code (that is, y_vec is an INPUT to the functions that 783 | # calculate the residual, Jacobian, and rate) 784 | y_vec = sym.IndexedBase('y', shape=(self.nvariables,)) 785 | vac_vec = sym.IndexedBase('vac', shape=(len(self.vacancy),)) 786 | # Map y_vec elements (1-indexed, of course) onto 'modelparam' symbols 787 | trans = {self.symbols[i]: y_vec[i + 1] for i in range(self.nvariables)} 788 | trans.update({vac.symbol: y_vec[i + 1] for i, vac in enumerate(self.vacancy)}) 789 | # Map string represntation of 'modelparam' symbols onto string 790 | # representation of y-vec elements 791 | str_trans = {} 792 | for i, symbol in enumerate(self.symbols): 793 | str_trans[sym.fcode(symbol, source_format='free')] = \ 794 | sym.fcode(y_vec[i + 1], source_format='free') 795 | for i, vac in enumerate(self.vacancy): 796 | str_trans[sym.fcode(vac.symbol, source_format='free')] = \ 797 | sym.fcode(vac_vec[i + 1], source_format='free') 798 | 799 | str_list = [key for key in str_trans] 800 | str_list.sort(key=len, reverse=True) 801 | 802 | # these will contain lists of strings, with each element being one 803 | # Fortran assignment for the master equation, Jacobian, and 804 | # rate expressions 805 | dypdrcode = [] 806 | drdycode = [] 807 | ratecode = [] 808 | vaccode = [] 809 | drdvaccode = [] 810 | dvacdycode = [] 811 | 812 | for i, expr in enumerate(self.vac_sym): 813 | fcode = sym.fcode(expr, source_format='free') 814 | for key in str_list: 815 | fcode = fcode.replace(key, str_trans[key]) 816 | vaccode.append(' vac({}) = '.format(i + 1) + fcode) 817 | 818 | for i, row in enumerate(self.drdvac): 819 | for j, elem in enumerate(row): 820 | if elem != 0: 821 | fcode = sym.fcode(elem, source_format='free') 822 | for key in str_list: 823 | fcode = fcode.replace(key, str_trans[key]) 824 | drdvaccode.append(' drdvac({}, {}) = '.format(i + 1, j + 1) + fcode) 825 | 826 | for i, row in enumerate(self.dvacdy): 827 | for j, elem in enumerate(row): 828 | if elem != 0: 829 | dvacdycode.append(' dvacdy({}, {}) = '.format(i+1, j+1) + sym.fcode(elem, source_format='free')) 830 | 831 | for i, row in enumerate(self.dypdr): 832 | for j, elem in enumerate(row): 833 | if elem != 0: 834 | dypdrcode.append(' dypdr({}, {}) = '.format(i+1, j+1) + sym.fcode(elem, source_format='free')) 835 | 836 | # Effectively the same as above, except on the two-dimensional Jacobian 837 | # matrix. 838 | for i, row in enumerate(self.drdy): 839 | for j, elem in enumerate(row): 840 | if elem != 0: 841 | fcode = sym.fcode(elem, source_format='free') 842 | for key in str_list: 843 | fcode = fcode.replace(key, str_trans[key]) 844 | drdycode.append(' drdy({}, {}) = '.format(i + 1, j + 1) + fcode) 845 | 846 | # See residual above 847 | for i, rate in enumerate(self.rates): 848 | fcode = sym.fcode(rate, source_format='free') 849 | for key in str_list: 850 | fcode = fcode.replace(key, str_trans[key]) 851 | ratecode.append(' rates({}) = '.format(i + 1) + fcode) 852 | 853 | # We insert all of the parameters of this differential equation into 854 | # the prewritten Fortran template, including the residual, Jacobian, 855 | # and rate expressions we just calculated. 856 | program = f90_template.format(neq=self.nvariables, nx=1, 857 | nrates=len(self.rates), 858 | nvac=len(self.vacancy), 859 | dypdrcalc='\n'.join(dypdrcode), 860 | drdycalc='\n'.join(drdycode), 861 | ratecalc='\n'.join(ratecode), 862 | vaccalc='\n'.join(vaccode), 863 | drdvaccalc='\n'.join(drdvaccode), 864 | dvacdycalc='\n'.join(dvacdycode), 865 | ) 866 | 867 | # Generate a randomly-named temp directory for compiling the module. 868 | # We will name the actual module file after the directory. 869 | dname = tempfile.mkdtemp() 870 | modname = os.path.split(dname)[1] 871 | fname = modname + '.f90' 872 | pyfname = modname + '.pyf' 873 | 874 | # For debugging purposes, write out the generated module 875 | with open('solve_ida.f90', 'w') as f: 876 | f.write(program) 877 | 878 | # Write the pertinent data into the temp directory 879 | with open(os.path.join(dname, pyfname), 'w') as f: 880 | f.write(pyf_template.format(modname=modname, neq=self.nvariables, 881 | nrates=len(self.rates), nvac=len(self.vacancy))) 882 | 883 | # Compile the module with f2py 884 | lapack = "-lmkl_rt" 885 | if "MICKI_LAPACK" in os.environ: 886 | lapack = os.environ["MICKI_LAPACK"] 887 | os.environ["CFLAGS"] = "-w -std=c99" 888 | output=f2py.compile(program, modulename=modname, verbose=0, 889 | full_output=1, 890 | extra_args='--quiet ' 891 | '--f90flags="-Wno-unused-dummy-argument ' 892 | '-Wno-unused-variable -Wno-unused-func -w" ' 893 | '-lsundials_fida ' 894 | '-lsundials_fnvecserial ' 895 | '-lsundials_ida ' 896 | '-lsundials_fsunlinsollapackdense ' 897 | '-lsundials_sunlinsollapackdense ' 898 | '-lsundials_nvecserial ' + lapack + ' ' + 899 | os.path.join(dname, pyfname), 900 | source_fn=os.path.join(dname, fname)) 901 | if output.returncode != 0: 902 | print(output.stderr) 903 | # Delete the temporary directory 904 | shutil.rmtree(dname) 905 | 906 | # Import the module on-the-fly with __import__. This is kind of a hack. 907 | solve_ida = __import__(modname) 908 | self._solve_ida = solve_ida 909 | 910 | # The Fortran module's initialize, solve, and finalize routines 911 | # are mapped onto finitialize, fsolve, and ffinalize inside the Model 912 | # object. We don't want users touching these manually 913 | self.finitialize = solve_ida.initialize 914 | self.ffind_steady_state = solve_ida.find_steady_state 915 | self.fsolve = solve_ida.solve 916 | self.ffinalize = solve_ida.finalize 917 | 918 | # Delete the module file. We've already imported it, so it's in memory. 919 | library=glob.glob(modname + '*.so')[0] 920 | os.remove(library) 921 | 922 | def _out_array_to_dict(self, U, dU, r): 923 | Ui = {} 924 | dUi = {} 925 | ri = {} 926 | fixed = self.fixed 927 | if self.solvent is not None: 928 | fixed += [self.solvent] 929 | for name in fixed: 930 | dUi[name] = 0. 931 | Ui[name] = self.U0[name] 932 | for j, symbol in enumerate(self.symbols): 933 | for species, isymbol in self.symbols_dict.items(): 934 | if symbol == isymbol: 935 | Ui[species.label] = U[j] 936 | dUi[species.label] = dU[j] 937 | for vacancy in self.vacancy: 938 | Ui[vacancy.label] = self.vactot[vacancy] 939 | for species in self.vacspecies[vacancy]: 940 | Ui[vacancy.label] -= Ui[species.label] 941 | dUi[vacancy.label] = 0 942 | 943 | j = 0 944 | rxn_to_name = {} 945 | for name, reaction in self.reactions.items(): 946 | rxn_to_name[reaction] = name 947 | for reaction in self._reactions: 948 | ri[rxn_to_name[reaction]] = r[j] 949 | j += 1 950 | 951 | return Ui, dUi, ri 952 | 953 | def find_steady_state(self, dt=60, maxiter=2000, epsilon=1e-8): 954 | t, U1, dU1, r1 = self.ffind_steady_state(self.nvariables, 955 | len(self.rates), 956 | dt, 957 | maxiter, 958 | epsilon) 959 | self.t = t 960 | self.U = [] 961 | self.dU = [] 962 | self.r = [] 963 | U, dU, r = self._out_array_to_dict(U1.T, dU1.T, r1.T) 964 | self.U.append(U) 965 | self.dU.append(dU) 966 | self.r.append(r) 967 | self.check_rates(U) 968 | return t, U, r 969 | 970 | def solve(self, t, ncp): 971 | self.t, U1, dU1, r1 = self.fsolve(self.nvariables, 972 | len(self.rates), ncp, t) 973 | self.U1 = U1.T 974 | self.dU1 = dU1.T 975 | self.r1 = r1.T 976 | self.U = [] 977 | self.dU = [] 978 | self.r = [] 979 | for i, t in enumerate(self.t): 980 | Ui, dUi, ri = self._out_array_to_dict(self.U1[i], self.dU1[i], 981 | self.r1[i]) 982 | self.U.append(Ui) 983 | self.dU.append(dUi) 984 | self.r.append(ri) 985 | self.check_rates(self.U[-1]) 986 | return self.U, self.r 987 | 988 | def finalize(self): 989 | self.initialized = False 990 | # self.ffinalize() 991 | 992 | def check_rates(self, U, epsilon=1e-6): 993 | symbol_to_coverage = {} 994 | for name, Ui in U.items(): 995 | if name in self.species and self.species[name].symbol is not None: 996 | symbol_to_coverage[self.species[name].symbol] = Ui 997 | 998 | for species in self.vacancy: 999 | if species.label in U and species.symbol is not None: 1000 | symbol_to_coverage[species.symbol] = U[species.label] 1001 | 1002 | for name, reaction in self.reactions.items(): 1003 | kfor = sym.sympify(reaction.kfor).subs(symbol_to_coverage) 1004 | krev = sym.sympify(reaction.krev).subs(symbol_to_coverage) 1005 | kmax = _k * self.T / _hplanck 1006 | for k, word in [(kfor, "Forwards"), (krev, "Reverse")]: 1007 | ratio = k / kmax 1008 | if (ratio - 1.0) > 1e-6: 1009 | warnings.warn(word + " rate constant for {} is too large! " 1010 | "Value is {} kB T / h (should be <= 1)." 1011 | "".format(reaction, ratio), 1012 | RuntimeWarning, stacklevel=2) 1013 | 1014 | def copy(self, initialize=True): 1015 | newmodel = Model(self.T, self.Asite, self.z, self.lattice, self.rhocat) 1016 | newmodel.add_reactions(self.reactions) 1017 | newmodel.set_fixed(self.fixed) 1018 | newmodel.set_solvent(self.solvent) 1019 | if initialize: 1020 | newmodel.set_initial_conditions(self.U0) 1021 | return newmodel 1022 | -------------------------------------------------------------------------------- /micki/reactants.py: -------------------------------------------------------------------------------- 1 | """This module contains object definitions of species 2 | and collections of species""" 3 | 4 | import copy 5 | import warnings 6 | import numpy as np 7 | 8 | from sympy import Symbol 9 | 10 | from ase import Atoms 11 | from ase.io import read 12 | from ase.db import connect 13 | from ase.db.row import AtomsRow 14 | from ase.units import J, mol, _hplanck, m, kg, _k, kB, _c, Pascal, _Nav 15 | 16 | from micki.masses import masses 17 | from micki.io import parse_vasp_out 18 | from micki.utils import calculate_avg_vdw_radius 19 | 20 | 21 | class _Thermo(object): 22 | """Generic thermodynamics object 23 | 24 | This is the base object that all reactant objects inherit from. 25 | It initializes many parameters and provides methods for calculating 26 | the partition function from translation, rotation, and vibration.""" 27 | 28 | def __init__(self): 29 | self.T = None 30 | 31 | self.mode = ['tot', 'trans', 'trans2D', 'rot', 'vib', 'elec'] 32 | 33 | self.q = dict.fromkeys(self.mode) 34 | self.S = dict.fromkeys(self.mode) 35 | self.E = dict.fromkeys(self.mode) 36 | self.H = None 37 | 38 | self.scale = {'E': dict.fromkeys(self.mode, 1.0), 39 | 'S': dict.fromkeys(self.mode, 1.0), 40 | 'H': 1.0} 41 | self.scale_old = copy.deepcopy(self.scale) 42 | 43 | self.atoms = None 44 | self.metal = None 45 | self.eref = None 46 | self.potential_energy = 0. 47 | self.symm = 1 48 | self.spin = 0. 49 | self.ts = False 50 | self.label = None 51 | self.lateral = 0. 52 | self.dE = 0. 53 | self.sites = [] 54 | self.lattice = None 55 | self.D = None 56 | self.Sliq = None 57 | self.rho0 = 1. 58 | self.freqs = [] 59 | 60 | def set_atoms(self, atoms): 61 | if atoms is None: 62 | self._atoms = atoms 63 | return 64 | elif isinstance(atoms, AtomsRow): 65 | self._atoms = atoms.toatoms() 66 | self.freqs = atoms.data.get('freqs') 67 | elif isinstance(atoms, Atoms): 68 | self._atoms = atoms 69 | elif isinstance(atoms, str): 70 | # TODO: make this more robust (catch/handle errors) 71 | a, f = parse_vasp_out(atoms) 72 | self._atoms = a 73 | self.freqs = f 74 | else: 75 | raise ValueError("Unrecognized atoms object!") 76 | self.mass = [masses[atom.symbol] for atom in self.atoms] 77 | self.atoms.set_masses(self.mass) 78 | self.update_potential_energy() 79 | 80 | def get_atoms(self): 81 | return self._atoms 82 | 83 | atoms = property(get_atoms, set_atoms) 84 | 85 | def set_reference(self, reference): 86 | self._eref = reference 87 | self.update_potential_energy() 88 | 89 | def get_reference(self): 90 | return self._eref 91 | 92 | eref = property(get_reference, set_reference) 93 | 94 | def update_potential_energy(self): 95 | if self.atoms is None or len(self.atoms) == 0: 96 | self.potential_energy = 0. 97 | else: 98 | self.potential_energy = self.atoms.get_potential_energy() 99 | if self.eref is not None: 100 | for element in self.atoms.get_chemical_symbols(): 101 | self.potential_energy -= self.eref[element] 102 | 103 | def set_sites(self, sites): 104 | if isinstance(sites, list): 105 | self._sites = sites 106 | elif isinstance(sites, Adsorbate): 107 | self._sites = [sites] 108 | else: 109 | raise ValueError("Invalid format for adsorption sites") 110 | 111 | def get_sites(self): 112 | return self._sites 113 | 114 | sites = property(get_sites, set_sites) 115 | 116 | def set_freqs(self, freqs): 117 | if freqs is not None: 118 | self._freqs = np.array(freqs) 119 | 120 | def get_freqs(self): 121 | return self._freqs 122 | 123 | freqs = property(get_freqs, set_freqs) 124 | 125 | def set_label(self, label): 126 | self._label = label 127 | if label is None: 128 | self._symbol = None 129 | else: 130 | self._symbol = Symbol(label) 131 | 132 | def get_label(self): 133 | return self._label 134 | 135 | label = property(get_label, set_label) 136 | 137 | def get_symbol(self): 138 | return self._symbol 139 | 140 | symbol = property(get_symbol, None) 141 | 142 | def update(self, T=None, force=False): 143 | """Updates the object's thermodynamic properties""" 144 | if not self.is_update_needed(T) and not force: 145 | return 146 | 147 | if T is None: 148 | T = self.T 149 | 150 | self.T = T 151 | self._calc_q(T) 152 | self.scale_old = copy.deepcopy(self.scale) 153 | 154 | def is_update_needed(self, T): 155 | if self.q['tot'] is None: 156 | return True 157 | if T is not None and T != self.T: 158 | return True 159 | if self.scale != self.scale_old: 160 | return True 161 | return False 162 | 163 | def get_H(self, T=None): 164 | self.update(T) 165 | return (self.H + self.lateral) * self.scale['H'] 166 | 167 | def get_S(self, T=None): 168 | self.update(T) 169 | return self.S['tot'] * self.scale['S']['tot'] 170 | 171 | def get_G(self, T=None): 172 | self.update(T) 173 | return self.get_H(T) - T * self.get_S(T) 174 | 175 | def get_E(self, T=None): 176 | self.update(T) 177 | return (self.E['tot'] + self.lateral) * self.scale['E']['tot'] 178 | 179 | def get_q(self, T=None): 180 | self.update(T) 181 | return self.q['tot'] 182 | 183 | def get_reference_state(self): 184 | raise NotImplementedError 185 | 186 | def save_to_db(self, db): 187 | if isinstance(db, str): 188 | db = connect(db) 189 | elif not isinstance(db, Database): 190 | raise ValueError("Must pass active ASE DB connection, or name of ASE DB file!") 191 | 192 | data = {'freqs': self.freqs, 193 | 'ts': self.ts, 194 | 'symm': self.symm, 195 | 'spin': self.spin, 196 | 'D': self.D, 197 | 'S': self.Sliq, 198 | 'rhoref': self.rho0, 199 | 'sites': [site.label for site in self.sites], 200 | 'dE': self.dE} 201 | 202 | if isinstance(self, Adsorbate): 203 | data['thermo'] = 'Adsorbate' 204 | elif isinstance(self, Gas): 205 | data['thermo'] = 'Gas' 206 | elif isinstance(self, Liquid): 207 | data['thermo'] = 'Liquid' 208 | else: 209 | raise ValueError("Unknown Thermo object type {}".format(type(self))) 210 | 211 | if self.lattice: 212 | warnings.warn('Lattice cannot be stored in a db! You must recreate ' 213 | 'the lattice when you re-use this species.', 214 | RuntimeWarning, stacklevel=2) 215 | 216 | if self.eref: 217 | warnings.warn('Energy reference cannot be stored in a db! You must ' 218 | 'recreate the energy reference when you re-use this ' 219 | 'species.', RuntimeWarning, stacklevel=2) 220 | 221 | if self.lateral != 0.: 222 | warnings.warn('Coverage dependence cannot be stored in a db! You ' 223 | 'must recreate the coverage dependence when you ' 224 | 're-use this species.', RuntimeWarning, stacklevel=2) 225 | 226 | db.write(self.atoms, name=self.label, data=data) 227 | 228 | def _calc_q(self, T): 229 | raise NotImplementedError 230 | 231 | def _calc_qtrans2D(self, T, A): 232 | mtot = sum(self.mass) / kg 233 | self.q['trans2D'] = 2 * np.pi * mtot * _k * T / _hplanck**2 * A 234 | self.E['trans2D'] = kB * T * self.scale['E']['trans2D'] 235 | self.S['trans2D'] = kB * (2. + np.log(self.q['trans2D'])) * \ 236 | self.scale['S']['trans2D'] 237 | 238 | def _calc_qtrans(self, T): 239 | mtot = sum(self.mass) / kg 240 | self.q['trans'] = 0.001*(2*np.pi*mtot*_k*T/_hplanck**2)**(3./2.) \ 241 | / (mol * self.rho0) 242 | self.E['trans'] = 3. * kB * T / 2. * self.scale['E']['trans'] 243 | self.S['trans'] = kB * (5./2. + np.log(self.q['trans'])) * \ 244 | self.scale['S']['trans'] 245 | 246 | def _calc_qrot(self, T): 247 | com = self.atoms.get_center_of_mass() 248 | if self.linear: 249 | I = 0 250 | for atom in self.atoms: 251 | I += atom.mass * np.linalg.norm(atom.position - com)**2 252 | I /= (kg * m**2) 253 | self.q['rot'] = 8*np.pi**2*I*_k*T/(_hplanck**2*self.symm) 254 | self.E['rot'] = kB * T * self.scale['E']['rot'] 255 | self.S['rot'] = kB * (1. + np.log(self.q['rot'])) * \ 256 | self.scale['S']['rot'] 257 | else: 258 | I = self.atoms.get_moments_of_inertia() / (kg * m**2) 259 | thetarot = _hplanck**2 / (8 * np.pi**2 * I * _k) 260 | self.q['rot'] = np.sqrt(np.pi*T**3/np.prod(thetarot))/self.symm 261 | self.E['rot'] = 3. * kB * T / 2. * self.scale['E']['rot'] 262 | self.S['rot'] = kB * (3./2. + np.log(self.q['rot'])) * \ 263 | self.scale['S']['rot'] 264 | 265 | def _calc_qvib(self, T, ncut=0): 266 | thetavib = self.freqs[ncut:] / kB 267 | self.q['vib'] = np.prod(np.exp(-thetavib/(2. * T)) / 268 | (1. - np.exp(-thetavib/T))) 269 | self.E['vib'] = kB * sum(thetavib * 270 | (1./2. + 1./(np.exp(thetavib/T) - 1.))) * \ 271 | self.scale['E']['vib'] 272 | self.S['vib'] = kB * sum((thetavib/T)/(np.exp(thetavib/T) - 1.) - 273 | np.log(1. - np.exp(-thetavib/T))) * \ 274 | self.scale['S']['vib'] 275 | 276 | def _calc_qelec(self, T): 277 | self.E['elec'] = self.potential_energy + self.dE 278 | self.E['elec'] *= self.scale['E']['elec'] 279 | self.S['elec'] = kB * np.log(2. * self.spin + 1.) * \ 280 | self.scale['S']['elec'] 281 | 282 | def _is_linear(self): 283 | pos = self.atoms.get_positions() 284 | vecs = pos[1:] - pos[0] 285 | for vec in vecs[1:]: 286 | if np.linalg.norm(np.cross(vecs[0], vec)) > 1e-8: 287 | return False 288 | return True 289 | 290 | def copy(self): 291 | raise NotImplementedError 292 | 293 | def __repr__(self): 294 | if self.label is not None: 295 | return self.label 296 | else: 297 | return self.atoms.get_chemical_formula() 298 | 299 | def __add__(self, other): 300 | return _Reactants([self, other]) 301 | 302 | def __iadd__(self, other): 303 | raise NotImplementedError 304 | 305 | def __mul__(self, factor): 306 | assert isinstance(factor, int) 307 | return _Reactants([self for i in range(factor)]) 308 | 309 | def __rmul__(self, factor): 310 | return self.__mul__(factor) 311 | 312 | 313 | class _Fluid(_Thermo): 314 | """Master object for both liquids and gasses""" 315 | def __init__(self, atoms, label, freqs=None, symm=1, spin=0., 316 | eref=None, rhoref=1., dE=0.): 317 | _Thermo.__init__(self) 318 | self.atoms = atoms 319 | self.freqs = freqs 320 | self.label = label 321 | self.symm = symm 322 | self.spin = spin 323 | self.eref = eref 324 | self.linear = self._is_linear() 325 | self.ncut = 6 - self.linear + self.ts 326 | self.rho0 = rhoref 327 | self.dE = dE 328 | self._R = None 329 | assert np.all(self.freqs[self.ncut:] > 0), \ 330 | "Extra imaginary frequencies found!" 331 | 332 | def get_reference_state(self): 333 | return self.rho0 334 | 335 | def copy(self, newlabel=None): 336 | label = self.label 337 | if newlabel is not None: 338 | label = newlabel 339 | return self.__class__(self.atoms, label, self.freqs, 340 | self.symm, self.spin, self.eref, 341 | self.rhoref, self.dE) 342 | 343 | def _calc_q(self, T): 344 | self._calc_qelec(T) 345 | self._calc_qtrans(T) 346 | self._calc_qrot(T) 347 | self._calc_qvib(T, ncut=self.ncut) 348 | self.q['tot'] = self.q['trans'] * self.q['rot'] * self.q['vib'] 349 | self.E['tot'] = self.E['elec'] + self.E['trans'] + self.E['rot'] + \ 350 | self.E['vib'] 351 | self.H = self.E['tot'] #+ kB * T 352 | self.S['tot'] = self.S['elec'] + self.S['trans'] + self.S['rot'] + \ 353 | self.S['vib'] 354 | 355 | def get_R(self): 356 | if self._R is None: 357 | self._R = calculate_avg_vdw_radius(self.atoms) 358 | return self._R 359 | 360 | R = property(get_R, None) 361 | 362 | 363 | class Electron(_Thermo): 364 | def __init__(self, E, self_repulsion, label): 365 | _Thermo.__init__(self) 366 | self.atoms = Atoms() 367 | self.potential_energy = E 368 | self.label = label 369 | self.lateral = self_repulsion * self.symbol 370 | 371 | def get_reference_state(self): 372 | return 1. 373 | 374 | def copy(self, newlabel=None): 375 | label = self.label 376 | if newlabel is not None: 377 | label = newlabel 378 | return self.__class(self.potential_energy, self.lateral, label) 379 | 380 | def _calc_q(self, T): 381 | self._calc_qelec(T) 382 | if self.q['elec'] is None: 383 | self.q['elec'] = 1. 384 | self.q['tot'] = self.q['elec'] 385 | self.E['tot'] = self.E['elec'] 386 | self.H = self.E['tot'] 387 | self.S['tot'] = self.S['elec'] 388 | 389 | 390 | class Gas(_Fluid): 391 | pass 392 | 393 | 394 | class Liquid(_Fluid): 395 | def __init__(self, atoms, label, freqs=None, symm=1, 396 | spin=0., eref=None, rhoref=1., S=None, D=None, dE=0.): 397 | _Fluid.__init__(self, atoms, label, freqs, symm, spin, eref, 398 | rhoref, dE) 399 | self.Sliq = S 400 | self.D = D 401 | 402 | def _calc_q(self, T): 403 | _Fluid._calc_q(self, T) 404 | # if self.Sliq is None: 405 | # # Use Trouton's Rule 406 | # self.S['tot'] -= (4.5 + np.log(T)) * kB 407 | # else: 408 | # self.S['tot'] = self.Sliq 409 | 410 | def copy(self, newlabel=None): 411 | label = self.label 412 | if newlabel is not None: 413 | label = newlabel 414 | return self.__class__(self.atoms, label, self.freqs, 415 | self.symm, self.spin, self.eref, 416 | self.rhoref, self.Sliq, self.D, self.dE) 417 | 418 | 419 | class Adsorbate(_Thermo): 420 | def __init__(self, atoms, label, freqs=None, ts=None, 421 | spin=0., sites=[], lattice=None, eref=None, dE=0., 422 | symm=1): 423 | _Thermo.__init__(self) 424 | self.atoms = atoms 425 | self.freqs = freqs 426 | self.label = label 427 | self.ts = ts 428 | self.spin = spin 429 | self.sites = sites 430 | self.lattice = lattice 431 | self.eref = eref 432 | self.dE = dE 433 | self.symm = symm 434 | assert np.all(self.freqs[1 if ts else 0:] > 0), \ 435 | "Imaginary frequencies found!" 436 | 437 | def get_reference_state(self): 438 | return 1. 439 | 440 | def _calc_q(self, T): 441 | self._calc_qvib(T, ncut=1 if self.ts else 0) 442 | self._calc_qelec(T) 443 | self.q['tot'] = self.q['vib'] 444 | self.E['tot'] = self.E['elec'] + self.E['vib'] 445 | self.H = self.E['tot'] 446 | self.S['tot'] = self.S['elec'] + self.S['vib'] 447 | self.S['tot'] += kB * np.log(self.symm) 448 | if self.lattice is not None: 449 | self.S['tot'] += self.lattice.get_S_conf(self.sites) 450 | 451 | 452 | def copy(self, newlabel=None): 453 | label = self.label 454 | if newlabel is not None: 455 | label = newlabel 456 | return self.__class__(self.atoms, label, self.freqs, 457 | self.ts, self.spin, self.sites, 458 | self.lattice, self.eref, self.dE, 459 | self.symm) 460 | 461 | 462 | class Shomate(_Thermo): 463 | def __init__(self): 464 | raise NotImplementedError 465 | 466 | 467 | class _Reactants(object): 468 | def __init__(self, species): 469 | self.species = [] 470 | self.elements = {} 471 | for i, other in enumerate(species): 472 | if isinstance(other, _Reactants): 473 | # If we're adding a _Reactants object to another 474 | # _Reactants object, just merge species and elements. 475 | self.species += other.species 476 | for key in other.elements: 477 | if key in self.elements: 478 | self.elements[key] += other.elements[key] 479 | else: 480 | self.elements[key] = other.elements[key] 481 | 482 | elif isinstance(other, _Thermo): 483 | # If we're adding a _Thermo object to a reactants 484 | # object, append the _Thermo to species and update 485 | # elements 486 | self.species.append(other) 487 | if isinstance(other, Shomate): 488 | for symbol in other.elements: 489 | if symbol in self.elements: 490 | self.elements[symbol] += other.elements[symbol] 491 | else: 492 | self.elements[symbol] = other.elements[symbol] 493 | else: 494 | for symbol in other.atoms.get_chemical_symbols(): 495 | if symbol in self.elements: 496 | self.elements[symbol] += 1 497 | else: 498 | self.elements[symbol] = 1 499 | 500 | else: 501 | raise NotImplementedError 502 | self.reference_state = 1. 503 | for species in self.species: 504 | self.reference_state *= species.get_reference_state() 505 | 506 | def get_H(self, T=None): 507 | H = 0. 508 | for species in self.species: 509 | H += species.get_H(T) 510 | return H 511 | 512 | def get_S(self, T=None): 513 | S = 0. 514 | for species in self.species: 515 | S += species.get_S(T) 516 | return S 517 | 518 | def get_G(self, T=None): 519 | G = 0. 520 | for species in self.species: 521 | G += species.get_G(T) 522 | return G 523 | 524 | def get_E(self, T=None): 525 | E = 0. 526 | for species in self.species: 527 | E += species.get_E(T) 528 | return E 529 | 530 | def get_q(self, T=None): 531 | q = 1. 532 | for species in self.species: 533 | q *= species.get_q(T) 534 | return q 535 | 536 | def get_reference_state(self): 537 | return self.reference_state 538 | 539 | def copy(self): 540 | return self.__class__(self.species) 541 | 542 | def get_mass(self): 543 | mtot = 0 544 | for species in self.species: 545 | mtot += species.atoms.get_masses().sum() 546 | return mtot 547 | 548 | def __iadd__(self, other): 549 | if isinstance(other, _Reactants): 550 | self.species += other.species 551 | for key in other.elements: 552 | if key in self.elements: 553 | self.elements[key] += other.elements[key] 554 | else: 555 | self.elements[key] = other.elements[key] 556 | 557 | elif isinstance(other, _Thermo): 558 | self.species.append(other) 559 | for symbol in other.atoms.get_chemical_symbols(): 560 | if symbol in self.elements: 561 | self.elements[symbol] += 1 562 | else: 563 | self.elements[symbol] = 1 564 | else: 565 | raise NotImplementedError 566 | return self 567 | 568 | def __add__(self, other): 569 | return _Reactants([self, other]) 570 | 571 | def __imul__(self, factor): 572 | assert isinstance(factor, int) and factor > 0 573 | self.species *= factor 574 | for key in self.elements: 575 | self.elements[key] *= factor 576 | return self 577 | 578 | def __mul__(self, factor): 579 | assert isinstance(factor, int) and factor > 0 580 | new = self.copy() 581 | new *= factor 582 | return new 583 | 584 | def __rmul__(self, factor): 585 | return self.__mul__(factor) 586 | 587 | def __repr__(self): 588 | return ' + '.join([species.__repr__() for species in self.species]) 589 | 590 | def __getitem__(self, i): 591 | return self.species[i] 592 | 593 | def __getslice__(self, i, j): 594 | return _Reactants([self.species[i:j]]) 595 | 596 | def __len__(self): 597 | return len(self.species) 598 | -------------------------------------------------------------------------------- /micki/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | 5 | import numpy as np 6 | from ase.data import vdw_radii 7 | 8 | def calculate_avg_vdw_radius(atoms, npoints=8001): 9 | if npoints % 2 == 0: 10 | npoints += 1 11 | 12 | vecs = np.zeros((npoints, 3)) 13 | 14 | # Create a Fibonacci spiral 15 | offset = 2 / npoints 16 | increment = np.pi * (3 - np.sqrt(5)) 17 | for i in range(npoints): 18 | y = i * offset - 1 + offset/2 19 | r = np.sqrt(1 - y**2) 20 | phi = (i + 1 % npoints) * increment 21 | x = np.cos(phi) * r 22 | z = np.sin(phi) * r 23 | vecs[i] = np.array([x, y, z]) 24 | 25 | natoms = len(atoms) 26 | pos = atoms.get_positions() 27 | pos -= atoms.get_center_of_mass() 28 | rad = np.array([vdw_radii[atom.number] for atom in atoms]) 29 | 30 | Rmol = np.zeros(npoints) 31 | for i, vec in enumerate(vecs): 32 | for j in range(natoms): 33 | Xparr = vec * np.dot(pos[j], vec) 34 | Xperp = pos[j] - Xparr 35 | Ratom = rad[j] 36 | Rperp = np.linalg.norm(Xperp) 37 | # Skip atom if line doesn't intersect its vdW sphere 38 | if Rperp > Ratom: 39 | continue 40 | Rparr = np.linalg.norm(Xparr) * np.sign(np.dot(pos[j], vec)) 41 | b = -2 * Rperp 42 | c = Rperp**2 - Ratom**2 43 | d1 = np.abs((-b + np.sqrt(b**2 - 4 * c))/2) 44 | d2 = np.abs((-b - np.sqrt(b**2 - 4 * c))/2) 45 | if d1 > Ratom and d2 < Ratom: 46 | d = d2 47 | elif d1 < Ratom and d2 > Ratom: 48 | d = d1 49 | elif d1 > Ratom and d2 > Ratom: 50 | raise ValueError('Error! Both distances greater than Ratom!') 51 | elif d1 < Ratom and d2 < Ratom: 52 | raise ValueError('Error! Both distances less than Ratom!') 53 | else: 54 | raise RuntimeError("This should be impossible! d1 = {}, d2 = {}, Ratom = {}".format(d1, d2, Ratom)) 55 | if Rparr + d > Rmol[i]: 56 | Rmol[i] = Rparr + d 57 | return np.average(Rmol) 58 | --------------------------------------------------------------------------------