-
Notifications
You must be signed in to change notification settings - Fork 854
/
quasiharmonic.py
347 lines (302 loc) · 13.3 KB
/
quasiharmonic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# coding: utf-8
# Copyright (c) Pymatgen Development Team.
# Distributed under the terms of the MIT License.
"""
This module implements the Quasi-harmonic Debye approximation that can
be used to compute thermal properties.
See the following papers for more info:
http://doi.org/10.1016/j.comphy.2003.12.001 (2004)
http://doi.org/10.1103/PhysRevB.90.174107 (2014)
"""
import logging
from collections import defaultdict
import numpy as np
from scipy.constants import physical_constants
from scipy.integrate import quadrature
from scipy.misc import derivative
from scipy.optimize import minimize
from pymatgen.analysis.eos import EOS, PolynomialEOS
from pymatgen.core.units import FloatWithUnit
__author__ = "Kiran Mathew, Brandon Bocklund"
__credits__ = "Cormac Toher"
logger = logging.getLogger(__name__)
class QuasiharmonicDebyeApprox:
"""
Quasiharmonic approximation.
"""
def __init__(
self,
energies,
volumes,
structure,
t_min=300.0,
t_step=100,
t_max=300.0,
eos="vinet",
pressure=0.0,
poisson=0.25,
use_mie_gruneisen=False,
anharmonic_contribution=False,
):
"""
Args:
energies (list): list of DFT energies in eV
volumes (list): list of volumes in Ang^3
structure (Structure):
t_min (float): min temperature
t_step (float): temperature step
t_max (float): max temperature
eos (str): equation of state used for fitting the energies and the
volumes.
options supported by pymatgen: "quadratic", "murnaghan", "birch",
"birch_murnaghan", "pourier_tarantola", "vinet",
"deltafactor", "numerical_eos"
pressure (float): in GPa, optional.
poisson (float): poisson ratio.
use_mie_gruneisen (bool): whether or not to use the mie-gruneisen
formulation to compute the gruneisen parameter.
The default is the slater-gamma formulation.
anharmonic_contribution (bool): whether or not to consider the anharmonic
contribution to the Debye temperature. Cannot be used with
use_mie_gruneisen. Defaults to False.
"""
self.energies = energies
self.volumes = volumes
self.structure = structure
self.temperature_min = t_min
self.temperature_max = t_max
self.temperature_step = t_step
self.eos_name = eos
self.pressure = pressure
self.poisson = poisson
self.use_mie_gruneisen = use_mie_gruneisen
self.anharmonic_contribution = anharmonic_contribution
if self.use_mie_gruneisen and self.anharmonic_contribution:
raise ValueError(
"The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and "
"cannot be used together."
)
self.mass = sum([e.atomic_mass for e in self.structure.species])
self.natoms = self.structure.composition.num_atoms
self.avg_mass = physical_constants["atomic mass constant"][0] * self.mass / self.natoms # kg
self.kb = physical_constants["Boltzmann constant in eV/K"][0]
self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0]
self.gpa_to_ev_ang = 1.0 / 160.21766208 # 1 GPa in ev/Ang^3
self.gibbs_free_energy = [] # optimized values, eV
# list of temperatures for which the optimized values are available, K
self.temperatures = []
self.optimum_volumes = [] # in Ang^3
# fit E and V and get the bulk modulus(used to compute the Debye
# temperature)
logger.info("Fitting E and V")
self.eos = EOS(eos)
self.ev_eos_fit = self.eos.fit(volumes, energies)
self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa
self.optimize_gibbs_free_energy()
def optimize_gibbs_free_energy(self):
"""
Evaluate the gibbs free energy as a function of V, T and P i.e
G(V, T, P), minimize G(V, T, P) wrt V for each T and store the
optimum values.
Note: The data points for which the equation of state fitting fails
are skipped.
"""
temperatures = np.linspace(
self.temperature_min,
self.temperature_max,
int(np.ceil((self.temperature_max - self.temperature_min) / self.temperature_step) + 1),
)
for t in temperatures:
try:
G_opt, V_opt = self.optimizer(t)
except Exception:
if len(temperatures) <= 1:
raise
logger.info("EOS fitting failed, so skipping this data point, {}".format(t))
self.gibbs_free_energy.append(G_opt)
self.temperatures.append(t)
self.optimum_volumes.append(V_opt)
def optimizer(self, temperature):
"""
Evaluate G(V, T, P) at the given temperature(and pressure) and
minimize it wrt V.
1. Compute the vibrational helmholtz free energy, A_vib.
2. Compute the gibbs free energy as a function of volume, temperature
and pressure, G(V,T,P).
3. Preform an equation of state fit to get the functional form of
gibbs free energy:G(V, T, P).
4. Finally G(V, P, T) is minimized with respect to V.
Args:
temperature (float): temperature in K
Returns:
float, float: G_opt(V_opt, T, P) in eV and V_opt in Ang^3.
"""
G_V = [] # G for each volume
# G = E(V) + PV + A_vib(V, T)
for i, v in enumerate(self.volumes):
G_V.append(
self.energies[i] + self.pressure * v * self.gpa_to_ev_ang + self.vibrational_free_energy(temperature, v)
)
# fit equation of state, G(V, T, P)
eos_fit = self.eos.fit(self.volumes, G_V)
# minimize the fit eos wrt volume
# Note: the ref energy and the ref volume(E0 and V0) not necessarily
# the same as minimum energy and min volume.
volume_guess = eos_fit.volumes[np.argmin(eos_fit.energies)]
min_wrt_vol = minimize(eos_fit.func, volume_guess)
# G_opt=G(V_opt, T, P), V_opt
return min_wrt_vol.fun, min_wrt_vol.x[0]
def vibrational_free_energy(self, temperature, volume):
"""
Vibrational Helmholtz free energy, A_vib(V, T).
Eq(4) in doi.org/10.1016/j.comphy.2003.12.001
Args:
temperature (float): temperature in K
volume (float)
Returns:
float: vibrational free energy in eV
"""
y = self.debye_temperature(volume) / temperature
return (
self.kb * self.natoms * temperature * (9.0 / 8.0 * y + 3 * np.log(1 - np.exp(-y)) - self.debye_integral(y))
)
def vibrational_internal_energy(self, temperature, volume):
"""
Vibrational internal energy, U_vib(V, T).
Eq(4) in doi.org/10.1016/j.comphy.2003.12.001
Args:
temperature (float): temperature in K
volume (float): in Ang^3
Returns:
float: vibrational internal energy in eV
"""
y = self.debye_temperature(volume) / temperature
return self.kb * self.natoms * temperature * (9.0 / 8.0 * y + 3 * self.debye_integral(y))
def debye_temperature(self, volume):
"""
Calculates the debye temperature.
Eq(6) in doi.org/10.1016/j.comphy.2003.12.001. Thanks to Joey.
Eq(6) above is equivalent to Eq(3) in doi.org/10.1103/PhysRevB.37.790
which does not consider anharmonic effects. Eq(20) in the same paper
and Eq(18) in doi.org/10.1016/j.commatsci.2009.12.006 both consider
anharmonic contributions to the Debye temperature through the Gruneisen
parameter at 0K (Gruneisen constant).
The anharmonic contribution is toggled by setting the anharmonic_contribution
to True or False in the QuasiharmonicDebyeApprox constructor.
Args:
volume (float): in Ang^3
Returns:
float: debye temperature in K
"""
term1 = (2.0 / 3.0 * (1.0 + self.poisson) / (1.0 - 2.0 * self.poisson)) ** 1.5
term2 = (1.0 / 3.0 * (1.0 + self.poisson) / (1.0 - self.poisson)) ** 1.5
f = (3.0 / (2.0 * term1 + term2)) ** (1.0 / 3.0)
debye = 2.9772e-11 * (volume / self.natoms) ** (-1.0 / 6.0) * f * np.sqrt(self.bulk_modulus / self.avg_mass)
if self.anharmonic_contribution:
gamma = self.gruneisen_parameter(0, self.ev_eos_fit.v0) # 0K equilibrium Gruneisen parameter
return debye * (self.ev_eos_fit.v0 / volume) ** (gamma)
return debye
@staticmethod
def debye_integral(y):
"""
Debye integral. Eq(5) in doi.org/10.1016/j.comphy.2003.12.001
Args:
y (float): debye temperature/T, upper limit
Returns:
float: unitless
"""
# floating point limit is reached around y=155, so values beyond that
# are set to the limiting value(T-->0, y --> \infty) of
# 6.4939394 (from wolfram alpha).
factor = 3.0 / y ** 3
if y < 155:
integral = quadrature(lambda x: x ** 3 / (np.exp(x) - 1.0), 0, y)
return list(integral)[0] * factor
return 6.493939 * factor
def gruneisen_parameter(self, temperature, volume):
"""
Slater-gamma formulation(the default):
gruneisen paramter = - d log(theta)/ d log(V)
= - ( 1/6 + 0.5 d log(B)/ d log(V) )
= - (1/6 + 0.5 V/B dB/dV),
where dB/dV = d^2E/dV^2 + V * d^3E/dV^3
Mie-gruneisen formulation:
Eq(31) in doi.org/10.1016/j.comphy.2003.12.001
Eq(7) in Blanco et. al. Joumal of Molecular Structure (Theochem)
368 (1996) 245-255
Also se J.P. Poirier, Introduction to the Physics of the Earth’s
Interior, 2nd ed. (Cambridge University Press, Cambridge,
2000) Eq(3.53)
Args:
temperature (float): temperature in K
volume (float): in Ang^3
Returns:
float: unitless
"""
if isinstance(self.eos, PolynomialEOS):
p = np.poly1d(self.eos.eos_params) # pylint: disable=E1101
# first derivative of energy at 0K wrt volume evaluated at the
# given volume, in eV/Ang^3
dEdV = np.polyder(p, 1)(volume)
# second derivative of energy at 0K wrt volume evaluated at the
# given volume, in eV/Ang^6
d2EdV2 = np.polyder(p, 2)(volume)
# third derivative of energy at 0K wrt volume evaluated at the
# given volume, in eV/Ang^9
d3EdV3 = np.polyder(p, 3)(volume)
else:
func = self.ev_eos_fit.func
dEdV = derivative(func, volume, dx=1e-3)
d2EdV2 = derivative(func, volume, dx=1e-3, n=2, order=5)
d3EdV3 = derivative(func, volume, dx=1e-3, n=3, order=7)
# Mie-gruneisen formulation
if self.use_mie_gruneisen:
p0 = dEdV
return (
self.gpa_to_ev_ang
* volume
* (self.pressure + p0 / self.gpa_to_ev_ang)
/ self.vibrational_internal_energy(temperature, volume)
)
# Slater-gamma formulation
# first derivative of bulk modulus wrt volume, eV/Ang^6
dBdV = d2EdV2 + d3EdV3 * volume
return -(1.0 / 6.0 + 0.5 * volume * dBdV / FloatWithUnit(self.ev_eos_fit.b0_GPa, "GPa").to("eV ang^-3"))
def thermal_conductivity(self, temperature, volume):
"""
Eq(17) in 10.1103/PhysRevB.90.174107
Args:
temperature (float): temperature in K
volume (float): in Ang^3
Returns:
float: thermal conductivity in W/K/m
"""
gamma = self.gruneisen_parameter(temperature, volume)
theta_d = self.debye_temperature(volume) # K
theta_a = theta_d * self.natoms ** (-1.0 / 3.0) # K
prefactor = (0.849 * 3 * 4 ** (1.0 / 3.0)) / (20.0 * np.pi ** 3)
# kg/K^3/s^3
prefactor = prefactor * (self.kb / self.hbar) ** 3 * self.avg_mass
kappa = prefactor / (gamma ** 2 - 0.514 * gamma + 0.228)
# kg/K/s^3 * Ang = (kg m/s^2)/(Ks)*1e-10
# = N/(Ks)*1e-10 = Nm/(Kms)*1e-10 = W/K/m*1e-10
kappa = kappa * theta_a ** 2 * volume ** (1.0 / 3.0) * 1e-10
return kappa
def get_summary_dict(self):
"""
Returns a dict with a summary of the computed properties.
"""
d = defaultdict(list)
d["pressure"] = self.pressure
d["poisson"] = self.poisson
d["mass"] = self.mass
d["natoms"] = int(self.natoms)
d["bulk_modulus"] = self.bulk_modulus
d["gibbs_free_energy"] = self.gibbs_free_energy
d["temperatures"] = self.temperatures
d["optimum_volumes"] = self.optimum_volumes
for v, t in zip(self.optimum_volumes, self.temperatures):
d["debye_temperature"].append(self.debye_temperature(v))
d["gruneisen_parameter"].append(self.gruneisen_parameter(t, v))
d["thermal_conductivity"].append(self.thermal_conductivity(t, v))
return d