-
Notifications
You must be signed in to change notification settings - Fork 41
/
solution.py
372 lines (318 loc) · 12.6 KB
/
solution.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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit for
# the Earth and Planetary Sciences
# Copyright (C) 2012 - 2022 by the BurnMan team, released under the GNU
# GPL v2 or later.
from __future__ import absolute_import
import numpy as np
from sympy import Matrix
from ..classes.combinedmineral import CombinedMineral
from ..classes.solution import Solution
from ..classes.solutionmodel import (
IdealSolution,
SymmetricRegularSolution,
AsymmetricRegularSolution,
SubregularSolution,
)
from ..utils.chemistry import site_occupancies_to_strings
from ..utils.math import complete_basis
def _decompose_3D_matrix(W):
"""
Decomposes a 3D matrix W_ijk where E = W_ijk p_i p_j p_k
into a subregular form where
E = G_i p_i + WB_ij (1 - p_j + p_i) / 2 + WT_ijk p_i p_j p_k,
and i < j < k.
:param W: 3D interaction matrix.
:type W: numpy.array
:returns: The 1D array G_i, the 2D upper triangular array WB_ij and
the ternary terms in a list where each item is in the form [i, j, k, WT_ijk]
:rtype: tuple
"""
n_mbrs = len(W)
# New endmember components
# W_iii needs to be copied, otherwise just a view onto W
new_endmember_excesses = np.copy(np.einsum("iii->i", W))
# Removal of endmember components from 3D representation
W -= (
np.einsum(
"i, j, k->ijk", new_endmember_excesses, np.ones(n_mbrs), np.ones(n_mbrs)
)
+ np.einsum(
"i, j, k->ijk", np.ones(n_mbrs), new_endmember_excesses, np.ones(n_mbrs)
)
+ np.einsum(
"i, j, k->ijk", np.ones(n_mbrs), np.ones(n_mbrs), new_endmember_excesses
)
) / 3.0
# Transformed 2D components
# (i=j, i=k, j=k)
new_binary_matrix = (
np.einsum("jki, jk -> ij", W, np.identity(n_mbrs))
+ np.einsum("jik, jk -> ij", W, np.identity(n_mbrs))
+ np.einsum("ijk, jk -> ij", W, np.identity(n_mbrs))
).round(decimals=12)
# Wb is the 3D matrix corresponding to the terms in the binary matrix,
# such that the two following print statements produce the same answer
# for a given array of endmember proportions
Wb = (
np.einsum("ijk, ij->ijk", W, np.identity(n_mbrs))
+ np.einsum("ijk, jk->ijk", W, np.identity(n_mbrs))
+ np.einsum("ijk, ik->ijk", W, np.identity(n_mbrs))
)
# Remove binary component from 3D representation
# The extra terms are needed because the binary term in the formulation
# of a subregular solution model given by
# Helffrich and Wood includes ternary components (the sum_k X_k part)..
W -= (
Wb
+ (
np.einsum("ij, k", new_binary_matrix, np.ones(n_mbrs))
- np.einsum("ij, ik->ijk", new_binary_matrix, np.identity(n_mbrs))
- np.einsum("ij, jk->ijk", new_binary_matrix, np.identity(n_mbrs))
)
/ 2.0
)
# Find the 3D components Wijk by adding the elements at
# the six equivalent positions in the matrix
new_ternary_terms = []
for i in range(n_mbrs):
for j in range(i + 1, n_mbrs):
for k in range(j + 1, n_mbrs):
val = (
W[i, j, k]
+ W[j, k, i]
+ W[k, i, j]
+ W[k, j, i]
+ W[j, i, k]
+ W[i, k, j]
).round(decimals=12)
if np.abs(val) > 1.0e-12:
new_ternary_terms.append([i, j, k, val])
return (new_endmember_excesses, new_binary_matrix, new_ternary_terms)
def _subregular_matrix_conversion(
new_basis, binary_matrix, ternary_terms=None, endmember_excesses=None
):
"""
Converts the arrays reguired to describe a subregular solution
from one endmember basis to another.
The excess nonconfigurational energies of the subregular solution model
are described as follows:
E = G_i p_i + WB_ij (1 - p_j + p_i) / 2 + WT_ijk p_i p_j p_k,
and i < j < k.
:param new_basis: The new endmember basis, given as amounts of the old endmembers.
:type new_basis: 2D numpy array
:param binary_matrix: The upper triangular matrix WB_ij.
:type binary_matrix: 2D numpy array
:param ternary_terms: The ternary terms in a list where each
item is in the form [i, j, k, WT_ijk]
:type ternary_terms: list of lists of length 4
:param endmember_excesses: The array G_i
:type endmember_excesses: 1D numpy array
:returns: The 1D array G_i, the 2D upper triangular array WB_ij and
the ternary terms in a list where each item is in the form [i, j, k, WT_ijk]
:rtype: tuple
"""
n_mbrs = len(binary_matrix)
# Compact 3D representation of original interactions
W = (
np.einsum("i, jk -> ijk", np.ones(n_mbrs), binary_matrix)
+ np.einsum("ij, jk -> ijk", binary_matrix, np.identity(n_mbrs))
- np.einsum("ij, ik -> ijk", binary_matrix, np.identity(n_mbrs))
) / 2.0
# Add endmember components to 3D representation
if endmember_excesses is not None:
W += (
np.einsum(
"i, j, k->ijk", endmember_excesses, np.ones(n_mbrs), np.ones(n_mbrs)
)
+ np.einsum(
"j, i, k->ijk", endmember_excesses, np.ones(n_mbrs), np.ones(n_mbrs)
)
+ np.einsum(
"k, i, j->ijk", endmember_excesses, np.ones(n_mbrs), np.ones(n_mbrs)
)
) / 3.0
# Add ternary values to 3D representation
if ternary_terms is not None:
for i, j, k, val in ternary_terms:
W[i, j, k] += val
# Transformation to new 3D representation
A = new_basis.T
Wn = np.einsum("il, jm, kn, ijk -> lmn", A, A, A, W)
new_endmember_excesses, new_binary_terms, new_ternary_terms = _decompose_3D_matrix(
Wn
)
return (new_endmember_excesses, new_binary_terms, new_ternary_terms)
def transform_solution_to_new_basis(
solution,
new_basis,
n_mbrs=None,
solution_name=None,
endmember_names=None,
molar_fractions=None,
):
"""
Transforms a solution model from one endmember basis to another.
Returns a new Solution object.
:param solution: The original solution object.
:type solution: :class:`burnman.Solution` object
:param new_basis: The new endmember basis, given as amounts of the old endmembers.
:type new_basis: 2D numpy array
:param n_mbrs: The number of endmembers in the new solution
(defaults to the length of new_basis).
:type n_mbrs: float, optional
:param solution_name: A name corresponding to the new solution.
:type solution_name: str, optional
:param endmember_names: A list corresponding to the names of the new endmembers.
:type endmember_names: list of str, optional
:param molar_fractions: Fractions of the new endmembers in the new solution.
:type molar_fractions: numpy.array, optional
:returns: The transformed solution.
:rtype: :class:`burnman.Solution` object
"""
new_basis = np.array(new_basis)
if n_mbrs is None:
n_mbrs, n_all_mbrs = new_basis.shape
else:
_, n_all_mbrs = new_basis.shape
if solution_name is None:
name = "child solution"
else:
name = solution_name
solution_model = solution.solution_model
# Use type here to avoid inheritance problems
solution_type = type(solution_model)
if solution_type == IdealSolution:
ESV_modifiers = [[0.0, 0.0, 0.0] for v in new_basis]
if (
solution_type == AsymmetricRegularSolution
or solution_type == SymmetricRegularSolution
):
A = complete_basis(new_basis).T
old_alphas = solution.solution_model.alphas
alphas = np.einsum("i, ij", solution.solution_model.alphas, A)
inv_diag_alphas = np.diag(1.0 / alphas)
B = np.einsum("ij, jk, kl->il", np.diag(old_alphas), A, inv_diag_alphas)
alphas = list(alphas[0:n_mbrs])
Qe = np.einsum("ik, ij, kl->jl", solution.solution_model.We, B, B)
Qs = np.einsum("ik, ij, kl->jl", solution.solution_model.Ws, B, B)
Qv = np.einsum("ik, ij, kl->jl", solution.solution_model.Wv, B, B)
def new_interactions(Q, n_mbrs):
return [
[
float(
(Q[i, j] + Q[j, i] - Q[i, i] - Q[j, j])
* (alphas[i] + alphas[j])
/ 2.0
)
for j in range(i + 1, n_mbrs)
]
for i in range(n_mbrs - 1)
]
energy_interaction = new_interactions(Qe, n_mbrs)
entropy_interaction = new_interactions(Qs, n_mbrs)
volume_interaction = new_interactions(Qv, n_mbrs)
ESV_modifiers = [
[Qe[i, i] * alphas[i], Qs[i, i] * alphas[i], Qv[i, i] * alphas[i]]
for i in range(n_mbrs)
]
elif solution_type == SubregularSolution:
full_basis = complete_basis(new_basis)
def new_interactions(W, n_mbrs):
return [
[[W[i, j], W[j, i]] for j in range(i + 1, n_mbrs)]
for i in range(n_mbrs - 1)
]
# N.B. initial endmember_excesses are zero
Emod, We, ternary_e = _subregular_matrix_conversion(
full_basis,
solution.solution_model.We,
solution.solution_model.ternary_terms_e,
)
Smod, Ws, ternary_s = _subregular_matrix_conversion(
full_basis,
solution.solution_model.Ws,
solution.solution_model.ternary_terms_s,
)
Vmod, Wv, ternary_v = _subregular_matrix_conversion(
full_basis,
solution.solution_model.Wv,
solution.solution_model.ternary_terms_v,
)
energy_interaction = new_interactions(We, n_mbrs)
entropy_interaction = new_interactions(Ws, n_mbrs)
volume_interaction = new_interactions(Wv, n_mbrs)
ESV_modifiers = [[Emod[i], Smod[i], Vmod[i]] for i in range(n_mbrs)]
else:
raise Exception(
"The function to change basis for the "
"{0} solution type has not yet been "
"implemented.".format(solution_type)
)
# Create site formulae
new_occupancies = np.array(new_basis).dot(
solution.solution_model.endmember_occupancies
)
new_multiplicities = np.array(new_basis).dot(
solution.solution_model.site_multiplicities
)
site_formulae = site_occupancies_to_strings(
solution.solution_model.sites, new_multiplicities, new_occupancies
)
# Create endmembers
endmembers = []
for i, vector in enumerate(new_basis):
nonzero_indices = np.nonzero(vector)[0]
if len(nonzero_indices) == 1:
endmembers.append(
[solution_model.endmembers[nonzero_indices[0]][0], site_formulae[i]]
)
else:
mbr = CombinedMineral(
[solution_model.endmembers[idx][0] for idx in nonzero_indices],
[vector[idx] for idx in nonzero_indices],
ESV_modifiers[i],
)
mbr.params["formula"] = {
key: value
for (key, value) in mbr.params["formula"].items()
if value > 1.0e-12
}
endmembers.append([mbr, site_formulae[i]])
if endmember_names is not None:
for i in range(n_mbrs):
endmembers[i][0].params["name"] = endmember_names[i]
endmembers[i][0].name = endmember_names[i]
if n_mbrs == 1:
endmembers[0][0].name = name
endmembers[0][0].parent = solution
endmembers[0][0].basis = new_basis
return endmembers[0][0]
else:
if solution_type == IdealSolution:
new_solution_model = IdealSolution(endmembers=endmembers)
elif (
solution_type == SymmetricRegularSolution
or solution_type == SubregularSolution
):
new_solution_model = type(solution_model)(
endmembers=endmembers,
energy_interaction=energy_interaction,
volume_interaction=volume_interaction,
entropy_interaction=entropy_interaction,
)
else:
new_solution_model = type(solution_model)(
endmembers=endmembers,
energy_interaction=energy_interaction,
volume_interaction=volume_interaction,
entropy_interaction=entropy_interaction,
alphas=alphas,
)
new_solution = Solution(
name=name,
solution_model=new_solution_model,
molar_fractions=molar_fractions,
)
new_solution.parent = solution
new_solution.basis = new_basis
return new_solution