Skip to content

Commit 5ed30e1

Browse files
authored
Merge pull request geodynamics#665 from bobmyhill/generic_debye
Modular Mie-Debye-Grueneisen equation of state
2 parents bbd1937 + 3cc23ec commit 5ed30e1

File tree

9 files changed

+1332
-3
lines changed

9 files changed

+1332
-3
lines changed

burnman/eos/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit for
22
# the Earth and Planetary Sciences
3-
# Copyright (C) 2012 - 2021 by the BurnMan team, released under the GNU
3+
# Copyright (C) 2012 - 2025 by the BurnMan team, released under the GNU
44
# GPL v2 or later.
55

66

@@ -17,6 +17,13 @@
1717
from .birch_murnaghan import BM3Shear2, BM3, BM4
1818
from .mie_grueneisen_debye import MGD2, MGD3
1919
from .slb import SLB2, SLB3, SLB3Conductive
20+
from .modular_mie_grueneisen_debye import ModularMGD
21+
from .debye_temperature_models import (
22+
DebyeTemperatureModelBase,
23+
SLB,
24+
PowerLawGammaSimple,
25+
PowerLawGamma,
26+
)
2027
from .modified_tait import MT
2128
from .hp import HP_TMT
2229
from .hp import HP_TMTL
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit for the Earth and Planetary Sciences
2+
# Copyright (C) 2012 - 2025 by the BurnMan team, released under the GNU
3+
# GPL v2 or later.
4+
5+
import numpy as np
6+
7+
try:
8+
import os
9+
10+
if "NUMBA_DISABLE_JIT" in os.environ and int(os.environ["NUMBA_DISABLE_JIT"]) == 1:
11+
raise ImportError("NOOOO!")
12+
from numba import jit
13+
except ImportError:
14+
15+
def jit(nopython=True):
16+
def decorator(fn):
17+
return fn
18+
19+
return decorator
20+
21+
22+
# Pade coefficients for approximation of the anharmonic contribution to the
23+
# Helmholtz energy. Based on the Taylor expansion of the integral of the
24+
# Debye thermal energy at x_0 = 0.4 divided by x^4,
25+
# followed by a 3-5 Pade approximation multiplied by x^4.
26+
p = np.array([0.0, 0.0, 0.0, 0.0, 0.235256039, -0.00744162069, 23.0722431, 366.827130])
27+
q = np.array([1.0, 4.1784544, 46.00154792, 168.5910059, 568.90929887, 737.13224108])
28+
29+
# First derivatives
30+
p1 = np.polyder(p[::-1])[::-1]
31+
q1 = np.polyder(q[::-1])[::-1]
32+
33+
# Second derivatives
34+
p2 = np.polyder(p1[::-1])[::-1]
35+
q2 = np.polyder(q1[::-1])[::-1]
36+
37+
38+
@jit(nopython=True)
39+
def _helmholtz_pade_pq(t, to_nth_derivative=0):
40+
"""
41+
Evaluate the Pade approximant to the nondimensional Helmholtz energy
42+
at a given nondimensional temperature. See the documentation of
43+
`helmholtz_energy` for details on the model.
44+
45+
:param t: Nondimensional temperature, defined as
46+
:math:`T / T_{D}`, where :math:`T_{D}` is the
47+
Debye temperature.
48+
:param to_nth_derivative: If 0, return p and q. If 1, also return the
49+
first derivatives of p and q. If 2, also return the second derivatives
50+
of p and q.
51+
:return: A tuple containing the coefficients of the Pade approximant in
52+
the form ([p, ...], [q, ...]) up to the desired derivative.
53+
:rtype: tuple of (list, list)
54+
"""
55+
t2 = t * t
56+
t3 = t2 * t
57+
t4 = t3 * t
58+
t5 = t4 * t
59+
t6 = t5 * t
60+
t7 = t6 * t
61+
62+
xp = [p[4] * t4 + p[5] * t5 + p[6] * t6 + p[7] * t7]
63+
xq = [q[0] + q[1] * t + q[2] * t2 + q[3] * t3 + q[4] * t4 + q[5] * t5]
64+
65+
if to_nth_derivative > 0:
66+
xp.append(p1[3] * t3 + p1[4] * t4 + p1[5] * t5 + p1[6] * t6)
67+
xq.append(q1[0] + q1[1] * t + q1[2] * t2 + q1[3] * t3 + q1[4] * t4)
68+
69+
if to_nth_derivative > 1:
70+
xp.append(p2[2] * t2 + p2[3] * t3 + p2[4] * t4 + p2[5] * t5)
71+
xq.append(q2[0] + q2[1] * t + q2[2] * t2 + q2[3] * t3)
72+
73+
return xp, xq
74+
75+
76+
@jit(nopython=True)
77+
def _helmholtz_pade(t):
78+
"""
79+
Evaluate the Pade approximant to the nondimensional Helmholtz energy
80+
at a given nondimensional temperature. See the documentation of
81+
`helmholtz_energy` for details on the model.
82+
83+
:param t: Nondimensional temperature, defined as
84+
:math:`T / T_{D}`, where :math:`T_{D}` is the
85+
Debye temperature.
86+
:return: Nondimensional Helmholtz energy.
87+
:rtype: float
88+
"""
89+
xp, xq = _helmholtz_pade_pq(t, to_nth_derivative=0)
90+
return xp[0] / xq[0]
91+
92+
93+
@jit(nopython=True)
94+
def _dhelmholtzdt_pade(t):
95+
"""
96+
Evaluate the first derivative of the Pade approximant to the
97+
nondimensional Helmholtz energy with respect to nondimensional temperature.
98+
See the documentation of `helmholtz_energy` for details on the model.
99+
100+
:param t: Nondimensional temperature, defined as
101+
:math:`T / T_{D}`, where :math:`T_{D}` is the
102+
Debye temperature.
103+
:return: float
104+
"""
105+
xp, xq = _helmholtz_pade_pq(t, to_nth_derivative=1)
106+
return (xp[1] * xq[0] - xp[0] * xq[1]) / (xq[0] * xq[0])
107+
108+
109+
@jit(nopython=True)
110+
def _d2helmholtzdt2_pade(t):
111+
"""
112+
Evaluate the second derivative of the Pade approximant to the
113+
nondimensional Helmholtz energy with respect to nondimensional temperature.
114+
See the documentation of `helmholtz_energy` for details on the model.
115+
116+
:param t: Nondimensional temperature, defined as
117+
:math:`T / T_{D}`, where :math:`T_{D}` is the
118+
Debye temperature.
119+
:return: float
120+
"""
121+
xp, xq = _helmholtz_pade_pq(t, to_nth_derivative=2)
122+
return (
123+
xp[2] * xq[0] * xq[0]
124+
- xp[0] * xq[2] * xq[0]
125+
- 2.0 * (xp[1] * xq[0] - xp[0] * xq[1]) * xq[1]
126+
) / (xq[0] * xq[0] * xq[0])
127+
128+
129+
@jit(nopython=True)
130+
def _nondimensional_helmholtz_energy(T, debye_T):
131+
"""
132+
Helmholtz free energy of the anharmonic contribution
133+
at a given temperature.
134+
135+
This model is based on a 3-5 Pade approximation to the following
136+
expression:
137+
:math:`\\int_0^x (E_{D}/3nR) dt / x^{4}`, which is then
138+
post-multiplied by :math:`x^{4}` to yield the Helmholtz energy.
139+
The :math:`E_{D}` term inside the integral is the thermal energy of a
140+
Debye solid per mole of atoms. This expression is chosen because it
141+
matches the behaviour of the anharmonic contribution to the entropy
142+
at low and high temperatures - i.e., it is equal to zero at low temperature
143+
(with all derivatives also equal to zero) and linear at high temperature.
144+
See Figure 3 in
145+
Oganov and Dorogokupets (2004; dx.doi.org/10.1088/0953-8984/16/8/018).
146+
147+
:param T: Temperature in Kelvin.
148+
:type T: float
149+
:param debye_T: Debye temperature in Kelvin, which is used to
150+
nondimensionalise the temperature.
151+
:type debye_T: float
152+
:return: Helmholtz energy
153+
:rtype: float
154+
"""
155+
t = T / debye_T
156+
return _helmholtz_pade(t)
157+
158+
159+
@jit(nopython=True)
160+
def _nondimensional_entropy(T, debye_T):
161+
"""
162+
Entropy of the anharmonic contribution. See the documentation of
163+
`helmholtz_energy` for details on the model.
164+
165+
:param T: Temperature in Kelvin.
166+
:type T: float
167+
:param debye_T: Debye temperature in Kelvin.
168+
:type debye_T: float
169+
:return: Entropy
170+
:rtype: float
171+
"""
172+
t = T / debye_T
173+
return -_dhelmholtzdt_pade(t) / debye_T
174+
175+
176+
@jit(nopython=True)
177+
def _nondimensional_heat_capacity(T, debye_T):
178+
"""
179+
Heat capacity of the anharmonic contribution. See the documentation of
180+
`helmholtz_energy` for details on the model.
181+
182+
:param T: Temperature in Kelvin.
183+
:type T: float
184+
:param debye_T: Debye temperature in Kelvin.
185+
:type debye_T: float
186+
:return: Heat capacity
187+
:rtype: float
188+
"""
189+
t = T / debye_T
190+
return -t * _d2helmholtzdt2_pade(t) / debye_T
191+
192+
193+
@jit(nopython=True)
194+
def _nondimensional_dhelmholtz_dTheta(T, debye_T):
195+
"""
196+
Derivative of the anharmonic contribution to the Helmholtz energy
197+
with respect to the Debye temperature. See the documentation of
198+
`helmholtz_energy` for details on the model.
199+
200+
:param T: Temperature in Kelvin.
201+
:type T: float
202+
:param debye_T: Debye temperature in Kelvin.
203+
:type debye_T: float
204+
:return: Derivative of Helmholtz energy with respect to Debye temperature
205+
:rtype: float
206+
"""
207+
t = T / debye_T
208+
return -_dhelmholtzdt_pade(t) * t / debye_T
209+
210+
211+
@jit(nopython=True)
212+
def _nondimensional_d2helmholtz_dTheta2(T, debye_T):
213+
"""
214+
Second derivative of the anharmonic contribution to the Helmholtz energy
215+
with respect to the Debye temperature. See the documentation of
216+
`helmholtz_energy` for details on the model.
217+
218+
:param T: Temperature in Kelvin.
219+
:type T: float
220+
:param debye_T: Debye temperature in Kelvin.
221+
:type debye_T: float
222+
:return: Second derivative of Helmholtz energy with respect to Debye temperature
223+
:rtype: float
224+
"""
225+
t = T / debye_T
226+
return t * (t * _d2helmholtzdt2_pade(t) + 2.0 * _dhelmholtzdt_pade(t)) / debye_T**2
227+
228+
229+
@jit(nopython=True)
230+
def _nondimensional_dentropy_dTheta(T, debye_T):
231+
"""
232+
Derivative of the anharmonic contribution to the entropy
233+
with respect to the Debye temperature. See the documentation of
234+
`helmholtz_energy` for details on the model.
235+
236+
:param T: Temperature in Kelvin.
237+
:type T: float
238+
:param debye_T: Debye temperature in Kelvin.
239+
:type debye_T: float
240+
:return: Derivative of entropy with respect to Debye temperature
241+
:rtype: float
242+
"""
243+
t = T / debye_T
244+
return (_d2helmholtzdt2_pade(t) * t + _dhelmholtzdt_pade(t)) / debye_T**2
245+
246+
247+
class AnharmonicDebyePade:
248+
"""
249+
Class providing methods to compute the anharmonic contribution to the
250+
Helmholtz free energy, pressure, entropy, isochoric heat capacity,
251+
isothermal bulk modulus and thermal expansion coefficient
252+
multiplied by the isothermal bulk modulus.
253+
254+
The full Helmholtz free energy (relative to the reference isotherm) is
255+
given by :math:`F = A * (F_a - F_a(T_0))`, where
256+
:math:`A = a_{anh} * (V/V_0)^{m_{anh}}`, with both :math:`a_{anh}`
257+
and :math:`m_{anh}` being parameters of the model.
258+
The term :math:`F_a` is calculated using the 3-5 Pade approximant to
259+
the function: :math:`\\int_0^x (E_{D}/3nR) dt / x^{4}`, then
260+
post-multiplied by :math:`x^{4}`.
261+
262+
The :math:`E_{D}` term inside the integral is the thermal energy of a
263+
Debye solid per mole of atoms. This expression is chosen because it
264+
matches the behaviour of the anharmonic contribution to the entropy
265+
at low and high temperatures - i.e., it is equal to zero at low temperature
266+
(with all derivatives also equal to zero) and linear at high temperature.
267+
See Figure 3 in Oganov and Dorogokupets
268+
(2004; dx.doi.org/10.1088/0953-8984/16/8/018).
269+
270+
:return: _description_
271+
:rtype: _type_
272+
"""
273+
274+
@staticmethod
275+
def helmholtz_energy(temperature, volume, params):
276+
x = volume / params["V_0"]
277+
A = params["a_anh"] * np.power(x, params["m_anh"])
278+
theta_model = params["debye_temperature_model"]
279+
debye_T = theta_model(x, params)
280+
F_a = _nondimensional_helmholtz_energy(temperature, debye_T)
281+
F_a0 = _nondimensional_helmholtz_energy(params["T_0"], debye_T)
282+
return A * (F_a - F_a0)
283+
284+
@staticmethod
285+
def entropy(temperature, volume, params):
286+
x = volume / params["V_0"]
287+
A = params["a_anh"] * np.power(x, params["m_anh"])
288+
theta_model = params["debye_temperature_model"]
289+
debye_T = theta_model(x, params)
290+
S_a = _nondimensional_entropy(temperature, debye_T)
291+
return A * S_a
292+
293+
@staticmethod
294+
def heat_capacity_v(temperature, volume, params):
295+
x = volume / params["V_0"]
296+
A = params["a_anh"] * np.power(x, params["m_anh"])
297+
theta_model = params["debye_temperature_model"]
298+
debye_T = theta_model(x, params)
299+
Cv_a = _nondimensional_heat_capacity(temperature, debye_T)
300+
return A * Cv_a
301+
302+
@staticmethod
303+
def pressure(temperature, volume, params):
304+
x = volume / params["V_0"]
305+
A = params["a_anh"] * np.power(x, params["m_anh"])
306+
theta_model = params["debye_temperature_model"]
307+
debye_T = theta_model(x, params)
308+
F_a = _nondimensional_helmholtz_energy(temperature, debye_T)
309+
F_a0 = _nondimensional_helmholtz_energy(params["T_0"], debye_T)
310+
F_ad = _nondimensional_dhelmholtz_dTheta(temperature, debye_T)
311+
F_ad0 = _nondimensional_dhelmholtz_dTheta(params["T_0"], debye_T)
312+
return -A * (
313+
(params["m_anh"] / volume) * (F_a - F_a0)
314+
+ (theta_model.dVrel(x, params) / params["V_0"]) * (F_ad - F_ad0)
315+
)
316+
317+
@staticmethod
318+
def isothermal_bulk_modulus(temperature, volume, params):
319+
x = volume / params["V_0"]
320+
A = params["a_anh"] * np.power(x, params["m_anh"])
321+
theta_model = params["debye_temperature_model"]
322+
debye_T = theta_model(x, params)
323+
324+
F_a = _nondimensional_helmholtz_energy(temperature, debye_T)
325+
F_a0 = _nondimensional_helmholtz_energy(params["T_0"], debye_T)
326+
F_ad = _nondimensional_dhelmholtz_dTheta(temperature, debye_T)
327+
F_ad0 = _nondimensional_dhelmholtz_dTheta(params["T_0"], debye_T)
328+
F_add = _nondimensional_d2helmholtz_dTheta2(temperature, debye_T)
329+
F_add0 = _nondimensional_d2helmholtz_dTheta2(params["T_0"], debye_T)
330+
331+
return (
332+
A
333+
* volume
334+
* (
335+
params["m_anh"] * (params["m_anh"] - 1.0) / volume**2 * (F_a - F_a0)
336+
+ 2
337+
* params["m_anh"]
338+
/ volume
339+
* (F_ad - F_ad0)
340+
* theta_model.dVrel(x, params)
341+
/ params["V_0"]
342+
+ (F_add - F_add0) * (theta_model.dVrel(x, params) / params["V_0"]) ** 2
343+
+ (F_ad - F_ad0) * theta_model.d2dVrel2(x, params) / params["V_0"] ** 2
344+
)
345+
)
346+
347+
@staticmethod
348+
def dSdV(temperature, volume, params):
349+
x = volume / params["V_0"]
350+
A = params["a_anh"] * np.power(x, params["m_anh"])
351+
theta_model = params["debye_temperature_model"]
352+
debye_T = theta_model(x, params)
353+
354+
S_a = _nondimensional_entropy(temperature, debye_T)
355+
S_ad = _nondimensional_dentropy_dTheta(temperature, debye_T)
356+
357+
aK_T = A * (
358+
(params["m_anh"] / volume) * S_a
359+
+ (theta_model.dVrel(x, params) / params["V_0"]) * S_ad
360+
)
361+
362+
return aK_T

0 commit comments

Comments
 (0)