pyIntersecT 0.1.4__py3-none-any.whl
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.
- pyIntersecT/IntersecT.py +1023 -0
- pyIntersecT/Logo-IntersecT.png +0 -0
- pyIntersecT/__init__.py +0 -0
- pyIntersecT/mineral_aliases.py +447 -0
- pyIntersecT/mineral_symbols.toml +700 -0
- pyIntersecT/parsers.py +341 -0
- pyintersect-0.1.4.dist-info/METADATA +918 -0
- pyintersect-0.1.4.dist-info/RECORD +10 -0
- pyintersect-0.1.4.dist-info/WHEEL +4 -0
- pyintersect-0.1.4.dist-info/licenses/LICENSE +674 -0
|
Binary file
|
pyIntersecT/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mineral_aliases.py
|
|
3
|
+
|
|
4
|
+
Robust loader / resolver for mineral name aliases + solvus discrimination.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# TOML: prefer tomllib (Py>=3.11), fallback to tomli for older Pythons
|
|
8
|
+
try:
|
|
9
|
+
import tomllib # Python 3.11+
|
|
10
|
+
except Exception:
|
|
11
|
+
import tomli as tomllib # type: ignore
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import logging
|
|
15
|
+
import numpy as np
|
|
16
|
+
from typing import Dict, Any
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
logging.getLogger("pyIntersecT.mineral_aliases").setLevel(logging.INFO)
|
|
20
|
+
|
|
21
|
+
# Cache for normalized endmember arrays:
|
|
22
|
+
_NORMALIZED_ENDMEMBERS_ARRAY: Dict[Any, Any] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MineralAliases:
|
|
26
|
+
"""
|
|
27
|
+
Load mineral aliases from a TOML and resolve phase names case-insensitively.
|
|
28
|
+
|
|
29
|
+
toml_path: path to mineral_symbols.toml which should contain sections
|
|
30
|
+
like ["MAGEMin to Warr2021"] and ["PerpleX to Warr2021"] mapping names.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, toml_path, strict: bool = True, persist_unknowns: bool = False):
|
|
34
|
+
self.strict = strict
|
|
35
|
+
self.persist_unknowns = persist_unknowns
|
|
36
|
+
|
|
37
|
+
# store mappings with normalized (lower().strip()) keys
|
|
38
|
+
self.magemin: Dict[str, str] = {}
|
|
39
|
+
self.perplex: Dict[str, str] = {}
|
|
40
|
+
|
|
41
|
+
self._load(toml_path)
|
|
42
|
+
|
|
43
|
+
def _load(self, toml_path):
|
|
44
|
+
toml_path = Path(toml_path)
|
|
45
|
+
if not toml_path.exists():
|
|
46
|
+
raise FileNotFoundError(f"mineral_symbols TOML not found: {toml_path}")
|
|
47
|
+
|
|
48
|
+
with open(toml_path, "rb") as f:
|
|
49
|
+
data = tomllib.load(f)
|
|
50
|
+
|
|
51
|
+
for section_name, mapping in data.items():
|
|
52
|
+
section_lower = section_name.lower()
|
|
53
|
+
if "magemin" in section_lower:
|
|
54
|
+
for k, v in mapping.items():
|
|
55
|
+
if isinstance(k, str):
|
|
56
|
+
self.magemin[k.strip().lower()] = v
|
|
57
|
+
elif "perple" in section_lower or "perplex" in section_lower:
|
|
58
|
+
for k, v in mapping.items():
|
|
59
|
+
if isinstance(k, str):
|
|
60
|
+
self.perplex[k.strip().lower()] = v
|
|
61
|
+
|
|
62
|
+
logger.debug("Loaded %d magemin aliases, %d perplex aliases",
|
|
63
|
+
len(self.magemin), len(self.perplex))
|
|
64
|
+
|
|
65
|
+
def resolve(self, phase_name: str, system: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Resolve phase_name to canonical name using the chosen system ('magemin'|'perplex').
|
|
68
|
+
Case-insensitive. In non-strict mode unknowns are returned as-is (and optionally persisted).
|
|
69
|
+
"""
|
|
70
|
+
if not isinstance(phase_name, str):
|
|
71
|
+
raise TypeError("phase_name must be a string")
|
|
72
|
+
|
|
73
|
+
key = phase_name.strip().lower()
|
|
74
|
+
system = system.strip().lower()
|
|
75
|
+
|
|
76
|
+
if system == "magemin":
|
|
77
|
+
table = self.magemin
|
|
78
|
+
elif system == "perplex" or system == "perple_x":
|
|
79
|
+
table = self.perplex
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f"Unknown system: {system!r}")
|
|
82
|
+
|
|
83
|
+
if key in table:
|
|
84
|
+
return table[key]
|
|
85
|
+
|
|
86
|
+
if self.strict:
|
|
87
|
+
raise KeyError(f"Phase '{phase_name}' not found in {system} dictionary")
|
|
88
|
+
|
|
89
|
+
# non-strict: return original (or persist a mapping to itself)
|
|
90
|
+
if self.persist_unknowns:
|
|
91
|
+
table[key] = phase_name
|
|
92
|
+
return phase_name
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# -------------------------
|
|
96
|
+
# Endmember compositions and solvus discrimination
|
|
97
|
+
# -------------------------
|
|
98
|
+
ENDMEMBER_COMPOSITIONS = {
|
|
99
|
+
"Mca": {
|
|
100
|
+
"Pg": {"Na": 1, "K": 0, "Al": 3, "Si": 3}, # Paragonite (Na-rich)
|
|
101
|
+
"Ms": {"Na": 0, "K": 1, "Al": 3, "Si": 3}, # Muscovite (K-rich)
|
|
102
|
+
"Mrg": {"Ca": 1, "Al": 4, "Si": 2}, # Margarite (Ca-rich)
|
|
103
|
+
},
|
|
104
|
+
"Fsp": {
|
|
105
|
+
"Ab": {"Na": 1, "Al": 1, "Si": 3}, # Albite
|
|
106
|
+
"An": {"Ca": 1, "Al": 2, "Si": 2}, # Anorthite
|
|
107
|
+
"Or": {"K": 1, "Al": 1, "Si": 3}, # Orthoclase
|
|
108
|
+
},
|
|
109
|
+
"Cpx": {
|
|
110
|
+
"Di": {"Ca": 1, "Mg": 1, "Si": 2}, # Diopside (high-Ca, Mg-rich)
|
|
111
|
+
"Hd": {"Ca": 1, "Fe": 1, "Si": 2}, # Hedenbergite (high-Ca, Fe-rich)
|
|
112
|
+
"Aug": {"Ca": 0.7, "Mg": 0.7, "Fe": 0.3, "Al": 0.3, "Si": 2}, # Augite (intermediate-Ca)
|
|
113
|
+
"Pig": {"Ca": 0.1, "Mg": 0.9, "Fe": 0.1, "Si": 2}, # Pigeonite (low-Ca, Mg-rich)
|
|
114
|
+
"Jd": {"Na": 1, "Al": 1, "Si": 2}, # Jadeite (Na-Al endmember)
|
|
115
|
+
"Aeg": {"Na": 1, "Fe": 1, "Si": 2}, # Aegirine (Na-Fe endmember)
|
|
116
|
+
},
|
|
117
|
+
"Amp": {
|
|
118
|
+
"Tr": {"Ca": 2, "Mg": 5, "Si": 8}, # Tremolite (calcic, Mg-rich)
|
|
119
|
+
"Act": {"Ca": 2, "Mg": 3, "Fe": 2, "Si": 8}, # Actinolite (calcic, intermediate Mg-Fe)
|
|
120
|
+
"Gln": {"Na": 2, "Mg": 3, "Al": 2, "Si": 8}, # Glaucophane (sodic, Al-bearing)
|
|
121
|
+
"Cum": {"Mg": 5, "Fe": 2, "Si": 8}, # Cummingtonite (Ca-poor, Mg-Fe amphibole)
|
|
122
|
+
},
|
|
123
|
+
"Spl": {
|
|
124
|
+
"Chr": {"Fe": 1, "Cr": 2}, # Chromite (Cr-rich spinel)
|
|
125
|
+
"Usp": {"Fe": 2, "Ti": 1}, # Ulvöspinel-like (Ti-bearing spinel)
|
|
126
|
+
"Mag": {"Fe": 3}, # Magnetite-like (Fe-dominant)
|
|
127
|
+
},
|
|
128
|
+
"Ilm": {
|
|
129
|
+
"Ilm": {"Fe": 1, "Ti": 1}, # Ilmenite (Fe-Ti oxide)
|
|
130
|
+
"Hem": {"Fe": 2}, # Hematite (Fe2O3)
|
|
131
|
+
},
|
|
132
|
+
"Carb": {
|
|
133
|
+
"Cal": {"Ca": 1}, # Calcite (Ca-only carbonate)
|
|
134
|
+
"Dol": {"Ca": 1, "Mg": 1}, # Dolomite (Ca-Mg carbonate)
|
|
135
|
+
"Ank": {"Ca": 1, "Fe": 1}, # Ankerite (Ca-Fe carbonate)
|
|
136
|
+
"Sid": {"Fe": 1}, # Siderite (Fe carbonate)
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ENDMEMBER_DISTANCE_THRESHOLD = {
|
|
141
|
+
k: 0.3 for k in ENDMEMBER_COMPOSITIONS.keys()
|
|
142
|
+
}
|
|
143
|
+
# Stricter threshold for clinopyroxenes due to more endmembers
|
|
144
|
+
ENDMEMBER_DISTANCE_THRESHOLD["Cpx"] = 0.35
|
|
145
|
+
|
|
146
|
+
def _get_element_value(phase: dict, element: str) -> float:
|
|
147
|
+
"""Get element value from phase dict, trying multiple key patterns."""
|
|
148
|
+
|
|
149
|
+
# Endmember classification uses total iron (Fe2+ + Fe3+) when available
|
|
150
|
+
if element == "Fe" and "Fetot_apfu" in phase:
|
|
151
|
+
return float(phase["Fetot_apfu"])
|
|
152
|
+
|
|
153
|
+
# Try simple element name first
|
|
154
|
+
if element in phase:
|
|
155
|
+
val = phase[element]
|
|
156
|
+
if val is not None:
|
|
157
|
+
try:
|
|
158
|
+
return float(val)
|
|
159
|
+
except (ValueError, TypeError):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Try with _apfu suffix
|
|
163
|
+
key_apfu = f"{element}_apfu"
|
|
164
|
+
if key_apfu in phase:
|
|
165
|
+
val = phase[key_apfu]
|
|
166
|
+
if val is not None:
|
|
167
|
+
try:
|
|
168
|
+
return float(val)
|
|
169
|
+
except (ValueError, TypeError):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return 0.0
|
|
173
|
+
|
|
174
|
+
def discriminate_solvus(phases, system: str = "perplex"):
|
|
175
|
+
"""
|
|
176
|
+
Group phases by parent phase name and apply endmember classification.
|
|
177
|
+
For Perple_X output, applies compositional discrimination to identify
|
|
178
|
+
specific endmember compositions within solid solution series.
|
|
179
|
+
For MAGEMin output, solvus discrimination is skipped as MAGEMin already
|
|
180
|
+
handles phase separation internally.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (processed_phases, transformation_stats)
|
|
184
|
+
where transformation_stats is dict: {(parent, assigned): count}
|
|
185
|
+
"""
|
|
186
|
+
if system.lower() == "magemin":
|
|
187
|
+
transformations = {}
|
|
188
|
+
for p in phases:
|
|
189
|
+
name = p.get("phase", "")
|
|
190
|
+
key = (name, name)
|
|
191
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
192
|
+
return phases, transformations
|
|
193
|
+
|
|
194
|
+
grouped = {}
|
|
195
|
+
for p in phases:
|
|
196
|
+
name = p.get("phase", "")
|
|
197
|
+
grouped.setdefault(name, []).append(p)
|
|
198
|
+
|
|
199
|
+
output = []
|
|
200
|
+
transformations = {} # Track (original_name, assigned_name) -> count
|
|
201
|
+
|
|
202
|
+
for parent_name, group in grouped.items():
|
|
203
|
+
parent_key = None
|
|
204
|
+
for k in ENDMEMBER_COMPOSITIONS:
|
|
205
|
+
if k.lower() == parent_name.lower():
|
|
206
|
+
parent_key = k
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
if parent_key is None:
|
|
210
|
+
logger.debug("No endmember rules for phase '%s', keeping as-is", parent_name)
|
|
211
|
+
output.extend(group)
|
|
212
|
+
# Count unchanged phases
|
|
213
|
+
for phase in group:
|
|
214
|
+
key = (parent_name, parent_name)
|
|
215
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
logger.debug("Applying endmember classification to %d phase(s) of '%s'", len(group), parent_name)
|
|
219
|
+
|
|
220
|
+
if parent_key == "Cpx":
|
|
221
|
+
logger.debug(" Using omphacite heuristic for Cpx phases")
|
|
222
|
+
discriminated = _discriminate_cpx(group)
|
|
223
|
+
output.extend(discriminated)
|
|
224
|
+
# Count transformations
|
|
225
|
+
for phase in discriminated:
|
|
226
|
+
assigned = phase.get("phase")
|
|
227
|
+
key = (parent_name, assigned)
|
|
228
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# Special handling for feldspars
|
|
232
|
+
if parent_key == "Fsp":
|
|
233
|
+
logger.debug(" Using Ca-alkali criteria for Fsp discrimination")
|
|
234
|
+
for phase in group:
|
|
235
|
+
phase["orig_phase"] = phase.get("phase")
|
|
236
|
+
|
|
237
|
+
na = _get_element_value(phase, "Na")
|
|
238
|
+
ca = _get_element_value(phase, "Ca")
|
|
239
|
+
k = _get_element_value(phase, "K")
|
|
240
|
+
|
|
241
|
+
total_alkali = na + ca + k
|
|
242
|
+
if total_alkali <= 0:
|
|
243
|
+
assigned_name = "Fsp"
|
|
244
|
+
elif ca / total_alkali > 0.10:
|
|
245
|
+
assigned_name = "Pl"
|
|
246
|
+
elif na > k:
|
|
247
|
+
assigned_name = "Pl"
|
|
248
|
+
else:
|
|
249
|
+
assigned_name = "Afs"
|
|
250
|
+
|
|
251
|
+
phase["phase"] = assigned_name
|
|
252
|
+
key = (parent_name, assigned_name)
|
|
253
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
254
|
+
output.append(phase)
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Standard endmember distance-based discrimination for ALL phases
|
|
258
|
+
threshold = ENDMEMBER_DISTANCE_THRESHOLD.get(parent_key, 0.3)
|
|
259
|
+
for idx, phase in enumerate(group):
|
|
260
|
+
phase["orig_phase"] = phase.get("phase")
|
|
261
|
+
best_em, best_dist = _find_nearest_endmember(phase, parent_key)
|
|
262
|
+
|
|
263
|
+
logger.debug(" Phase %d/%d: nearest endmember='%s', distance=%.4f (threshold=%.2f)",
|
|
264
|
+
idx+1, len(group), best_em, best_dist, threshold)
|
|
265
|
+
|
|
266
|
+
if best_dist <= threshold:
|
|
267
|
+
old_name = phase["phase"]
|
|
268
|
+
phase["phase"] = best_em
|
|
269
|
+
logger.debug(" Assigned '%s' -> '%s' (distance %.4f <= threshold %.2f)",
|
|
270
|
+
old_name, best_em, best_dist, threshold)
|
|
271
|
+
# Count transformation
|
|
272
|
+
key = (parent_name, best_em)
|
|
273
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
274
|
+
else:
|
|
275
|
+
logger.debug(" Keeping original name '%s' (distance %.4f > threshold %.2f)",
|
|
276
|
+
phase["phase"], best_dist, threshold)
|
|
277
|
+
# Count unchanged
|
|
278
|
+
key = (parent_name, parent_name)
|
|
279
|
+
transformations[key] = transformations.get(key, 0) + 1
|
|
280
|
+
output.append(phase)
|
|
281
|
+
|
|
282
|
+
return output, transformations
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _discriminate_cpx(cpx_group):
|
|
286
|
+
"""
|
|
287
|
+
Discriminate clinopyroxenes using compositional heuristics.
|
|
288
|
+
|
|
289
|
+
Classifies clinopyroxenes based on the standard nomenclature following
|
|
290
|
+
Morimoto et al. (1988) pyroxene classification scheme. Recognizes six
|
|
291
|
+
endmember types plus omphacite as an intermediate composition and a generic Cpx category for compositions that do not fit well:
|
|
292
|
+
|
|
293
|
+
- Jd (jadeite): Na-Al pyroxene, very high jadeite component (>0.8), very low Ca (<0.15)
|
|
294
|
+
- Omp (omphacite): Intermediate Di-Jd solid solution, significant jadeite component (>0.25)
|
|
295
|
+
- Acm (aegirine): Na-Fe pyroxene, high Na, low Ca, Fe-dominant
|
|
296
|
+
- Di (diopside): High-Ca Mg-rich pyroxene (Ca>0.5, Mg#>0.6)
|
|
297
|
+
- Hd (hedenbergite): High-Ca Fe-rich pyroxene (Ca>0.5, Mg#<0.4)
|
|
298
|
+
- Aug (augite): Intermediate-Ca pyroxene (0.2<Ca<0.5)
|
|
299
|
+
- Pig (pigeonite): Low-Ca pyroxene (0.05<Ca<0.2)
|
|
300
|
+
- Cpx (generic): Compositions not matching specific endmember criteria
|
|
301
|
+
|
|
302
|
+
Returns list of phases with updated names.
|
|
303
|
+
"""
|
|
304
|
+
output = []
|
|
305
|
+
|
|
306
|
+
for phase in cpx_group:
|
|
307
|
+
phase["orig_phase"] = phase.get("phase")
|
|
308
|
+
|
|
309
|
+
# Get cation abundances
|
|
310
|
+
na = _get_element_value(phase, "Na")
|
|
311
|
+
al = _get_element_value(phase, "Al")
|
|
312
|
+
ca = _get_element_value(phase, "Ca")
|
|
313
|
+
mg = _get_element_value(phase, "Mg")
|
|
314
|
+
fe = _get_element_value(phase, "Fe")
|
|
315
|
+
|
|
316
|
+
logger.debug(" Cpx composition: Ca=%.3f, Mg=%.3f, Fe=%.3f, Na=%.3f, Al=%.3f",
|
|
317
|
+
ca, mg, fe, na, al)
|
|
318
|
+
|
|
319
|
+
# Calculate diagnostic ratios
|
|
320
|
+
total_cations = ca + mg + fe + na + al
|
|
321
|
+
if total_cations <= 0:
|
|
322
|
+
phase["phase"] = "Cpx"
|
|
323
|
+
logger.debug(" No cation data available, keeping as 'Cpx'")
|
|
324
|
+
output.append(phase)
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Normalized fractions
|
|
328
|
+
jd_component = (na + al) / total_cations # Jadeite component
|
|
329
|
+
ca_frac = ca / total_cations # Calcium content
|
|
330
|
+
na_frac = na / total_cations # Sodium content
|
|
331
|
+
mg_number = mg / (mg + fe) if (mg + fe) > 0 else 0.0 # Mg/(Mg+Fe)
|
|
332
|
+
|
|
333
|
+
# Hierarchical classification based on standard pyroxene nomenclature
|
|
334
|
+
|
|
335
|
+
# First branch: Sodic pyroxenes (high Na+Al content)
|
|
336
|
+
|
|
337
|
+
# 1) Jadeite: Pure sodic pyroxene with very high jadeite component and minimal Ca
|
|
338
|
+
# Corresponds to compositions near the Jd apex in the Jd-Ae-Di+Hd diagram
|
|
339
|
+
if jd_component > 0.8 and ca_frac < 0.15:
|
|
340
|
+
assigned_name = "Jd"
|
|
341
|
+
logger.debug(" Assigned 'Jd' (jadeite): jd_component=%.3f, ca_frac=%.3f",
|
|
342
|
+
jd_component, ca_frac)
|
|
343
|
+
|
|
344
|
+
# 2) Omphacite: Jadeite with significant Ca, representing Di-Jd solid solution
|
|
345
|
+
# Occupies the region between pure jadeite and the diopside-hedenbergite join
|
|
346
|
+
# Critical for high-pressure metamorphic assemblages (eclogites)
|
|
347
|
+
elif jd_component > 0.25:
|
|
348
|
+
assigned_name = "Omp"
|
|
349
|
+
logger.debug(" Assigned 'Omp' (omphacite): jd_component=%.3f, ca_frac=%.3f",
|
|
350
|
+
jd_component, ca_frac)
|
|
351
|
+
|
|
352
|
+
# 3) Aegirine: Sodic Fe-rich pyroxene with high Na, low Ca, Fe-dominant
|
|
353
|
+
# Forms solid solution toward aegirine-augite with increasing Ca
|
|
354
|
+
elif na_frac > 0.4 and ca_frac < 0.25 and mg_number < 0.3:
|
|
355
|
+
assigned_name = "Acm"
|
|
356
|
+
logger.debug(" Assigned 'Acm' (aegirine): na_frac=%.3f, ca_frac=%.3f, mg_number=%.3f",
|
|
357
|
+
na_frac, ca_frac, mg_number)
|
|
358
|
+
|
|
359
|
+
# Second branch: Non-sodic pyroxenes (low Na+Al, classified by Ca content)
|
|
360
|
+
|
|
361
|
+
# 4) Diopside: High-Ca, Mg-rich endmember
|
|
362
|
+
# Requires Ca > 0.5 (>50% of M2 site) and high Mg# characteristic of diopside
|
|
363
|
+
elif ca_frac > 0.5 and mg_number > 0.6:
|
|
364
|
+
assigned_name = "Di"
|
|
365
|
+
logger.debug(" Assigned 'Di' (diopside): ca_frac=%.3f, mg_number=%.3f",
|
|
366
|
+
ca_frac, mg_number)
|
|
367
|
+
|
|
368
|
+
# 5) Hedenbergite: High-Ca, Fe-rich endmember
|
|
369
|
+
# Requires Ca > 0.5 and Fe-dominant composition
|
|
370
|
+
elif ca_frac > 0.5 and mg_number < 0.4:
|
|
371
|
+
assigned_name = "Hd"
|
|
372
|
+
logger.debug(" Assigned 'Hd' (hedenbergite): ca_frac=%.3f, mg_number=%.3f",
|
|
373
|
+
ca_frac, mg_number)
|
|
374
|
+
|
|
375
|
+
# 6) Augite: Intermediate-Ca pyroxene
|
|
376
|
+
# Standard definition: 20-45% Ca in M2 site (Wo20-45 in Wo-En-Fs diagram)
|
|
377
|
+
# Characteristic of many igneous and metamorphic rocks
|
|
378
|
+
elif 0.2 <= ca_frac <= 0.5:
|
|
379
|
+
assigned_name = "Aug"
|
|
380
|
+
logger.debug(" Assigned 'Aug' (augite): ca_frac=%.3f, mg_number=%.3f",
|
|
381
|
+
ca_frac, mg_number)
|
|
382
|
+
|
|
383
|
+
# 7) Pigeonite: Low-Ca pyroxene
|
|
384
|
+
# Standard definition: 5-20% Ca in M2 site (Wo5-20)
|
|
385
|
+
# Monoclinic structure distinguishes it from orthopyroxene (Wo < 5)
|
|
386
|
+
# Common in rapidly cooled igneous rocks and some metamorphic assemblages
|
|
387
|
+
elif 0.05 <= ca_frac < 0.2:
|
|
388
|
+
assigned_name = "Pig"
|
|
389
|
+
logger.debug(" Assigned 'Pig' (pigeonite): ca_frac=%.3f, mg_number=%.3f",
|
|
390
|
+
ca_frac, mg_number)
|
|
391
|
+
|
|
392
|
+
# 8) Generic Cpx: Very low Ca (<5%) or compositions not matching criteria
|
|
393
|
+
# Very low Ca compositions (Ca < 0.05) approach orthopyroxene field
|
|
394
|
+
# but retain monoclinic structure, hence generic Cpx designation
|
|
395
|
+
else:
|
|
396
|
+
assigned_name = "Cpx"
|
|
397
|
+
logger.debug(" Kept as 'Cpx' (intermediate/low-Ca composition): jd_comp=%.3f, ca_frac=%.3f, mg_number=%.3f",
|
|
398
|
+
jd_component, ca_frac, mg_number)
|
|
399
|
+
|
|
400
|
+
phase["phase"] = assigned_name
|
|
401
|
+
output.append(phase)
|
|
402
|
+
|
|
403
|
+
return output
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _find_nearest_endmember(phase: dict, parent_name: str):
|
|
407
|
+
endmembers = ENDMEMBER_COMPOSITIONS[parent_name]
|
|
408
|
+
|
|
409
|
+
phase_elems = set()
|
|
410
|
+
for sto in endmembers.values():
|
|
411
|
+
phase_elems |= set(sto.keys())
|
|
412
|
+
elems = tuple(sorted(phase_elems))
|
|
413
|
+
|
|
414
|
+
key = (parent_name, elems)
|
|
415
|
+
cached = _NORMALIZED_ENDMEMBERS_ARRAY.get(key)
|
|
416
|
+
if cached is not None:
|
|
417
|
+
em_names, em_array = cached
|
|
418
|
+
else:
|
|
419
|
+
em_names = list(endmembers.keys())
|
|
420
|
+
em_vecs = []
|
|
421
|
+
for nm in em_names:
|
|
422
|
+
sto = endmembers[nm]
|
|
423
|
+
vec = np.array([sto.get(e, 0.0) for e in elems], dtype=float)
|
|
424
|
+
total = vec.sum()
|
|
425
|
+
if total <= 0:
|
|
426
|
+
total = 1.0
|
|
427
|
+
vec = vec / total
|
|
428
|
+
em_vecs.append(vec)
|
|
429
|
+
em_array = np.vstack(em_vecs)
|
|
430
|
+
_NORMALIZED_ENDMEMBERS_ARRAY[key] = (em_names, em_array)
|
|
431
|
+
|
|
432
|
+
p_vec = np.array([_get_element_value(phase, e) for e in elems], dtype=float)
|
|
433
|
+
p_total = p_vec.sum()
|
|
434
|
+
if p_total <= 0:
|
|
435
|
+
p_total = 1.0
|
|
436
|
+
p_vec = p_vec / p_total
|
|
437
|
+
|
|
438
|
+
diffs = em_array - p_vec
|
|
439
|
+
dists = np.linalg.norm(diffs, axis=1)
|
|
440
|
+
idx = int(dists.argmin())
|
|
441
|
+
|
|
442
|
+
phase_comp_str = ", ".join([f"{e}={_get_element_value(phase, e):.3f}" for e in elems if _get_element_value(phase, e) > 0.001])
|
|
443
|
+
logger.debug(" Phase composition (raw): %s", phase_comp_str)
|
|
444
|
+
logger.debug(" Distances to endmembers: %s",
|
|
445
|
+
", ".join([f"{em_names[i]}={dists[i]:.4f}" for i in range(len(em_names))]))
|
|
446
|
+
|
|
447
|
+
return em_names[idx], float(dists[idx])
|