Skip to content

Commit 4ac4257

Browse files
authored
Merge pull request #692 from bobmyhill/adaptive_logish_solve
Add adaptive logish epsilon fallback for difficult equilibrate solves
2 parents 1d1486f + b2b68a6 commit 4ac4257

File tree

4 files changed

+130
-21
lines changed

4 files changed

+130
-21
lines changed

burnman/classes/solutionmodel.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66

77
import importlib
88
import numpy as np
9+
from ..constants import logish_eps
910
from ..utils.chemistry import process_solution_chemistry
1011
from .. import constants
1112
import warnings
1213
from .material import material_property, cached_property
1314
from dataclasses import make_dataclass
1415

15-
np_eps = np.finfo(float).eps
16-
1716
Interaction = make_dataclass(
1817
"Interaction", ["inds", "expts", "f_r", "m_jr", "f_rs", "m_jrs"]
1918
)
@@ -106,26 +105,26 @@ def _non_ideal_interactions_subreg(p, n_endmembers, Wijk):
106105
return Wint
107106

108107

109-
def logish(x, eps=np_eps):
108+
def logish(x, eps=logish_eps):
110109
"""
111110
2nd order series expansion of log(x) about eps:
112111
log(eps) - sum_k=1^infty (f_eps)^k / k
113112
Prevents infinities at x=0
114113
"""
115-
f_eps = 1.0 - x / eps
116-
mask = x > eps
117-
ln = np.where(x <= eps, np.log(eps) - f_eps - f_eps * f_eps / 2.0, 0.0)
114+
f_eps = 1.0 - x / eps[0]
115+
mask = x > eps[0]
116+
ln = np.where(x <= eps[0], np.log(eps[0]) - f_eps - f_eps * f_eps / 2.0, 0.0)
118117
ln[mask] = np.log(x[mask])
119118
return ln
120119

121120

122-
def inverseish(x, eps=np_eps):
121+
def inverseish(x, eps=logish_eps):
123122
"""
124123
1st order series expansion of 1/x about eps: 2/eps - x/eps/eps
125124
Prevents infinities at x=0
126125
"""
127-
mask = x > eps
128-
oneoverx = np.where(x <= eps, 2.0 / eps - x / eps / eps, 0.0)
126+
mask = x > eps[0]
127+
oneoverx = np.where(x <= eps[0], 2.0 / eps[0] - x / eps[0] / eps[0], 0.0)
129128
oneoverx[mask] = 1.0 / x[mask]
130129
return oneoverx
131130

burnman/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# GPL v2 or later.
44

55
import scipy.constants
6+
import numpy as np
67

78
"""
89
molar gas constant (R) in J mol^-1 K^-1
@@ -42,3 +43,10 @@
4243
1 cm^-1 in J/mol
4344
"""
4445
invcm = 11.9627
46+
47+
48+
"""
49+
Small number for numerical stability
50+
Kept in list form for mutability
51+
"""
52+
logish_eps = [np.finfo(float).eps]

burnman/tools/equilibration.py

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from collections import namedtuple
1111
import logging
1212

13-
from ..optimize.nonlinear_solvers import damped_newton_solve
13+
from ..constants import logish_eps
14+
from ..optimize.nonlinear_solvers import damped_newton_solve, TerminationCode
1415
from ..classes.solution import Solution
1516

1617
P_scaling = 1.0e9 # Pa
@@ -1073,32 +1074,94 @@ def equilibrate(
10731074

10741075
F_tol = default_F_tolerances(assemblage, equality_constraints, n_atoms)
10751076

1076-
# Set the initial fractions and compositions
1077-
# of the phases in the assemblage:
1078-
sol = damped_newton_solve(
1079-
F=lambda x: F(
1077+
def F_fn(x):
1078+
return F(
10801079
x,
10811080
assemblage,
10821081
equality_constraints,
10831082
prm.reduced_composition_vector,
10841083
prm.reduced_free_composition_vectors,
1085-
),
1086-
J=lambda x: jacobian(
1084+
)
1085+
1086+
def J_fn(x):
1087+
return jacobian(
10871088
x,
10881089
assemblage,
10891090
equality_constraints,
10901091
prm.reduced_free_composition_vectors,
1091-
),
1092-
lambda_bounds=lambda dx, x: lambda_bounds(
1093-
dx, x, assemblage.endmembers_per_phase
1094-
),
1092+
)
1093+
1094+
def lambda_bounds_fn(dx, x):
1095+
return lambda_bounds(dx, x, assemblage.endmembers_per_phase)
1096+
1097+
linear_constraints = (prm.constraint_matrix, prm.constraint_vector)
1098+
1099+
sol = damped_newton_solve(
1100+
F=F_fn,
1101+
J=J_fn,
1102+
lambda_bounds=lambda_bounds_fn,
10951103
guess=parameters,
1096-
linear_constraints=(prm.constraint_matrix, prm.constraint_vector),
1104+
linear_constraints=linear_constraints,
10971105
tol=tol,
10981106
F_tol=F_tol,
10991107
store_iterates=store_iterates,
11001108
max_iterations=max_iterations,
11011109
)
1110+
# If we hit max iterations, we try relaxing the logish_eps
1111+
# and solving again. This helps convergence in hard problems because
1112+
# it allows larger steps when phase compositions are close to the
1113+
# boundaries of the solution (where the gradient in excess entropy is large).
1114+
if sol.code == TerminationCode.MAX_ITERATIONS:
1115+
old_logish_eps = logish_eps[0]
1116+
logish_eps[0] = 1.0e-5
1117+
c_shifts = sol.x[-n_free_compositional_vectors:]
1118+
new_parameters = get_parameters(assemblage, n_free_compositional_vectors)
1119+
new_parameters[-n_free_compositional_vectors:] = c_shifts
1120+
set_compositions_and_state_from_parameters(assemblage, parameters) # reset
1121+
1122+
sol2 = damped_newton_solve(
1123+
F=F_fn,
1124+
J=J_fn,
1125+
lambda_bounds=lambda_bounds_fn,
1126+
guess=new_parameters,
1127+
linear_constraints=linear_constraints,
1128+
tol=tol,
1129+
F_tol=F_tol,
1130+
store_iterates=store_iterates,
1131+
max_iterations=max_iterations,
1132+
)
1133+
1134+
logish_eps[0] = old_logish_eps
1135+
c_shifts = sol2.x[-n_free_compositional_vectors:]
1136+
new_parameters = get_parameters(assemblage, n_free_compositional_vectors)
1137+
new_parameters[-n_free_compositional_vectors:] = c_shifts
1138+
set_compositions_and_state_from_parameters(assemblage, parameters) # reset
1139+
sol3 = damped_newton_solve(
1140+
F=F_fn,
1141+
J=J_fn,
1142+
lambda_bounds=lambda_bounds_fn,
1143+
guess=new_parameters,
1144+
linear_constraints=linear_constraints,
1145+
tol=tol,
1146+
F_tol=F_tol,
1147+
store_iterates=store_iterates,
1148+
max_iterations=max_iterations,
1149+
)
1150+
1151+
# combine iterates from all three solves
1152+
sol3.n_it = sol.n_it + sol2.n_it + sol3.n_it
1153+
if store_iterates:
1154+
sol3.iterates.x = np.concatenate(
1155+
(sol.iterates.x, sol2.iterates.x, sol3.iterates.x), axis=0
1156+
)
1157+
sol3.iterates.F = np.concatenate(
1158+
(sol.iterates.F, sol2.iterates.F, sol3.iterates.F), axis=0
1159+
)
1160+
sol3.iterates.lmda = np.concatenate(
1161+
(sol.iterates.lmda, sol2.iterates.lmda, sol3.iterates.lmda),
1162+
axis=0,
1163+
)
1164+
sol = sol3
11021165

11031166
if sol.success and len(assemblage.reaction_affinities) > 0.0:
11041167
maxres = np.max(np.abs(assemblage.reaction_affinities)) + 1.0e-5

tests/test_equilibration.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,45 @@ def test_mg_rich_ol_wad_eqm_with_free_compositional_vector(self):
284284
)
285285
self.assertEqual(sol.code, TerminationCode.SUCCESS)
286286

287+
def test_very_mg_rich_ol_wad_eqm_with_free_compositional_vector(self):
288+
# Without both the scaling of parameters in equilibrate,
289+
# and adaptive relaxation of logish_eps, this problem fails to converge.
290+
assemblage = make_ol_wad_assemblage()
291+
ol = assemblage.phases[0]
292+
wad = assemblage.phases[1]
293+
294+
assemblage = burnman.Composite([ol, wad], [1.0, 0.0])
295+
296+
# Set the pressure and temperature
297+
P = 12.0e9 # Pa
298+
T = 1673.0 # K
299+
assemblage.set_state(P, T)
300+
301+
# Define the starting compositions of the phases
302+
x_fe_ol = 1.0e-10
303+
ol.set_composition([0.8, 0.2])
304+
wad.set_composition([0.7, 0.3])
305+
306+
# Set up the constraints and compositional degree of freedom
307+
composition = {"Fe": 0.1, "Mg": 1.9, "Si": 1.0, "O": 4.0}
308+
free_compositional_vectors = [{"Mg": 1.0, "Fe": -1.0}]
309+
310+
equality_constraints = [
311+
("T", T),
312+
(
313+
"phase_composition",
314+
(ol, [["Mg_A", "Fe_A"], [0.0, 1.0], [1.0, 1.0], x_fe_ol]),
315+
),
316+
("phase_fraction", (wad, 0.5)),
317+
]
318+
sol, _ = equilibrate(
319+
composition,
320+
assemblage,
321+
equality_constraints,
322+
free_compositional_vectors,
323+
)
324+
self.assertEqual(sol.code, TerminationCode.SUCCESS)
325+
287326
def test_convergence_with_singular_system(self):
288327

289328
composition = {"Mg": 1.0, "Fe": 1.0, "Si": 1.0, "O": 4.0}

0 commit comments

Comments
 (0)