pyrolite 0.0.14__zip

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__init__.py +10 -0
  2. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/__init__.cpython-36.pyc +0 -0
  3. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/_version.cpython-36.pyc +0 -0
  4. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/alteration.cpython-36.pyc +0 -0
  5. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/classification.cpython-36.pyc +0 -0
  6. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/compositions.cpython-36.pyc +0 -0
  7. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/geochem.cpython-36.pyc +0 -0
  8. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/melts.cpython-36.pyc +0 -0
  9. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/norm.cpython-36.pyc +0 -0
  10. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/normalisation.cpython-36.pyc +0 -0
  11. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/plot.cpython-36.pyc +0 -0
  12. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/_version.py +21 -0
  13. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/alteration.py +66 -0
  14. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/classification.py +222 -0
  15. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__init__.py +9 -0
  16. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/__init__.cpython-36.pyc +0 -0
  17. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/aggregate.cpython-36.pyc +0 -0
  18. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/codata.cpython-36.pyc +0 -0
  19. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/impute.cpython-36.pyc +0 -0
  20. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/renorm.cpython-36.pyc +0 -0
  21. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/aggregate.py +391 -0
  22. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/codata.py +266 -0
  23. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/impute.py +82 -0
  24. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/renorm.py +40 -0
  25. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/compositions.py +524 -0
  26. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_CFB_Dataset_List.csv +42 -0
  27. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_Convergent_Dataset_List.csv +42 -0
  28. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OBFB_Dataset_List.csv +5 -0
  29. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OIB_Dataset_List.csv +49 -0
  30. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OceanicPlateau_Dataset_List.csv +18 -0
  31. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/contents.json +1 -0
  32. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/__pycache__/env.cpython-35.pyc +0 -0
  33. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/__pycache__/env.cpython-36.pyc +0 -0
  34. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/env.py +1063 -0
  35. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Ba.modelfield +0 -0
  36. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Bs.modelfield +0 -0
  37. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.F.modelfield +0 -0
  38. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O1.modelfield +0 -0
  39. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O2.modelfield +0 -0
  40. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O3.modelfield +0 -0
  41. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Pc.modelfield +0 -0
  42. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Ph.modelfield +0 -0
  43. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.R.modelfield +0 -0
  44. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S1.modelfield +0 -0
  45. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S2.modelfield +0 -0
  46. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S3.modelfield +0 -0
  47. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.T1.modelfield +0 -0
  48. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.T2.modelfield +0 -0
  49. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U1.modelfield +0 -0
  50. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U2.modelfield +0 -0
  51. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U3.modelfield +0 -0
  52. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.modelfields +0 -0
  53. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.nan.modelfield +0 -0
  54. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.none.modelfield +0 -0
  55. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS.clsf.gz +0 -0
  56. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/aphanitic.clsf.gz +0 -0
  57. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/gabbroic.clsf.gz +0 -0
  58. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/peralkalinity.clsf.gz +0 -0
  59. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/phaneritic.clsf.gz +0 -0
  60. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/ultramafic.clsf.gz +0 -0
  61. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/CH_PalmeONeill2014.csv +95 -0
  62. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DDMM_WorkmanHart2005.csv +105 -0
  63. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DMM_WorkmanHart2005.csv +105 -0
  64. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DM_SaltersStrake2004.csv +95 -0
  65. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/EDMM_WorkmanHart2005.csv +105 -0
  66. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/PM_PalmeONeill2014.csv +95 -0
  67. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/timescale/geotimescale_spans.csv +180 -0
  68. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/geochem.py +821 -0
  69. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/melts.py +92 -0
  70. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__init__.py +10 -0
  71. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/__init__.cpython-36.pyc +0 -0
  72. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/db.cpython-36.pyc +0 -0
  73. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/ions.cpython-36.pyc +0 -0
  74. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/mineral.cpython-36.pyc +0 -0
  75. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/sites.cpython-36.pyc +0 -0
  76. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/db.py +88 -0
  77. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/ions.py +78 -0
  78. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/mineral.py +587 -0
  79. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/sites.py +134 -0
  80. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/norm.py +224 -0
  81. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/normalisation.py +204 -0
  82. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/plot.py +514 -0
  83. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__init__.py +13 -0
  84. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/__init__.cpython-36.pyc +0 -0
  85. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/database.cpython-36.pyc +0 -0
  86. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/env.cpython-36.pyc +0 -0
  87. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/general.cpython-36.pyc +0 -0
  88. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/georoc.cpython-36.pyc +0 -0
  89. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/math.cpython-36.pyc +0 -0
  90. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/melts.cpython-36.pyc +0 -0
  91. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/multip.cpython-36.pyc +0 -0
  92. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/multiprocessing.cpython-36.pyc +0 -0
  93. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/pd.cpython-36.pyc +0 -0
  94. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/plot.cpython-36.pyc +0 -0
  95. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/skl.cpython-36.pyc +0 -0
  96. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/spatial.cpython-36.pyc +0 -0
  97. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/text.cpython-36.pyc +0 -0
  98. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/time.cpython-36.pyc +0 -0
  99. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/wfs.cpython-36.pyc +0 -0
  100. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/database.py +88 -0
  101. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/env.py +81 -0
  102. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/general.py +266 -0
  103. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/georoc.py +444 -0
  104. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/math.py +371 -0
  105. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/melts.py +397 -0
  106. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/multip.py +29 -0
  107. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/multiprocessing.py +29 -0
  108. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/pd.py +214 -0
  109. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/plot.py +345 -0
  110. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/skl.py +847 -0
  111. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/spatial.py +91 -0
  112. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/text.py +207 -0
  113. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/time.py +224 -0
  114. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/wfs.py +10 -0
  115. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/PKG-INFO +61 -0
  116. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/SOURCES.txt +83 -0
  117. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/dependency_links.txt +1 -0
  118. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/requires.txt +47 -0
  119. ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/top_level.txt +1 -0
@@ -0,0 +1,587 @@
1
+ import pandas as pd
2
+ import pandas_flavor as pf
3
+ import numpy as np
4
+ import periodictable as pt
5
+ from pyrolite.geochem import to_molecular
6
+ from pyrolite.comp.codata import renormalise
7
+ from pyrolite.util.pd import to_frame
8
+ from collections import OrderedDict
9
+ from scipy.optimize import least_squares, Bounds
10
+ from .sites import *
11
+ import logging
12
+
13
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MineralTemplate(object):
18
+ """
19
+ Generic mineral stucture template. Formatted collection of crystallographic sites.
20
+ """
21
+
22
+ def __init__(self, name, *components):
23
+ self.name = name
24
+ self.structure = {}
25
+ self.site_occupancy = None
26
+ self.set_structure(*components)
27
+
28
+ def set_structure(self, *components):
29
+ """
30
+ Set the structure of the mineral template.
31
+
32
+ Parameters
33
+ ----------
34
+ components
35
+ Argument list consisting of each of the structural components. Can consist
36
+ of any mixture of Sites or argument tuples which can be passed to
37
+ Site __init__.
38
+ """
39
+ self.components = list(components)
40
+ self.structure = OrderedDict()
41
+ if len(components):
42
+ _bag = []
43
+ for c in self.components:
44
+ if not isinstance(c, Site):
45
+ c = Site(c)
46
+ if c not in _bag:
47
+ _bag.append(c)
48
+
49
+ for item in _bag:
50
+ self.structure[item] = self.components.count(item)
51
+
52
+ self.affinities = {c: c.affinities for c in self.structure}
53
+ self.ideal_cations = sum(
54
+ [c.cationic * self.structure[c] for c in self.structure]
55
+ )
56
+ self.ideal_oxygens = sum([c.oxygen * self.structure[c] for c in self.structure])
57
+
58
+ def copy(self):
59
+ return MineralTemplate(self.name, *self.components)
60
+
61
+ def __repr__(self):
62
+ if self.structure != {}:
63
+ component_string = ", ".join(
64
+ ["{}".format(c.__repr__()) for c in list(self.structure)]
65
+ )
66
+ return """{}("{}", {})""".format(
67
+ self.__class__.__name__, self.name, component_string
68
+ )
69
+ else:
70
+ return """{}("{}")""".format(self.__class__.__name__, self.name)
71
+
72
+ def __str__(self):
73
+ if self.structure != {}:
74
+ structure = self.structure
75
+ c_list = []
76
+ names = [c.name for c in list(structure)]
77
+ counts = [structure[c] for c in list(structure)]
78
+ for site in list(structure):
79
+ n, c = site.name, structure[site]
80
+ if c > 1:
81
+ c_str = "[{}]$_{}$".format(n, c)
82
+ else:
83
+ c_str = "[{}]".format(n)
84
+ c_list.append(c_str)
85
+ component_string = "".join(c_list)
86
+ return """`{}` {}""".format(self.name, component_string)
87
+ else:
88
+ return """`{}`""".format(self.name)
89
+
90
+ def __hash__(self):
91
+ return hash(self.__repr__().encode("UTF-8"))
92
+
93
+
94
+ class Mineral(object):
95
+ """Mineral, with structure and composition."""
96
+
97
+ db = {}
98
+
99
+ def __init__(self, name=None, template=None, composition=None, endmembers=None):
100
+ self.name = name
101
+ self.template = None
102
+ self.composition = None
103
+ self.formula = None
104
+ self.endmembers = {}
105
+ self.set_template(template)
106
+ self.set_composition(composition)
107
+ self.set_endmembers(endmembers)
108
+ self.endmember_decomposition = None
109
+ self.init = True
110
+ self.db[self.name] = self
111
+
112
+ def set_endmembers(self, endmembers=None):
113
+ """Set the endmbmer components for a mineral."""
114
+ if endmembers is not None:
115
+ if isinstance(endmembers, list):
116
+ for em in endmembers:
117
+ self.add_endmember(em)
118
+ elif isinstance(endmembers, dict):
119
+ for name, em in endmembers.items():
120
+ self.add_endmember(em, name=name)
121
+
122
+ def add_endmember(self, em, name=None):
123
+ """Add a single endmember to the database."""
124
+ min = em
125
+ if isinstance(min, tuple):
126
+ name, min = min
127
+ if min is not None:
128
+ # process different options for getting a mineral output
129
+ if isinstance(min, str):
130
+ min = self.db.get(em, None)
131
+ elif isinstance(min, pt.formulas.Formula):
132
+ name = name or str(min)
133
+ min = Mineral(name, None, min)
134
+ else:
135
+ pass
136
+
137
+ name = name or min.name
138
+ self.endmembers[name] = min
139
+
140
+ def set_template(self, template, name=None):
141
+ """
142
+ Assign a mineral template to the mineral.
143
+ """
144
+ if template is not None:
145
+ if name is None:
146
+ name = self.name
147
+ if not isinstance(template, MineralTemplate):
148
+ template = MineralTemplate(name, *template)
149
+ else:
150
+ template = template.copy()
151
+ else:
152
+ template = MineralTemplate("")
153
+ if template is not None:
154
+ logger.debug("Setting Template: {}".format(template))
155
+ else:
156
+ logger.debug("Clearing Template")
157
+ self.template = template
158
+ self.sites = [i for i in list(self.template.structure)]
159
+ self.recalculate_cations()
160
+
161
+ def set_composition(self, composition=None):
162
+ """
163
+ Parse and assign a composition to the mineral.
164
+
165
+ Parameters
166
+ ---------
167
+ composition
168
+ Composition to assign to the mineral. Can be provided in any form which is
169
+ digestable by parse_composition.
170
+ """
171
+ if isinstance(composition, pt.formulas.Formula):
172
+ self.formula = composition
173
+ composition = parse_composition(composition)
174
+ if composition is not None:
175
+ logger.debug(
176
+ "Setting Composition: {}".format(
177
+ {k: np.round(v, 4) for k, v in composition.to_dict().items()}
178
+ )
179
+ )
180
+ else:
181
+ logger.debug("Clearing Composition")
182
+
183
+ self.composition = composition
184
+ self.recalculate_cations()
185
+
186
+ def recalculate_cations(
187
+ self,
188
+ composition=None,
189
+ ideal_cations=None,
190
+ ideal_oxygens=None,
191
+ Fe_species=["FeO", "Fe", "Fe2O3"],
192
+ oxygen_constrained=False,
193
+ ):
194
+ """
195
+ Recalculate a composition to give an elemental ionic breakdown.
196
+
197
+ Parameters
198
+ ----------
199
+ composition
200
+ Composition to recalculate. If not provided, will try to use the mineral
201
+ composition as set.
202
+ ideal_cations : int
203
+ Ideal number of cations to use for formulae calcuations. Will only be used
204
+ if oxygen is constrained (i.e. multiple Fe species present or
205
+ oxygen_constrained=True).
206
+ ideal_oxygens : int
207
+ Ideal number of oxygens to use for formulae calcuations. Will only be used
208
+ if oxygen is not constrained (i.e. single Fe species present and
209
+ oxygen_constrained=False).
210
+ Fe_species : list
211
+ List of iron species for identifying redox-defined compositions.
212
+ oxygen_constrained : bool, False
213
+ Whether the oxygen is a closed or open system for the specific composition.
214
+ """
215
+ composition = composition or self.composition
216
+
217
+ if composition is not None:
218
+ ideal_cations = ideal_cations or self.template.ideal_cations
219
+ ideal_oxygens = ideal_oxygens or self.template.ideal_cations
220
+
221
+ self.cationic_composition = recalc_cations(
222
+ self.composition,
223
+ ideal_cations=ideal_cations,
224
+ ideal_oxygens=ideal_oxygens,
225
+ Fe_species=Fe_species,
226
+ oxygen_constrained=oxygen_constrained,
227
+ )
228
+ return self.cationic_composition
229
+
230
+ def apfu(self):
231
+ """
232
+ Get the atoms per formula unit.
233
+ """
234
+ # recalculate_cations return apfu by default
235
+ return self.recalculate_cations()
236
+
237
+ def endmember_decompose(self, det_lim=0.01):
238
+ """
239
+ Decompose a mineral composition into endmember components.
240
+
241
+ Parameters
242
+ ----------
243
+ det_lim : float
244
+ Detection limit for individual
245
+
246
+ Notes
247
+ -----
248
+ Currently implmented using optimization based on mass fractions.
249
+
250
+ Todo
251
+ -----
252
+ Implement site-based endmember decomposition, which will enable more checks and
253
+ balances.
254
+ """
255
+ assert self.endmembers is not None
256
+
257
+ # take endmembers with components which may be present in composition
258
+ _target_components = set(self.composition.index.values)
259
+ potential_components = []
260
+ for em, tem in self.endmembers.items():
261
+ _components = set(tem.composition.index.values)
262
+ if _components.issubset(_target_components):
263
+ potential_components.append((em, tem))
264
+
265
+ compositions = pd.concat(
266
+ [c.composition for em, c in potential_components], axis=1, sort=False
267
+ ).fillna(0)
268
+ compositions.columns = [em for em, c in potential_components]
269
+ weights = np.ones((compositions.columns.size))
270
+ weights /= weights.sum()
271
+
272
+ x = compositions.values.T
273
+ y = self.composition.reindex(compositions.index).fillna(0).values
274
+
275
+ def mixture(weights, x, y):
276
+ return weights @ x - y
277
+
278
+ res = least_squares(
279
+ mixture,
280
+ weights,
281
+ bounds=([0.0] * weights.shape[0], [1.0] * weights.shape[0]),
282
+ args=(x, y),
283
+ )
284
+ abundances, cost = res.x, res.cost
285
+ if cost > det_lim:
286
+ logger.warn("Residuals are higher than detection limits.")
287
+
288
+ # convert abundances to molecular
289
+ abundances = pd.Series(
290
+ {c: v for (c, v) in zip(compositions.columns, abundances)}
291
+ )
292
+ abundances = abundances.div([c.formula.mass for em, c in potential_components])
293
+ abundances = abundances.div(abundances.sum())
294
+ abundances.loc[
295
+ (np.isclose(abundances, 0.0, atol=1e-06) | (abundances <= det_lim))
296
+ ] = np.nan
297
+ abundances = abundances.loc[~pd.isnull(abundances)]
298
+ abundances /= abundances.sum()
299
+ # optimise decomposition into endmember components
300
+ self.endmember_decomposition = abundances.to_dict()
301
+ return self.endmember_decomposition
302
+
303
+ def calculate_occupancy(
304
+ self, composition=None, error=10e-6, balances=[["Fe{2+}", "Mg{2+}"]]
305
+ ):
306
+ """
307
+ Calculate the estimated site occupancy for a given composition.
308
+ Ions will be assigned to sites according to affinities. Sites with equal
309
+ affinities should recieve equal assignment.
310
+
311
+ Parameters
312
+ -----------
313
+ composition
314
+ Composition to calculate site occupancy for.
315
+ error : float
316
+ Absolute error for floating point occupancy calculations.
317
+ balances : list
318
+ List of iterables containing ions to balance across multiple sites. Note
319
+ that the partitioning will occur after non-balanced cations are assigned,
320
+ and that ions are only balanced between sites which have defined affinities
321
+ for all of the particular ions defined in the 'balance'.
322
+ """
323
+ if self.template is not None:
324
+ if composition is None:
325
+ self.recalculate_cations()
326
+ composition = self.cationic_composition
327
+ else:
328
+ composition = parse_composition(composition)
329
+
330
+ if composition is None:
331
+ logger.warn('Composition not set. Cannot calculate occupancy.')
332
+
333
+ affinities = pd.DataFrame(
334
+ [site.affinities for site in self.template.structure]
335
+ ).T
336
+ affinities.columns = self.sites
337
+
338
+ occupancy = affinities.copy().reindex(composition.index)
339
+
340
+ unknown_site_ions = occupancy.loc[
341
+ occupancy.count(axis=1) == 0, :
342
+ ].index.values
343
+
344
+ if len(unknown_site_ions):
345
+ logging.warn("Unknown site for: {}".format(unknown_site_ions))
346
+
347
+ occupancy.loc[:, :] = 0.0
348
+
349
+ for site in self.sites:
350
+ site.occupancy = pd.Series(index=occupancy.index).fillna(0)
351
+
352
+ inventory = composition.copy()
353
+ for site in self.sites[::-1]:
354
+ accepts = [
355
+ i
356
+ for i in sorted(site.affinities, key=site.affinities.__getitem__)
357
+ if i in inventory.index
358
+ ]
359
+ capacity = np.float(self.template.structure[site])
360
+ site_balances = [b for b in balances if all([i in accepts for i in b])]
361
+ direct_assign = [
362
+ i for i in accepts if not any([i in b for b in site_balances])
363
+ ]
364
+
365
+ for ion in direct_assign:
366
+ current = site.occupancy.sum()
367
+ if not np.isclose(current, capacity + error):
368
+ assigning = np.nanmin([capacity - current, inventory[ion]])
369
+ if not assigning + current - (capacity + error) > 0.0:
370
+ logger.debug(
371
+ "Assigning {:.3f} {} to Site {}".format(
372
+ assigning, ion, site
373
+ )
374
+ )
375
+ occupancy.loc[ion, site] += assigning
376
+ site.occupancy[ion] += occupancy.loc[ion, site]
377
+ inventory[ion] -= assigning
378
+ else:
379
+ logger.warn(
380
+ "{} capacity encountered: {} / {}".format(
381
+ site, assigning + current, capacity
382
+ )
383
+ )
384
+
385
+ for group in site_balances:
386
+ current = site.occupancy.sum()
387
+ invent = inventory.loc[group].sum()
388
+ fractions = inventory.loc[group] / inventory.loc[group].sum()
389
+ if not np.isclose(current, capacity + error):
390
+ assigning = np.nanmin([capacity - current, invent])
391
+ if not assigning + current - (capacity + error) > 0.0:
392
+ logger.debug(
393
+ "Assigning {:.3f} {} to Site {}".format(
394
+ assigning, ion, site
395
+ )
396
+ )
397
+ assigning *= fractions
398
+ occupancy.loc[group, site] += assigning
399
+ site.occupancy[group] += occupancy.loc[group, site]
400
+ inventory.loc[group] -= assigning
401
+ else:
402
+ logger.warn(
403
+ "{} capacity encountered: {} / {}".format(
404
+ site, assigning + current, capacity
405
+ )
406
+ )
407
+
408
+ # check sums across all sites equal the full composition
409
+ self.template.site_occupancy = occupancy
410
+ return occupancy
411
+ else:
412
+ logger.warn('Template not yet set. Cannot calculate occupancy.')
413
+
414
+ def get_site_occupancy(self):
415
+ """
416
+ Get the site occupancy for the mineral.
417
+ """
418
+ self.calculate_occupancy()
419
+ return self.template.site_occupancy
420
+
421
+ def __str__(self):
422
+ D = {}
423
+ for kwarg in ["name", "template"]:
424
+ val = getattr(self, kwarg, None)
425
+ if val is not None:
426
+ D[kwarg] = val
427
+ callstrings = []
428
+ for k, v in D.items():
429
+ callstrings.append("""{}""".format(v.__str__()))
430
+
431
+ strstring = r"""{}: """.format(self.__class__.__name__) + ", ".join(callstrings)
432
+ return strstring
433
+
434
+ def __repr__(self):
435
+ D = {}
436
+ for kwarg in ["name", "template", "endmembers"]:
437
+ val = getattr(self, kwarg, None)
438
+ if val is not None:
439
+ D[kwarg] = val
440
+
441
+ callstrings = []
442
+ for k, v in D.items():
443
+ callstrings.append("""{}={},""".format(k, v.__repr__()))
444
+
445
+ reprstring = (
446
+ r"""{}(""".format(self.__class__.__name__) + "".join(callstrings) + r""")"""
447
+ )
448
+ return reprstring
449
+
450
+ def __hash__(self):
451
+ return hash(self.__repr__().encode("UTF-8"))
452
+
453
+
454
+ def formula_to_elemental(formula, weight=True):
455
+ """Convert a periodictable.formulas.Formula to elemental composition."""
456
+ formula = pt.formula(formula)
457
+ fmass = formula.mass
458
+ composition = {}
459
+ if weight:
460
+ for a, c in formula.atoms.items():
461
+ composition[str(a)] = (c * a.mass) / fmass
462
+ else:
463
+ atoms = sum([c for a, c in formula.atoms.items()])
464
+ for a, c in formula.atoms.items():
465
+ composition[str(a)] = c / atoms
466
+ return composition
467
+
468
+
469
+ def merge_formulae(formulas):
470
+ """
471
+ Combine multiple formulae into one. Particularly useful for defining oxide mineral
472
+ formulae.
473
+
474
+ Parameters
475
+ -----------
476
+ formulas: iterable
477
+ Iterable of multiple formulae to merge into a single larger molecular formulae.
478
+ """
479
+ molecule = pt.formula("")
480
+ for f in formulas:
481
+ molecule += pt.formula(f)
482
+ return molecule
483
+
484
+
485
+ def parse_composition(composition):
486
+ """
487
+ Parse a composition to provide an ionic elemental version in the form of a
488
+ pandas.Series. Currently accepts pandas.Series, periodictable.formulas.Formula
489
+ and structures which will directly convert to pandas.Series (list of tuples, dict).
490
+
491
+ Parameters
492
+ -----------
493
+ composition : {pandas.Series, periodictable.formulas.Formula}
494
+ Formulation of composition to parse.
495
+ """
496
+ if composition is not None:
497
+ if isinstance(composition, pd.Series):
498
+ # convert to molecular oxides, then to formula, then to wt% elemental
499
+ components = [pt.formula(c) for c in composition.index]
500
+ values = composition.values
501
+ formula = merge_formulae(
502
+ [v / c.mass * c for v, c in zip(values, components)]
503
+ )
504
+ return pd.Series(formula_to_elemental(formula))
505
+ elif isinstance(composition, pt.formulas.Formula):
506
+ return pd.Series(formula_to_elemental(composition))
507
+ else:
508
+ return parse_composition(pd.Series(composition))
509
+
510
+
511
+ @pf.register_series_method
512
+ @pf.register_dataframe_method
513
+ def recalc_cations(
514
+ df, ideal_cations=4, ideal_oxygens=6, Fe_species=["FeO", "Fe", "Fe2O3"],
515
+ oxygen_constrained=False,
516
+ ):
517
+ """
518
+ Recalculate a composition to a.p.f.u.
519
+ """
520
+ assert ideal_cations is not None or ideal_oxygens is not None
521
+ # if Fe2O3 and FeO are specified, calculate based on oxygen
522
+ moles = to_frame(df)
523
+ moles = moles.div([pt.formula(c).mass for c in moles.columns])
524
+ moles = moles.where(~np.isclose(moles, 0.0), np.nan)
525
+
526
+ # determine whether oxygen is an open or closed system
527
+ count_iron_species = np.array([i in moles.columns for i in Fe_species]).sum()
528
+ oxygen_constrained = oxygen_constrained
529
+ if not oxygen_constrained:
530
+ if count_iron_species > 1: # check that only one is defined
531
+ oxygen_constrained = (
532
+ count_iron_species - pd.isnull(moles.loc[:, Fe_species]).all(axis=1).sum()
533
+ ) > 1
534
+
535
+ if oxygen_constrained:
536
+ logger.info("Multiple iron species defined. Calculating using oxygen.")
537
+ else:
538
+ logger.info("Single iron species defined. Calculating using cations.")
539
+
540
+ components = moles.columns
541
+ as_oxides = len(list(pt.formula(components[0]).atoms)) > 1
542
+ schema = []
543
+ # if oxygen_constrained: # need to specifically separate Fe2 and Fe3
544
+ if as_oxides:
545
+ parts = [pt.formula(c).atoms for c in components]
546
+ for p in parts:
547
+ oxygens = p[pt.O]
548
+ other_components = [i for i in list(p) if not i == pt.O]
549
+ assert len(other_components) == 1 # need to be simple oxides
550
+ other = other_components[0]
551
+ charge = oxygens * 2 / p[other]
552
+ ion = other.ion[charge]
553
+ schema.append({str(ion): p[other], "O": oxygens})
554
+ else:
555
+ # elemental composition
556
+ parts = components
557
+ for part in parts:
558
+ p = list(pt.formula(part).atoms)[0]
559
+ if p.charge != 0:
560
+ charge = p.charge
561
+ else:
562
+ charge = p.default_charge
563
+ schema.append({p.ion[charge]: 1})
564
+
565
+ ref = pd.DataFrame(data=schema)
566
+ ref.columns = ref.columns.map(str)
567
+ ref.index = components
568
+ cation_masses = {c: pt.formula(c).mass for c in ref.columns}
569
+ oxygen_index = [i for i in ref.columns if "O" in i][0]
570
+ ref = ref.loc[:, [i for i in ref.columns if not i == oxygen_index] + [oxygen_index]]
571
+ moles_ref = ref.copy()
572
+
573
+ moles_ref.loc[:, :] = ref.values * moles.T.values
574
+
575
+ moles_O = moles_ref[oxygen_index].sum()
576
+ moles_cations = (
577
+ moles_ref.loc[:, [i for i in moles_ref.columns if not i == oxygen_index]]
578
+ .sum()
579
+ .sum()
580
+ )
581
+ if not oxygen_constrained: # oxygen unquantified, try to calculate using cations
582
+ scale = ideal_cations / moles_cations
583
+ else: # oxygen quantified, try to calculate using oxygen
584
+ scale = ideal_oxygens / moles_O
585
+
586
+ moles_ref *= scale
587
+ return moles_ref.sum(axis=0)