Skip to content

Commit 5e5249b

Browse files
authored
Merge pull request #637 from bobmyhill/set_state_with_volume_mineral
Speed up set_state_with_volume for P(V, T) minerals
2 parents 3d00c6a + 48a8834 commit 5e5249b

File tree

7 files changed

+137
-38
lines changed

7 files changed

+137
-38
lines changed

burnman/classes/material.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,20 @@ def set_state_with_volume(
186186
):
187187
"""
188188
This function acts similarly to set_state, but takes volume and
189-
temperature as input to find the pressure. In order to ensure
190-
self-consistency, this function does not use any pressure functions
191-
from the material classes, but instead finds the pressure using the
192-
brentq root-finding method.
189+
temperature as input to find the pressure.
190+
191+
In order to ensure self-consistency, this function does not
192+
use any pressure functions from the material classes,
193+
but instead finds the pressure using the brentq root-finding
194+
method. To provide more context, even if a mineral is being
195+
evaluated with a P(V, T) equation of state, there might be a
196+
property modifier G_mod(P, T) added on top - which might then
197+
introduce a pressure dependent V_mod(P, T).
198+
Thus, we must solve for the volume iteratively:
199+
V = V_i(P(V_i, T), T) + V_mod(P, T), where P(V_i, T) is
200+
solved for iteratively.
201+
202+
This function is overloaded by the Mineral class.
193203
194204
:param volume: The desired molar volume of the mineral [m^3].
195205
:type volume: float

burnman/classes/mineral.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from .material import Material, material_property
1414
from .. import eos
1515
from ..utils.misc import copy_documentation
16+
from ..utils.math import bracket
17+
from scipy.optimize import brentq
1618

1719

1820
class Mineral(Material):
@@ -145,6 +147,73 @@ def set_state(self, pressure, temperature):
145147
"no method set for mineral, or equation_of_state given in mineral.params"
146148
)
147149

150+
def set_state_with_volume(
151+
self, volume, temperature, pressure_guesses=[0.0e9, 10.0e9]
152+
):
153+
"""
154+
This function acts similarly to set_state, but takes volume and
155+
temperature as input to find the pressure.
156+
157+
If the mineral is an endmember that has a pressure(T, V)
158+
function and does not have property modifiers,
159+
this function evaluates the pressure directly.
160+
Otherwise, it finds the pressure using the brentq root-finding
161+
method on the molar_volume attribute of the mineral.
162+
This ensures self-consistency even if the mineral has a
163+
pressure-dependent volume modifier.
164+
165+
:param volume: The desired molar volume of the mineral [m^3].
166+
:type volume: float
167+
168+
:param temperature: The desired temperature of the mineral [K].
169+
:type temperature: float
170+
171+
:param pressure_guesses: A list of floats denoting the initial
172+
low and high guesses for bracketing of the pressure [Pa].
173+
These guesses should preferably bound the correct pressure,
174+
but do not need to do so. More importantly,
175+
they should not lie outside the valid region of
176+
the equation of state. Defaults to [0.e9, 10.e9].
177+
:type pressure_guesses: list
178+
"""
179+
180+
def _delta_volume(pressure, volume, temperature):
181+
# Try to set the state with this pressure,
182+
# but if the pressure is too low most equations of state
183+
# fail. In this case, treat the molar_volume as infinite
184+
# and brentq will try a larger pressure.
185+
try:
186+
self.set_state(pressure, temperature)
187+
return volume - self.molar_volume
188+
except Exception:
189+
return -np.inf
190+
191+
if (
192+
type(self.method) == str # require an endmember method
193+
or not self.method.pressure # that has a P(V, T) function
194+
or self.property_modifiers # and that doesn't have modifiers
195+
):
196+
# we need to have a sign change in [a,b] to find a zero.
197+
args = (volume, temperature)
198+
try:
199+
sol = bracket(
200+
_delta_volume, pressure_guesses[0], pressure_guesses[1], args
201+
)
202+
except ValueError:
203+
try: # Try again with 0 Pa lower bound on the pressure
204+
sol = bracket(_delta_volume, 0.0e9, pressure_guesses[1], args)
205+
except ValueError:
206+
raise Exception(
207+
"Cannot find a pressure, perhaps the volume or starting pressures "
208+
"are outside the range of validity for the equation of state?"
209+
)
210+
pressure = brentq(_delta_volume, sol[0], sol[1], args=args)
211+
self.set_state(pressure, temperature)
212+
else:
213+
self.set_state(
214+
self.method.pressure(temperature, volume, self.params), temperature
215+
)
216+
148217
"""
149218
Properties from equations of state
150219
We choose the P, T properties (e.g. Gibbs(P, T) rather than Helmholtz(V, T)),

burnman/eos/slb.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -254,41 +254,47 @@ def _K_T(V, T, params):
254254
"the equation of state?"
255255
)
256256

257-
return opt.brentq(_delta_pressure, sol[0], sol[1], args=args)
257+
return opt.brentq(_delta_pressure, sol[0], sol[1], args=args, xtol=1.0e-24)
258258

259259
def pressure(self, temperature, volume, params):
260260
"""
261261
Returns the pressure of the mineral at a given temperature and volume
262262
[Pa]
263263
"""
264-
debye_T = self._debye_temperature(params["V_0"] / volume, params)
265-
gr = _grueneisen_parameter_slb(
266-
params["V_0"], volume, params["grueneisen_0"], params["q_0"]
267-
)
268-
# does not depend on pressure
269-
# thermal energy at temperature T
270-
E_th = debye.thermal_energy(temperature, debye_T, params["n"])
271-
# thermal energy at reference temperature
272-
E_th_ref = debye.thermal_energy(params["T_0"], debye_T, params["n"])
264+
T_0 = params["T_0"]
265+
Debye_0 = params["Debye_0"]
266+
V_0 = params["V_0"]
267+
n = params["n"]
268+
269+
a1_ii = 6.0 * params["grueneisen_0"] # EQ 47
270+
a2_iikk = (
271+
-12.0 * params["grueneisen_0"]
272+
+ 36.0 * pow(params["grueneisen_0"], 2.0)
273+
- 18.0 * params["q_0"] * params["grueneisen_0"]
274+
) # EQ 47
273275

274276
b_iikk = 9.0 * params["K_0"] # EQ 28
275-
b_iikkmm = 27.0 * params["K_0"] * (params["Kprime_0"] - 4.0) # EQ 29
276-
f = 0.5 * (pow(params["V_0"] / volume, 2.0 / 3.0) - 1.0) # EQ 24
277-
P = (1.0 / 3.0) * (pow(1.0 + 2.0 * f, 5.0 / 2.0)) * (
278-
(b_iikk * f) + (0.5 * b_iikkmm * pow(f, 2.0))
279-
) + gr * (
280-
E_th - E_th_ref
281-
) / volume # EQ 21
277+
b_iikkmm = 27.0 * params["K_0"] * (params["Kprime_0"] - 4.0) # EQ 29z
278+
279+
bel_0, gel = 0.0, 1.0
282280
if self.conductive:
283-
P += el.pressure(
284-
temperature,
285-
volume,
286-
params["T_0"],
287-
params["V_0"],
288-
params["bel_0"],
289-
params["gel"],
290-
)
291-
return P
281+
bel_0, gel = params["bel_0"], params["gel"]
282+
283+
return _delta_pressure(
284+
volume,
285+
0.0,
286+
temperature,
287+
V_0,
288+
T_0,
289+
Debye_0,
290+
n,
291+
a1_ii,
292+
a2_iikk,
293+
b_iikk,
294+
b_iikkmm,
295+
bel_0,
296+
gel,
297+
)
292298

293299
def isothermal_bulk_modulus_reuss(self, pressure, temperature, volume, params):
294300
"""

burnman/tools/eos.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,6 @@ def equilibration_function(mineral):
318318
m.set_state(P + 0.5 * dP, T + 0.5 * dT)
319319
equilibration_function(m)
320320

321-
beta0 = -(logm(F3) - logm(F2)) / dP
322-
alpha0 = (logm(F1) - logm(F0)) / dT
323-
324321
Q = m.deformed_coordinate_frame
325322
beta1 = m.isothermal_compressibility_tensor
326323
alpha1 = m.thermal_expansivity_tensor
@@ -329,6 +326,9 @@ def equilibration_function(mineral):
329326
alpha1 = np.einsum("mi, nj, ij->mn", Q, Q, alpha1)
330327

331328
if m.orthotropic:
329+
beta0 = -(logm(F3) - logm(F2)) / dP
330+
alpha0 = (logm(F1) - logm(F0)) / dT
331+
332332
expr.extend(
333333
[f"SI = -d(lnm(F))/dP ({i}{j})" for i in range(3) for j in range(i, 3)]
334334
)

misc/benchmarks/ab_initio_solids.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@
5858
pressures = np.empty_like(volumes)
5959
for temperature in temperatures:
6060
for i, volume in enumerate(volumes):
61-
pressures[i] = phase.method.pressure(temperature, volume, phase.params)
61+
phase.set_state_with_volume(volume, temperature)
62+
pressures[i] = phase.pressure
6263
ax_P.plot(
6364
volumes * 1e6,
6465
pressures / 1e9,
@@ -77,8 +78,7 @@
7778
energies = np.empty_like(volumes)
7879
for temperature in temperatures:
7980
for i, volume in enumerate(volumes):
80-
P = phase.method.pressure(temperature, volume, phase.params)
81-
phase.set_state(P, temperature)
81+
phase.set_state_with_volume(volume, temperature)
8282
energies[i] = phase.molar_internal_energy
8383
ax_E.plot(
8484
volumes * 1e6,

test.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ $PYTHON -m pip install -q -e .[dev]
2424
echo ""
2525

2626
echo "Dependency tree:"
27-
$PYTHON -m pip install -q pipdeptree .
27+
$PYTHON -m pip install -q pipdeptree
2828
$PYTHON -m pipdeptree -p burnman -d 1 2> /dev/null
2929
pycddlib_version=`pip freeze | grep "pycddlib=" | awk -F"==" '{print $2}'`
3030
if [ ! -z "${pycddlib_version}" ]
@@ -64,7 +64,7 @@ with open('$t') as f:
6464
EOF
6565
) >$t.tmp 2>$t.tmp.error
6666
ret=$?
67-
cat $t.tmp.error >>$t.tmp
67+
grep -v "^$" $t.tmp.error >>$t.tmp #append non-empty error messages to output
6868
rm -f $t.tmp.error
6969

7070
grep -v "which is a non-GUI backend" $t.tmp | grep -v "plt.show()" > tmpfile

tests/test_composite.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,20 @@ def test_query_properties(self):
316316
self.assertFloatEqual(rock1.molar_heat_capacity_v, min1.C_v)
317317
self.assertFloatEqual(rock1.molar_heat_capacity_p, min1.C_p)
318318

319+
def test_set_state_with_volume(self):
320+
min1 = minerals.SLB_2011.periclase()
321+
rock1 = burnman.Composite([min1, min1], [0.5, 0.5])
322+
P = 1.0e9
323+
P2 = 2.0e9
324+
T = 1000.0
325+
rock1.set_state(P, T)
326+
V = rock1.molar_volume
327+
rock1.set_state(P2, T) # reset state
328+
_ = rock1.molar_volume
329+
rock1.set_state_with_volume(V, T) # try to find original state
330+
331+
self.assertFloatEqual(P, rock1.pressure)
332+
319333
def test_stoichiometric_matrix_single_mineral(self):
320334
rock = burnman.Composite([minerals.SLB_2011.quartz()])
321335

0 commit comments

Comments
 (0)