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.
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__init__.py +10 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/__init__.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/_version.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/alteration.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/classification.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/compositions.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/geochem.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/melts.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/norm.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/normalisation.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/__pycache__/plot.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/_version.py +21 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/alteration.py +66 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/classification.py +222 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__init__.py +9 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/__init__.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/aggregate.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/codata.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/impute.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/__pycache__/renorm.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/aggregate.py +391 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/codata.py +266 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/impute.py +82 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/comp/renorm.py +40 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/compositions.py +524 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_CFB_Dataset_List.csv +42 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_Convergent_Dataset_List.csv +42 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OBFB_Dataset_List.csv +5 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OIB_Dataset_List.csv +49 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/GEOROC_OceanicPlateau_Dataset_List.csv +18 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/georoc/contents.json +1 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/__pycache__/env.cpython-35.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/__pycache__/env.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/melts/env.py +1063 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Ba.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Bs.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.F.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O1.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O2.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.O3.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Pc.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.Ph.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.R.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S1.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S2.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.S3.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.T1.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.T2.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U1.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U2.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.U3.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.modelfields +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.nan.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS/TAS.none.modelfield +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/TAS.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/aphanitic.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/gabbroic.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/peralkalinity.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/phaneritic.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/models/ultramafic.clsf.gz +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/CH_PalmeONeill2014.csv +95 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DDMM_WorkmanHart2005.csv +105 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DMM_WorkmanHart2005.csv +105 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/DM_SaltersStrake2004.csv +95 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/EDMM_WorkmanHart2005.csv +105 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/refcomp/PM_PalmeONeill2014.csv +95 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/data/timescale/geotimescale_spans.csv +180 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/geochem.py +821 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/melts.py +92 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__init__.py +10 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/__init__.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/db.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/ions.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/mineral.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/__pycache__/sites.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/db.py +88 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/ions.py +78 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/mineral.py +587 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/mineral/sites.py +134 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/norm.py +224 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/normalisation.py +204 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/plot.py +514 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__init__.py +13 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/__init__.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/database.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/env.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/general.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/georoc.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/math.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/melts.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/multip.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/multiprocessing.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/pd.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/plot.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/skl.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/spatial.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/text.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/time.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/__pycache__/wfs.cpython-36.pyc +0 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/database.py +88 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/env.py +81 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/general.py +266 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/georoc.py +444 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/math.py +371 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/melts.py +397 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/multip.py +29 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/multiprocessing.py +29 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/pd.py +214 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/plot.py +345 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/skl.py +847 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/spatial.py +91 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/text.py +207 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/time.py +224 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite/util/wfs.py +10 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/PKG-INFO +61 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/SOURCES.txt +83 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/dependency_links.txt +1 -0
- ProgramData/Anaconda3_64/Lib/site-packages/pyrolite-0.0.14-py3.6.egg-info/requires.txt +47 -0
- 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)
|