micki 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- micki/__init__.py +8 -0
- micki/analysis.py +287 -0
- micki/db.py +80 -0
- micki/eref.py +85 -0
- micki/io.py +138 -0
- micki/lattice.py +92 -0
- micki/masses.py +244 -0
- micki/model.py +902 -0
- micki/reactants.py +581 -0
- micki/solver.py +452 -0
- micki/utils.py +38 -0
- micki-2.0.0.dist-info/METADATA +66 -0
- micki-2.0.0.dist-info/RECORD +16 -0
- micki-2.0.0.dist-info/WHEEL +5 -0
- micki-2.0.0.dist-info/licenses/LICENSE +165 -0
- micki-2.0.0.dist-info/top_level.txt +1 -0
micki/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
|
|
7
|
+
__all__ = ['Liquid', 'Gas', 'Adsorbate', 'Electron', 'Reaction', 'Model',
|
|
8
|
+
'EnergyReference', 'ModelAnalysis', 'Lattice']
|
micki/analysis.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Module for doing sensitivity analysis of microkinetic model"""
|
|
2
|
+
|
|
3
|
+
import collections.abc
|
|
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
|
+
|
|
12
|
+
|
|
13
|
+
class ModelAnalysis:
|
|
14
|
+
def __init__(self, model, product_reaction, Uequil, tol=1e-3, dt=3600):
|
|
15
|
+
self.model = model
|
|
16
|
+
self.reaction_name = product_reaction
|
|
17
|
+
self.product_reaction = model.reactions[product_reaction]
|
|
18
|
+
self.Uequil = Uequil
|
|
19
|
+
self.tol = tol
|
|
20
|
+
self.dt = dt
|
|
21
|
+
|
|
22
|
+
self.model.set_initial_conditions(self.Uequil)
|
|
23
|
+
|
|
24
|
+
t, self.U, self.r = self.model.find_steady_state()
|
|
25
|
+
model.finalize()
|
|
26
|
+
|
|
27
|
+
self.species_symbols = []
|
|
28
|
+
for species in self.model._species:
|
|
29
|
+
if species.symbol is not None:
|
|
30
|
+
self.species_symbols.append(species)
|
|
31
|
+
self.rmid = self.r[self.reaction_name]
|
|
32
|
+
|
|
33
|
+
def campbell_rate_control(self, rxn_name, scale=0.001):
|
|
34
|
+
reaction = self.model.reactions[rxn_name]
|
|
35
|
+
|
|
36
|
+
subs = {}
|
|
37
|
+
for species in self.species_symbols:
|
|
38
|
+
subs[species.symbol] = self.U[species.label]
|
|
39
|
+
|
|
40
|
+
kmid = reaction.get_kfor(self.model.T,
|
|
41
|
+
self.model.Asite,
|
|
42
|
+
self.model.z)
|
|
43
|
+
|
|
44
|
+
if isinstance(kmid, sym.Basic):
|
|
45
|
+
kmid = kmid.subs(subs)
|
|
46
|
+
|
|
47
|
+
reaction.set_scale('kfor', 1.0 - scale)
|
|
48
|
+
reaction.set_scale('krev', 1.0 - scale)
|
|
49
|
+
reaction.update(self.model.T,
|
|
50
|
+
self.model.Asite,
|
|
51
|
+
self.model.z,
|
|
52
|
+
force=True)
|
|
53
|
+
klow = reaction.get_kfor()
|
|
54
|
+
model = self.model.copy()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
t1, U1, r1 = model.find_steady_state()
|
|
58
|
+
finally:
|
|
59
|
+
reaction.set_scale('kfor', 1.0)
|
|
60
|
+
reaction.set_scale('krev', 1.0)
|
|
61
|
+
|
|
62
|
+
model.finalize()
|
|
63
|
+
rlow = r1[self.reaction_name]
|
|
64
|
+
if isinstance(klow, sym.Basic):
|
|
65
|
+
subs = {}
|
|
66
|
+
for species in self.species_symbols:
|
|
67
|
+
subs[species.symbol] = U1[species.label]
|
|
68
|
+
klow = klow.subs(subs)
|
|
69
|
+
|
|
70
|
+
reaction.set_scale('kfor', 1.0 + scale)
|
|
71
|
+
reaction.set_scale('krev', 1.0 + scale)
|
|
72
|
+
reaction.update(self.model.T,
|
|
73
|
+
self.model.Asite,
|
|
74
|
+
self.model.z,
|
|
75
|
+
force=True)
|
|
76
|
+
khigh = reaction.get_kfor()
|
|
77
|
+
model = self.model.copy()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
t2, U2, r2 = model.find_steady_state()
|
|
81
|
+
finally:
|
|
82
|
+
reaction.set_scale('kfor', 1.0)
|
|
83
|
+
reaction.set_scale('krev', 1.0)
|
|
84
|
+
|
|
85
|
+
model.finalize()
|
|
86
|
+
rhigh = r2[self.reaction_name]
|
|
87
|
+
if isinstance(khigh, sym.Basic):
|
|
88
|
+
subs = {}
|
|
89
|
+
for species in self.species_symbols:
|
|
90
|
+
subs[species.symbol] = U2[species.label]
|
|
91
|
+
khigh = khigh.subs(subs)
|
|
92
|
+
reaction.set_scale('kfor', 1.0)
|
|
93
|
+
reaction.set_scale('krev', 1.0)
|
|
94
|
+
|
|
95
|
+
return kmid * (rhigh - rlow) / (self.rmid * (khigh - klow))
|
|
96
|
+
|
|
97
|
+
def thermodynamic_rate_control(self, names, dg=None):
|
|
98
|
+
"""Degree of thermodynamic rate control, -(kT/r) dr/dG.
|
|
99
|
+
|
|
100
|
+
The free energies of all species in `names` are shifted together
|
|
101
|
+
by -dg and +dg (default 0.001 kT) and the derivative is taken by
|
|
102
|
+
central differences.
|
|
103
|
+
"""
|
|
104
|
+
T = self.model.T
|
|
105
|
+
if dg is None:
|
|
106
|
+
dg = 0.001 * kB * T
|
|
107
|
+
|
|
108
|
+
if not isinstance(names, (list, tuple)):
|
|
109
|
+
species = [self.model.species[names]]
|
|
110
|
+
else:
|
|
111
|
+
species = [self.model.species[name] for name in names]
|
|
112
|
+
|
|
113
|
+
for sp in species:
|
|
114
|
+
sp.dE -= dg
|
|
115
|
+
|
|
116
|
+
for reaction in self.model._reactions:
|
|
117
|
+
reaction.update(T=self.model.T,
|
|
118
|
+
Asite=self.model.Asite,
|
|
119
|
+
L=self.model.z,
|
|
120
|
+
force=True)
|
|
121
|
+
|
|
122
|
+
model = self.model.copy(initialize=False)
|
|
123
|
+
model.set_initial_conditions(self.U)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
t1, U1, r1 = model.find_steady_state()
|
|
127
|
+
finally:
|
|
128
|
+
for sp in species:
|
|
129
|
+
sp.dE += dg
|
|
130
|
+
|
|
131
|
+
model.finalize()
|
|
132
|
+
rlow = r1[self.reaction_name]
|
|
133
|
+
|
|
134
|
+
for sp in species:
|
|
135
|
+
sp.dE += dg
|
|
136
|
+
|
|
137
|
+
for reaction in self.model._reactions:
|
|
138
|
+
reaction.update(T=self.model.T,
|
|
139
|
+
Asite=self.model.Asite,
|
|
140
|
+
L=self.model.z,
|
|
141
|
+
force=True)
|
|
142
|
+
|
|
143
|
+
model = self.model.copy(initialize=False)
|
|
144
|
+
model.set_initial_conditions(self.U)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
t2, U2, r2 = model.find_steady_state()
|
|
148
|
+
finally:
|
|
149
|
+
for sp in species:
|
|
150
|
+
sp.dE -= dg
|
|
151
|
+
|
|
152
|
+
for reaction in self.model._reactions:
|
|
153
|
+
reaction.update(T=self.model.T,
|
|
154
|
+
Asite=self.model.Asite,
|
|
155
|
+
L=self.model.z,
|
|
156
|
+
force=True)
|
|
157
|
+
|
|
158
|
+
model.finalize()
|
|
159
|
+
rhigh = r2[self.reaction_name]
|
|
160
|
+
|
|
161
|
+
# central difference: the two rates are 2 * dg apart in free energy
|
|
162
|
+
return (rlow - rhigh) * kB * T / (self.rmid * 2 * dg)
|
|
163
|
+
|
|
164
|
+
def activation_barrier(self, dT=0.01):
|
|
165
|
+
T = self.model.T
|
|
166
|
+
|
|
167
|
+
model = self.model.copy(initialize=False)
|
|
168
|
+
model.T = T - dT
|
|
169
|
+
model.set_initial_conditions(self.Uequil)
|
|
170
|
+
t1, U1, r1 = model.find_steady_state()
|
|
171
|
+
model.finalize()
|
|
172
|
+
|
|
173
|
+
rlow = r1[self.reaction_name]
|
|
174
|
+
|
|
175
|
+
model = self.model.copy(initialize=False)
|
|
176
|
+
model.T = T + dT
|
|
177
|
+
model.set_initial_conditions(self.Uequil)
|
|
178
|
+
t2, U2, r2 = model.find_steady_state()
|
|
179
|
+
model.finalize()
|
|
180
|
+
|
|
181
|
+
rhigh = r2[self.reaction_name]
|
|
182
|
+
|
|
183
|
+
return kB * T**2 * (rhigh - rlow) / (self.rmid * 2 * dT)
|
|
184
|
+
|
|
185
|
+
def rate_order(self, name, drho=0.05):
|
|
186
|
+
species = self.model.species[name]
|
|
187
|
+
|
|
188
|
+
rhomid = self.Uequil[species.label]
|
|
189
|
+
if not rhomid > 0:
|
|
190
|
+
raise ValueError('{} has no positive concentration'.format(name))
|
|
191
|
+
|
|
192
|
+
U0 = self.Uequil.copy()
|
|
193
|
+
rholow = rhomid * (1.0 - drho)
|
|
194
|
+
U0[species.label] = rholow
|
|
195
|
+
model = self.model.copy(initialize=False)
|
|
196
|
+
model.set_initial_conditions(U0)
|
|
197
|
+
t1, U1, r1 = model.find_steady_state()
|
|
198
|
+
model.finalize()
|
|
199
|
+
rlow = r1[self.reaction_name]
|
|
200
|
+
|
|
201
|
+
rhohigh = rhomid * (1.0 + drho)
|
|
202
|
+
U0[species.label] = rhohigh
|
|
203
|
+
model.set_initial_conditions(U0)
|
|
204
|
+
t2, U2, r2 = model.find_steady_state()
|
|
205
|
+
model.finalize()
|
|
206
|
+
rhigh = r2[self.reaction_name]
|
|
207
|
+
|
|
208
|
+
return (rhomid / self.rmid) * (rhigh - rlow) / (rhohigh - rholow)
|
|
209
|
+
|
|
210
|
+
def drate_order_dg(self, fluid, adsorbates, rho_scale=0.01, g_scale=0.01):
|
|
211
|
+
"""(rho/r) d^2 r / (d rho dG): the change of the rate order in
|
|
212
|
+
`fluid` with the free energy of `adsorbates` (shifted together).
|
|
213
|
+
|
|
214
|
+
Central differences in the concentration of `fluid` (+-drho) and in
|
|
215
|
+
the free energies of `adsorbates` (+-dg).
|
|
216
|
+
"""
|
|
217
|
+
if not isinstance(fluid, _Fluid):
|
|
218
|
+
raise TypeError('fluid must be a Gas or Liquid species')
|
|
219
|
+
|
|
220
|
+
if not isinstance(adsorbates, collections.abc.Iterable):
|
|
221
|
+
adsorbates = [adsorbates]
|
|
222
|
+
|
|
223
|
+
if not isinstance(adsorbates[0], Adsorbate):
|
|
224
|
+
raise TypeError('adsorbates must be Adsorbate species')
|
|
225
|
+
|
|
226
|
+
rhomid = self.U[fluid.label]
|
|
227
|
+
if not rhomid > 0:
|
|
228
|
+
raise ValueError('{} has no positive concentration'
|
|
229
|
+
''.format(fluid.label))
|
|
230
|
+
rmid = self.r[self.reaction_name]
|
|
231
|
+
gmid = adsorbates[0].get_G(self.model.T)
|
|
232
|
+
if isinstance(gmid, sym.Basic):
|
|
233
|
+
trans = {}
|
|
234
|
+
for species in self.model._species:
|
|
235
|
+
if isinstance(species, Adsorbate) \
|
|
236
|
+
and species.symbol is not None:
|
|
237
|
+
trans[species.symbol] = self.U[species.label]
|
|
238
|
+
gmid = gmid.subs(trans)
|
|
239
|
+
dg = np.abs(gmid * g_scale * 2)
|
|
240
|
+
drho = rhomid * rho_scale * 2
|
|
241
|
+
|
|
242
|
+
dr = 0
|
|
243
|
+
|
|
244
|
+
def set_dg(species, dg):
|
|
245
|
+
species.dE += dg
|
|
246
|
+
|
|
247
|
+
for i in [-1, 1]:
|
|
248
|
+
for adsorbate in adsorbates:
|
|
249
|
+
set_dg(adsorbate, i * dg)
|
|
250
|
+
|
|
251
|
+
for reaction in self.model._reactions:
|
|
252
|
+
reaction.update(T=self.model.T,
|
|
253
|
+
Asite=self.model.Asite,
|
|
254
|
+
L=self.model.z,
|
|
255
|
+
force=True)
|
|
256
|
+
|
|
257
|
+
for j in [-1, 1]:
|
|
258
|
+
U0 = self.Uequil.copy()
|
|
259
|
+
U0[fluid.label] = rhomid + j * drho
|
|
260
|
+
|
|
261
|
+
model = self.model.copy(initialize=False)
|
|
262
|
+
model.set_initial_conditions(U0)
|
|
263
|
+
|
|
264
|
+
ti, Ui, ri = model.find_steady_state()
|
|
265
|
+
dr += i * j * ri[self.reaction_name]
|
|
266
|
+
|
|
267
|
+
for adsorbate in adsorbates:
|
|
268
|
+
set_dg(adsorbate, -i * dg)
|
|
269
|
+
|
|
270
|
+
for reaction in self.model._reactions:
|
|
271
|
+
reaction.update(T=self.model.T,
|
|
272
|
+
Asite=self.model.Asite,
|
|
273
|
+
L=self.model.z,
|
|
274
|
+
force=True)
|
|
275
|
+
|
|
276
|
+
# the four rates are 2 * dg and 2 * drho apart
|
|
277
|
+
return (rhomid / rmid) * dr / (4 * dg * drho)
|
|
278
|
+
|
|
279
|
+
def check_converged(self, *vals):
|
|
280
|
+
for val in vals:
|
|
281
|
+
for i, key in enumerate(val[0]):
|
|
282
|
+
if np.abs(val[-1][key] - val[-2][key]) > self.tol:
|
|
283
|
+
raise ValueError("Calculation not converged ({}: {}, "
|
|
284
|
+
"change {})! Increase dt or use better "
|
|
285
|
+
"initial guess.".format(
|
|
286
|
+
key, val[-1][key],
|
|
287
|
+
val[-1][key] - val[-2][key]))
|
micki/db.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from ase.db import connect
|
|
4
|
+
from ase.db.core import Database
|
|
5
|
+
|
|
6
|
+
from micki.reactants import Adsorbate, Gas, Liquid
|
|
7
|
+
from micki.eref import EnergyReference
|
|
8
|
+
|
|
9
|
+
class MickiDBReadError(ValueError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
# Attempts to parse attribute 'name' from dictionary 'data' and raises a
|
|
13
|
+
# parse error if it cannot be found.
|
|
14
|
+
def get_data(row, param):
|
|
15
|
+
if param not in row.data:
|
|
16
|
+
raise MickiDBReadError("DB row named {} does not have '{}' entry!"
|
|
17
|
+
"".format(row.name, param))
|
|
18
|
+
return row.data[param]
|
|
19
|
+
|
|
20
|
+
# Converts a single ASE DB row to a Micki Thermo object.
|
|
21
|
+
def row_to_thermo(row):
|
|
22
|
+
name = row.name
|
|
23
|
+
freqs = get_data(row, 'freqs')
|
|
24
|
+
thermo = get_data(row, 'thermo')
|
|
25
|
+
sites = get_data(row, 'sites')
|
|
26
|
+
rhoref = get_data(row, 'rhoref')
|
|
27
|
+
dE = get_data(row, 'dE')
|
|
28
|
+
symm = get_data(row, 'symm')
|
|
29
|
+
ts = get_data(row, 'ts')
|
|
30
|
+
spin = get_data(row, 'spin')
|
|
31
|
+
D = get_data(row, 'D')
|
|
32
|
+
S = get_data(row, 'S')
|
|
33
|
+
|
|
34
|
+
if thermo == 'Adsorbate':
|
|
35
|
+
return Adsorbate(row.toatoms(), name, freqs,
|
|
36
|
+
ts=ts, sites=sites, dE=dE, symm=symm)
|
|
37
|
+
elif thermo == 'Gas':
|
|
38
|
+
return Gas(row.toatoms(), name, freqs,
|
|
39
|
+
symm=symm, spin=spin, rhoref=rhoref, dE=dE)
|
|
40
|
+
elif thermo == 'Liquid':
|
|
41
|
+
return Liquid(row.toatoms(), name, freqs,
|
|
42
|
+
symm=symm, spin=spin, D=D, S=S, rhoref=rhoref, dE=dE)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError('Unknown Thermo type {}!'.format(thermo))
|
|
45
|
+
|
|
46
|
+
# Creates a dictionary of Thermo objects from a properly-formatted ASE DB file.
|
|
47
|
+
def read_from_db(db, names=None, eref=None):
|
|
48
|
+
if isinstance(db, str):
|
|
49
|
+
db = connect(db)
|
|
50
|
+
elif not isinstance(db, Database):
|
|
51
|
+
raise ValueError("Must pass active ASE DB connection, "
|
|
52
|
+
"or name of ASE DB file!")
|
|
53
|
+
|
|
54
|
+
species = {}
|
|
55
|
+
|
|
56
|
+
for row in db.select():
|
|
57
|
+
name = row.name
|
|
58
|
+
try:
|
|
59
|
+
species[name] = row_to_thermo(row)
|
|
60
|
+
except MickiDBReadError:
|
|
61
|
+
warnings.warn("Could not parse row {}, skipping.".format(name),
|
|
62
|
+
RuntimeWarning, stacklevel=2)
|
|
63
|
+
|
|
64
|
+
for name, sp in species.items():
|
|
65
|
+
newsites = []
|
|
66
|
+
for site in sp.sites:
|
|
67
|
+
if site in species:
|
|
68
|
+
newsites.append(species[site])
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("Unknown site named {}!".format(site))
|
|
71
|
+
sp.sites = newsites
|
|
72
|
+
|
|
73
|
+
if eref is not None:
|
|
74
|
+
reference = EnergyReference([species[name] for name in eref])
|
|
75
|
+
for name, sp in species.items():
|
|
76
|
+
sp.eref = reference
|
|
77
|
+
|
|
78
|
+
if names is not None:
|
|
79
|
+
return {name: species[name] for name in names}
|
|
80
|
+
return species
|
micki/eref.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
elif 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().__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().__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
|
micki/io.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from ase.io import read
|
|
4
|
+
from ase.units import _hplanck, J, m, kg
|
|
5
|
+
|
|
6
|
+
from micki.masses import masses
|
|
7
|
+
|
|
8
|
+
def parse_vasp_out(filename, ignore_atoms=()):
|
|
9
|
+
atoms = read(filename, index=0)
|
|
10
|
+
for atom in atoms:
|
|
11
|
+
atom.mass = masses[atom.symbol]
|
|
12
|
+
if 'OUTCAR' in filename:
|
|
13
|
+
# This reads the hessian from the OUTCAR and diagonalizes it
|
|
14
|
+
# to find the frequencies, rather than reading the frequencies
|
|
15
|
+
# directly from the OUTCAR. This is to ensure we use the same
|
|
16
|
+
# unit conversion factors, and also to make sure we use the same
|
|
17
|
+
# atom masses for all calculations. Also, allows for the possibility
|
|
18
|
+
# of doing partial hessian diagonalization should we want to do that.
|
|
19
|
+
hessblock = 0
|
|
20
|
+
with open(filename, 'r') as f:
|
|
21
|
+
for line in f:
|
|
22
|
+
line = line.strip()
|
|
23
|
+
if line != '':
|
|
24
|
+
if hessblock == 1:
|
|
25
|
+
if line.startswith('---'):
|
|
26
|
+
hessblock = 2
|
|
27
|
+
|
|
28
|
+
elif hessblock == 2:
|
|
29
|
+
line = line.split()
|
|
30
|
+
dof = len(line)
|
|
31
|
+
hess = np.zeros((dof, dof), dtype=float)
|
|
32
|
+
index = np.zeros(dof, dtype=int)
|
|
33
|
+
cart = np.zeros(dof, dtype=int)
|
|
34
|
+
for i, direction in enumerate(line):
|
|
35
|
+
index[i] = int(direction[:-1]) - 1
|
|
36
|
+
if direction[-1] == 'X':
|
|
37
|
+
cart[i] = 0
|
|
38
|
+
elif direction[-1] == 'Y':
|
|
39
|
+
cart[i] = 1
|
|
40
|
+
elif direction[-1] == 'Z':
|
|
41
|
+
cart[i] = 2
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError("Error reading Hessian!")
|
|
44
|
+
hessblock = 3
|
|
45
|
+
j = 0
|
|
46
|
+
|
|
47
|
+
elif hessblock == 3:
|
|
48
|
+
line = line.split()
|
|
49
|
+
hess[j] = np.array([float(val) for val in line[1:]],
|
|
50
|
+
dtype=float)
|
|
51
|
+
j += 1
|
|
52
|
+
|
|
53
|
+
elif line.startswith('SECOND DERIVATIVES'):
|
|
54
|
+
hessblock = 1
|
|
55
|
+
|
|
56
|
+
elif hessblock == 3:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
hess = -(hess + hess.T) / 2.
|
|
60
|
+
elif 'vasprun.xml' in filename:
|
|
61
|
+
import xml.etree.ElementTree as ET
|
|
62
|
+
|
|
63
|
+
tree = ET.parse(filename)
|
|
64
|
+
root = tree.getroot()
|
|
65
|
+
|
|
66
|
+
vasp_mass = {}
|
|
67
|
+
|
|
68
|
+
for element in root.find("atominfo/array[@name='atomtypes']/set"):
|
|
69
|
+
vasp_mass[element[1].text.strip()] = float(element[2].text)
|
|
70
|
+
|
|
71
|
+
selective = np.ones((len(atoms), 3), dtype=bool)
|
|
72
|
+
constblock = root.find(
|
|
73
|
+
'structure[@name="initialpos"]/varray[@name="selective"]')
|
|
74
|
+
if constblock is not None:
|
|
75
|
+
for i, v in enumerate(constblock):
|
|
76
|
+
for j, fixed in enumerate(v.text.split()):
|
|
77
|
+
selective[i, j] = (fixed == 'T')
|
|
78
|
+
index = []
|
|
79
|
+
for i, atom in enumerate(atoms):
|
|
80
|
+
for direction in selective[i]:
|
|
81
|
+
if direction:
|
|
82
|
+
index.append(i)
|
|
83
|
+
|
|
84
|
+
hess = np.zeros((len(index), len(index)), dtype=float)
|
|
85
|
+
|
|
86
|
+
for i, v in enumerate(root.find(
|
|
87
|
+
'calculation/dynmat/varray[@name="hessian"]')):
|
|
88
|
+
hess[i] = -np.array([float(val) for val in v.text.split()])
|
|
89
|
+
|
|
90
|
+
vasp_massvec = np.zeros(len(index), dtype=float)
|
|
91
|
+
for i, j in enumerate(index):
|
|
92
|
+
vasp_massvec[i] = vasp_mass[atoms[j].symbol]
|
|
93
|
+
|
|
94
|
+
# VASP uses weird masses, so we un-mass-weight here
|
|
95
|
+
hess *= np.sqrt(np.outer(vasp_massvec, vasp_massvec))
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError('Unknown file format {}!'.format(filename))
|
|
99
|
+
mass = np.array([atoms[i].mass for i in index], dtype=float)
|
|
100
|
+
hess /= np.sqrt(np.outer(mass, mass))
|
|
101
|
+
|
|
102
|
+
# Temporary work around: My test system OUTCARs include some
|
|
103
|
+
# metal atoms in the hessian, this seems to cause some problems
|
|
104
|
+
# with the MKM. So, here I'm taking only the non-metal part
|
|
105
|
+
# of the hessian and diagonalizing that.
|
|
106
|
+
partial = []
|
|
107
|
+
for i, j in enumerate(index):
|
|
108
|
+
if (j in ignore_atoms
|
|
109
|
+
or atoms[j] in ignore_atoms
|
|
110
|
+
or atoms[j].symbol in ignore_atoms):
|
|
111
|
+
continue
|
|
112
|
+
partial.append(i)
|
|
113
|
+
if not partial:
|
|
114
|
+
return atoms, np.array([])
|
|
115
|
+
partial_hess = np.zeros((len(partial), len(partial)))
|
|
116
|
+
partial_index = np.zeros(len(partial), dtype=int)
|
|
117
|
+
for i, a in enumerate(partial):
|
|
118
|
+
partial_index[i] = index[a]
|
|
119
|
+
for j, b in enumerate(partial):
|
|
120
|
+
partial_hess[i, j] = hess[a, b]
|
|
121
|
+
|
|
122
|
+
partial_hess *= _hplanck**2 * J * m**2 * kg / (4 * np.pi**2)
|
|
123
|
+
v, w = np.linalg.eig(partial_hess)
|
|
124
|
+
|
|
125
|
+
# We're taking the square root of an array that could include
|
|
126
|
+
# negative numbers, so the result has to be complex.
|
|
127
|
+
freq = np.sqrt(np.array(v, dtype=complex))
|
|
128
|
+
freqs = np.zeros_like(freq, dtype=float)
|
|
129
|
+
|
|
130
|
+
# We don't want to deal with complex numbers, so we just convert
|
|
131
|
+
# imaginary numbers to negative reals.
|
|
132
|
+
for i, val in enumerate(freq):
|
|
133
|
+
if val.imag == 0:
|
|
134
|
+
freqs[i] = val.real
|
|
135
|
+
else:
|
|
136
|
+
freqs[i] = -val.imag
|
|
137
|
+
freqs.sort()
|
|
138
|
+
return atoms, freqs
|
micki/lattice.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Lattice stuff"""
|
|
2
|
+
|
|
3
|
+
from .reactants import _Thermo
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ase.units import kB
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Lattice:
|
|
9
|
+
def __init__(self, neighborlist):
|
|
10
|
+
self.neighborlist = neighborlist
|
|
11
|
+
self.sites = [site for site in neighborlist]
|
|
12
|
+
if isinstance(self.sites[0], str):
|
|
13
|
+
self.string_names = True
|
|
14
|
+
elif isinstance(self.sites[0], _Thermo):
|
|
15
|
+
self.string_names = False
|
|
16
|
+
else:
|
|
17
|
+
raise ValueError('All sites must be _Thermo objects or strings!')
|
|
18
|
+
|
|
19
|
+
sitetype = str if self.string_names else _Thermo
|
|
20
|
+
|
|
21
|
+
for site in self.sites:
|
|
22
|
+
if not isinstance(site, sitetype):
|
|
23
|
+
raise ValueError('All sites must be _Thermo objects or strings!')
|
|
24
|
+
|
|
25
|
+
# Sanity check the input
|
|
26
|
+
self.totneighbors = {}
|
|
27
|
+
for site, neighbors in neighborlist.items():
|
|
28
|
+
self.totneighbors[site] = 0
|
|
29
|
+
for neighbor, val in neighbors.items():
|
|
30
|
+
self.totneighbors[site] += val
|
|
31
|
+
if neighbor not in self.sites:
|
|
32
|
+
raise ValueError("Neighbor {} is unknown!".format(neighbor))
|
|
33
|
+
for site in self.sites:
|
|
34
|
+
if site not in neighbors:
|
|
35
|
+
neighbors[site] = 0
|
|
36
|
+
|
|
37
|
+
# Create a neighbor list matrix
|
|
38
|
+
nsites = len(self.sites)
|
|
39
|
+
if nsites == 1:
|
|
40
|
+
self.ratio = {self.sites[0]: 1}
|
|
41
|
+
return
|
|
42
|
+
nmat = np.zeros((nsites, nsites), dtype=float)
|
|
43
|
+
for i, a in enumerate(self.sites):
|
|
44
|
+
for j, b in enumerate(self.sites):
|
|
45
|
+
nmat[i, j] = self.neighborlist[b].get(a, 0) / self.totneighbors[a]
|
|
46
|
+
|
|
47
|
+
# Diagonalize to find the element ratio. Only one eigenvector should
|
|
48
|
+
# have all positive values. This is the eigenvector that describes
|
|
49
|
+
# the element ratio
|
|
50
|
+
eigenvals, eigenvecs = np.linalg.eig(nmat)
|
|
51
|
+
for i in range(nsites):
|
|
52
|
+
if np.all(eigenvecs[:, i] < 0) or np.all(eigenvecs[:, i] > 0):
|
|
53
|
+
ratio = np.abs(eigenvecs[:, i])
|
|
54
|
+
ratio /= ratio.sum()
|
|
55
|
+
self.ratio = {site: ratio[i] for i, site in enumerate(self.sites)}
|
|
56
|
+
break
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError("Failed to find the element ratio! Please "
|
|
59
|
+
"double-check your neighbor count. "
|
|
60
|
+
"Eigenvectors: {}".format(eigenvecs))
|
|
61
|
+
|
|
62
|
+
def update_site_names(self, string_to_thermo):
|
|
63
|
+
if not self.string_names:
|
|
64
|
+
raise RuntimeError('Sites are already _Thermo objects!')
|
|
65
|
+
|
|
66
|
+
for site in self.sites:
|
|
67
|
+
if site not in string_to_thermo:
|
|
68
|
+
raise ValueError('No _Thermo object for site {}!'.format(site))
|
|
69
|
+
|
|
70
|
+
for string in string_to_thermo:
|
|
71
|
+
if string not in self.sites:
|
|
72
|
+
raise ValueError('Unknown site name {}!'.format(string))
|
|
73
|
+
|
|
74
|
+
new_neighborlist = {}
|
|
75
|
+
for site, neighbors in self.neighborlist.items():
|
|
76
|
+
new_neighborlist[string_to_thermo[site]] = {
|
|
77
|
+
string_to_thermo[neighbor]: count
|
|
78
|
+
for neighbor, count in neighbors.items()}
|
|
79
|
+
|
|
80
|
+
# rebuild everything (totneighbors, ratio, ...) with the new keys
|
|
81
|
+
self.__init__(new_neighborlist)
|
|
82
|
+
|
|
83
|
+
def get_S_conf(self, sites):
|
|
84
|
+
if sites is None or isinstance(sites, _Thermo) or len(sites) == 1:
|
|
85
|
+
return 0
|
|
86
|
+
nconfs = 1
|
|
87
|
+
for i in range(1, len(sites)):
|
|
88
|
+
ncount = self.neighborlist[sites[i - 1]][sites[i]]
|
|
89
|
+
if ncount == 0:
|
|
90
|
+
raise ValueError("This binding geometry is impossible!")
|
|
91
|
+
nconfs *= ncount / self.ratio[sites[i]]
|
|
92
|
+
return kB * np.log(nconfs)
|