├── .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 |
--------------------------------------------------------------------------------