/
polytope.py
262 lines (213 loc) · 9.29 KB
/
polytope.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
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit
# for the Earth and Planetary Sciences
# Copyright (C) 2012 - 2021 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 scipy.linalg import block_diag
import logging
import importlib
from ..classes.polytope import MaterialPolytope, independent_row_indices
from ..classes.solution import Solution
from ..classes.composite import Composite
from .solution import transform_solution_to_new_basis
try:
cp = importlib.import_module("cvxpy")
except ImportError as err:
print(
f"Warning: {err}. " "For full functionality of BurnMan, please install cvxpy."
)
def solution_polytope_from_charge_balance(
charges, charge_total, return_fractions=False
):
"""
Creates a polytope object from a list of the charges for each species on
each site and the total charge for all site-species.
:param charges: 2D list containing the total charge for species j on site i,
including the site multiplicity. So, for example,
a solution with the site formula [Mg,Fe]3[Mg,Al,Si]2Si3O12 would
have the following list: [[6., 6.], [4., 6., 8.]].
:type charges: 2D list of floats
:param charge_total: The total charge for all site-species per formula unit.
The example given above would have charge_total = 12.
:type charge_total: float
:param return_fractions: Determines whether the created polytope object returns its
attributes (such as endmember occupancies) as fractions or as floats.
Default is False.
:type return_fractions: bool
:returns: A polytope object corresponding to the parameters provided.
:rtype: :class:`burnman.polytope.MaterialPolytope` object
"""
n_sites = len(charges)
all_charges = np.concatenate(charges)
n_site_elements = len(all_charges)
equalities = np.empty((n_sites + 1, n_site_elements + 1))
equalities[:-1, 0] = -1
i = 0
for i_site, site_charges in enumerate(charges):
equalities[i_site, 1:] = [
1 if (j >= i and j < i + len(site_charges)) else 0
for j in range(n_site_elements)
]
i += len(site_charges)
equalities[-1, 0] = -charge_total
equalities[-1, 1:] = all_charges
pos_constraints = np.concatenate(
(np.zeros((len(equalities[0]) - 1, 1)), np.identity(len(equalities[0]) - 1)),
axis=1,
)
return MaterialPolytope(
equalities, pos_constraints, return_fractions=return_fractions
)
def solution_polytope_from_endmember_occupancies(
endmember_occupancies, return_fractions=False
):
"""
Creates a polytope object from a list of independent endmember occupancies.
:param endmember_occupancies: 2D list containing the
site-species occupancies j for endmember i.
So, for example, a solution with independent endmembers
[Mg]3[Al]2Si3O12, [Mg]3[Mg0.5Si0.5]2Si3O12, [Fe]3[Al]2Si3O12
might have the following array:
[[1., 0., 1., 0., 0.],
[1., 0., 0., 0.5, 0.5],
[0., 1., 1., 0., 0.]],
where the order of site-species is Mg_A, Fe_A, Al_B, Mg_B, Si_B.
:type endmember_occupancies: 2D numpy array
:param return_fractions: Determines whether the created polytope object
returns its attributes (such as endmember occupancies) as fractions
or as floats.
:type return_fractions: bool
:returns: A polytope object corresponding to the parameters provided.
:rtype: :class:`burnman.polytope.MaterialPolytope` object
"""
n_sites = sum(endmember_occupancies[0])
n_occs = endmember_occupancies.shape[1]
nullspace = np.array(Matrix(endmember_occupancies).nullspace(), dtype=float)
equalities = np.zeros((len(nullspace) + 1, n_occs + 1))
equalities[0, 0] = -n_sites
equalities[0, 1:] = 1
if len(nullspace) > 0:
try:
equalities[1:, 1:] = nullspace
except ValueError:
equalities[1:, 1:] = nullspace[:, :, 0]
pos_constraints = np.concatenate(
(np.zeros((len(equalities[0]) - 1, 1)), np.identity(len(equalities[0]) - 1)),
axis=1,
)
return MaterialPolytope(
equalities,
pos_constraints,
return_fractions=return_fractions,
independent_endmember_occupancies=endmember_occupancies,
)
def composite_polytope_at_constrained_composition(
composite, composition, return_fractions=False
):
"""
Creates a polytope object from a Composite object and a composition.
This polytope describes the complete set of valid composite
endmember amounts that satisfy the compositional constraints.
:param composite: A composite containing one or more Solution and Mineral objects.
:type composite: :class:`burnman.Composite` object
:param composition: A dictionary containing the amounts of each element.
:type composition: dict
:param return_fractions: Determines whether the created polytope object returns its
attributes (such as endmember occupancies) as fractions or as floats.
:type return_fractions: bool
:returns: A polytope object corresponding to the parameters provided.
:rtype: :class:`burnman.polytope.MaterialPolytope` object
"""
c_array = np.empty((composite.n_elements, 1))
c_array[:, 0] = [
-composition[e] if e in composition else 0.0 for e in composite.elements
]
equalities = np.concatenate((c_array, composite.stoichiometric_array.T), axis=1)
eoccs = []
for i, ph in enumerate(composite.phases):
if isinstance(ph, Solution):
eoccs.append(ph.solution_model.endmember_occupancies.T)
else:
eoccs.append(np.ones((1, 1)))
eoccs = block_diag(*eoccs)
inequalities = np.concatenate((np.zeros((len(eoccs), 1)), eoccs), axis=1)
return MaterialPolytope(
equalities, inequalities, number_type="float", return_fractions=return_fractions
)
def simplify_composite_with_composition(composite, composition):
"""
Takes a composite and a composition, and returns the simplest composite
object that spans the solution space at the given composition.
For example, if the composition is given as {'Mg': 2., 'Si': 1.5, 'O': 5.},
and the composite is given as a mix of Mg,Fe olivine and pyroxene
solutions, this function will return a composite that only contains
the Mg-bearing endmembers.
:param composite: The initial Composite object.
:type composite: :class:`burnman.Composite` object
:param composition: A dictionary containing the amounts of each element.
:type composition: dict
:returns: The simplified Composite object
:rtype: :class:`burnman.Composite` object
"""
polytope = composite_polytope_at_constrained_composition(
composite, composition, return_fractions=True
)
composite_changed = False
new_phases = []
mbr_amounts = polytope.endmember_occupancies
i = 0
for i_ph, n_mbrs in enumerate(composite.endmembers_per_phase):
ph = composite.phases[i_ph]
amounts = mbr_amounts[:, i : i + n_mbrs].astype(float)
i += n_mbrs
rank = np.linalg.matrix_rank(amounts, tol=1.0e-8)
if rank < n_mbrs:
if isinstance(ph, Solution) and rank > 0:
if len(amounts) > 1:
c_mean = np.mean(amounts, axis=0)
else:
c_mean = amounts[0]
poly = solution_polytope_from_endmember_occupancies(
ph.solution_model.endmember_occupancies
)
dmbrs = poly.endmembers_as_independent_endmember_amounts
x = cp.Variable(dmbrs.shape[0])
objective = cp.Minimize(cp.sum_squares(x))
constraints = [dmbrs.T @ x == c_mean, x >= 0]
prob = cp.Problem(objective, constraints)
prob.solve()
mbr_indices = np.argsort(x.value)[::-1]
ind_indices = [i for i in mbr_indices if x.value[i] > 1.0e-6]
new_basis = dmbrs[ind_indices]
# And now reduce the new basis if necessary
new_basis = new_basis[independent_row_indices(new_basis)]
if len(new_basis) < ph.n_endmembers:
logging.info(
f"Phase {i_ph} ({ph.name}) is "
"rank-deficient ({rank} < {n_mbrs}). "
"The transformed solution is described "
f"using {len(new_basis)} endmembers."
)
composite_changed = True
soln = transform_solution_to_new_basis(ph, new_basis)
new_phases.append(soln)
else:
logging.info(
"This solution is rank-deficient "
f"({rank} < {n_mbrs}), "
"but its composition requires all "
"independent endmembers."
)
else:
composite_changed = True
logging.info(
f"Phase {i_ph} ({ph.name}) removed from " "composite (rank = 0)."
)
else:
new_phases.append(ph)
if composite_changed:
return Composite(new_phases)
else:
return composite