PetThermoTools 0.2.41__py3-none-any.whl → 0.2.42__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.
- PetThermoTools/Barom.py +12 -0
- PetThermoTools/GenFuncs.py +63 -24
- PetThermoTools/Liq.py +46 -4
- PetThermoTools/MELTS.py +3 -1
- PetThermoTools/Melting.py +94 -52
- PetThermoTools/Path.py +2 -0
- PetThermoTools/PhaseDiagrams.py +11 -0
- PetThermoTools/Plotting.py +395 -164
- PetThermoTools/Saturation.py +5 -0
- PetThermoTools/_version.py +1 -1
- {PetThermoTools-0.2.41.dist-info → PetThermoTools-0.2.42.dist-info}/METADATA +1 -1
- PetThermoTools-0.2.42.dist-info/RECORD +20 -0
- PetThermoTools-0.2.41.dist-info/RECORD +0 -20
- {PetThermoTools-0.2.41.dist-info → PetThermoTools-0.2.42.dist-info}/LICENSE.txt +0 -0
- {PetThermoTools-0.2.41.dist-info → PetThermoTools-0.2.42.dist-info}/WHEEL +0 -0
- {PetThermoTools-0.2.41.dist-info → PetThermoTools-0.2.42.dist-info}/top_level.txt +0 -0
PetThermoTools/Barom.py
CHANGED
@@ -199,6 +199,18 @@ def mineral_cosaturation(Model="MELTSv1.0.2", cores=int(np.floor(multiprocessing
|
|
199
199
|
fO2_offset = to_float(fO2_offset)
|
200
200
|
|
201
201
|
comp = bulk.copy()
|
202
|
+
|
203
|
+
if fO2_buffer is not None:
|
204
|
+
if fO2_buffer != "NNO":
|
205
|
+
if fO2_buffer != "FMQ":
|
206
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
207
|
+
|
208
|
+
if "MELTS" not in Model:
|
209
|
+
if fO2_buffer == "FMQ":
|
210
|
+
fO2_buffer = "qfm"
|
211
|
+
if fO2_buffer == "NNO":
|
212
|
+
fO2_buffer = "nno"
|
213
|
+
|
202
214
|
if H2O_Sat:
|
203
215
|
comp['H2O_Liq'] = 20
|
204
216
|
|
PetThermoTools/GenFuncs.py
CHANGED
@@ -4,52 +4,53 @@ import pandas as pd
|
|
4
4
|
# from PetThermoTools.Liq import *
|
5
5
|
# from PetThermoTools.Crystallise import *
|
6
6
|
from PetThermoTools.MELTS import *
|
7
|
+
from PetThermoTools.Compositions import *
|
7
8
|
# try:
|
8
9
|
# from PetThermoTools.Holland import *
|
9
10
|
# except:
|
10
11
|
# pass
|
11
12
|
|
12
13
|
Names = {'liquid1': '_Liq',
|
13
|
-
'liquid2': '_Liq2',
|
14
|
-
'liquid3': '_Liq3',
|
15
|
-
'liquid4': '_Liq4',
|
16
14
|
'olivine1': '_Ol',
|
17
|
-
'
|
15
|
+
'orthopyroxene1': '_Opx',
|
18
16
|
'clinopyroxene1': '_Cpx',
|
19
|
-
'
|
17
|
+
'garnet1': '_Grt',
|
18
|
+
'spinel1': '_Sp',
|
19
|
+
'k-feldspar1': '_Kspar',
|
20
|
+
'quartz1': '_Qtz',
|
21
|
+
'rhm-oxide1': '_Rhm',
|
22
|
+
'apatite1': '_Apa',
|
23
|
+
'olivine2': '_Ol2',
|
20
24
|
'plagioclase1': '_Plag',
|
25
|
+
'clinopyroxene2': '_Cpx2',
|
21
26
|
'plagioclase2': '_Plag2',
|
22
|
-
'spinel1': '_Sp',
|
23
27
|
'spinel2': '_Sp2',
|
24
|
-
'k-feldspar1': '_Kspar',
|
25
28
|
'k-feldspar2': '_Kspar2',
|
26
|
-
'garnet1': '_Grt',
|
27
29
|
'garnet2': '_Grt2',
|
28
|
-
'rhm-oxide1': '_Rhm',
|
29
30
|
'rhm-oxide2': '_Rhm2',
|
30
|
-
'quartz1': '_Qtz',
|
31
31
|
'quartz2': '_Qtz2',
|
32
|
-
'orthopyroxene1': '_Opx',
|
33
32
|
'orthopyroxene2': '_Opx2',
|
34
|
-
'
|
35
|
-
'
|
33
|
+
'apatite2': '_Apa2',
|
34
|
+
'liquid2': '_Liq2',
|
35
|
+
'liquid3': '_Liq3',
|
36
|
+
'liquid4': '_Liq4'}
|
36
37
|
|
37
38
|
Names_MM = {'liq1': '_Liq',
|
38
|
-
'liq2': '_Liq2',
|
39
|
-
'liq3': '_Liq3',
|
40
|
-
'liq4': '_Liq4',
|
41
39
|
'ol1': '_Ol',
|
42
|
-
'
|
40
|
+
'opx1': '_Opx',
|
43
41
|
'cpx1': '_Cpx',
|
42
|
+
'g1': '_Grt',
|
43
|
+
'spl1': '_Sp',
|
44
|
+
'fsp1': '_Plag',
|
45
|
+
'ol2': '_Ol2',
|
44
46
|
'cpx2': '_Cpx2',
|
45
|
-
'opx1': '_Opx',
|
46
47
|
'opx2': '_Opx2',
|
47
|
-
'g1': '_Grt',
|
48
48
|
'g2': '_Grt2',
|
49
|
-
'fsp1': '_Plag',
|
50
49
|
'fsp2': '_Plag2',
|
51
|
-
'
|
52
|
-
'
|
50
|
+
'spl2': '_Sp2',
|
51
|
+
'liq2': '_Liq2',
|
52
|
+
'liq3': '_Liq3',
|
53
|
+
'liq4': '_Liq4'}
|
53
54
|
|
54
55
|
def to_float(x):
|
55
56
|
if x is None:
|
@@ -104,6 +105,36 @@ def supCalc(Model = "MELTSv1.0.2", bulk = None, phase = None, T_C = None, P_bar
|
|
104
105
|
|
105
106
|
return Results
|
106
107
|
|
108
|
+
def comp_check(comp_lith, Model, MELTS_filter, Fe3Fet):
|
109
|
+
if type(comp_lith) == str:
|
110
|
+
if Model != "pyMelt":
|
111
|
+
comp = Compositions[comp_lith]
|
112
|
+
else:
|
113
|
+
comp = comp_lith
|
114
|
+
else:
|
115
|
+
comp = comp_lith.copy()
|
116
|
+
|
117
|
+
# if comp is entered as a pandas series, it must first be converted to a dict
|
118
|
+
if Model != "pyMelt":
|
119
|
+
if type(comp) == pd.core.series.Series:
|
120
|
+
comp = comp.to_dict()
|
121
|
+
|
122
|
+
comp = comp_fix(Model = Model, comp = comp, Fe3Fet_Liq = Fe3Fet)
|
123
|
+
|
124
|
+
if "MELTS" in Model and MELTS_filter == True:
|
125
|
+
if type(comp) == pd.core.frame.DataFrame:
|
126
|
+
comp['K2O_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
127
|
+
comp['P2O5_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
128
|
+
comp['H2O_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
129
|
+
comp['CO2_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
130
|
+
else:
|
131
|
+
comp['K2O_Liq'] = 0
|
132
|
+
comp['P2O5_Liq'] = 0
|
133
|
+
comp['H2O_Liq'] = 0
|
134
|
+
comp['CO2_Liq'] = 0
|
135
|
+
|
136
|
+
return comp
|
137
|
+
|
107
138
|
|
108
139
|
def comp_fix(Model = None, comp = None, Fe3Fet_Liq = None, H2O_Liq = None, CO2_Liq = None):
|
109
140
|
'''
|
@@ -356,12 +387,20 @@ def stich_work(Results = None, Order = None, Model = "MELTS", Frac_fluid = None,
|
|
356
387
|
Results_Mass['fluid1_cumsum'] = Results_Mass['fluid1'].cumsum()
|
357
388
|
elif Frac_fluid is None:
|
358
389
|
for n in SN:
|
359
|
-
if n != 'liquid1' and n!= 'fluid1':
|
390
|
+
if n != 'liquid1' and n!= 'fluid1' and n != 'liq1' and n != 'fl1':
|
360
391
|
Results_Mass[n + '_cumsum'] = Results_Mass[n].cumsum()
|
392
|
+
if 'liq1' in SN:
|
393
|
+
Results_Mass[n + '_cumsum'] = Results_Mass.loc[0, 'liq1'] - Results_Mass.loc[:,Results_Mass.columns.str.contains('_cumsum')].sum(axis = 1)
|
394
|
+
elif 'liquid1' in SN:
|
395
|
+
Results_Mass[n + '_cumsum'] = Results_Mass.loc[0, 'liquid1'] - Results_Mass.loc[:,Results_Mass.columns.str.contains('_cumsum')].sum(axis = 1)
|
361
396
|
else:
|
362
397
|
for n in SN:
|
363
|
-
if n != 'liquid1':
|
398
|
+
if n != 'liquid1' and n != 'liq1':
|
364
399
|
Results_Mass[n + '_cumsum'] = Results_Mass[n].cumsum()
|
400
|
+
if 'liq1' in SN:
|
401
|
+
Results_Mass[n + '_cumsum'] = Results_Mass.loc[0, 'liq1'] - Results_Mass.loc[:,Results_Mass.columns.str.contains('_cumsum')].sum(axis = 1)
|
402
|
+
elif 'liquid1' in SN:
|
403
|
+
Results_Mass[n + '_cumsum'] = Results_Mass.loc[0, 'liquid1'] - Results_Mass.loc[:,Results_Mass.columns.str.contains('_cumsum')].sum(axis = 1)
|
365
404
|
|
366
405
|
Results_All = Results['Conditions'].copy()
|
367
406
|
for R in Results:
|
PetThermoTools/Liq.py
CHANGED
@@ -16,6 +16,26 @@ def equilibrate_multi(cores = None, Model = None, bulk = None, T_C = None, P_bar
|
|
16
16
|
Fe3Fet_Liq = None, H2O_Liq = None, CO2_Liq = None, fO2_buffer = None, fO2_offset = None,
|
17
17
|
timeout = None, copy_columns = None, Suppress = None):
|
18
18
|
|
19
|
+
T_C = to_float(T_C)
|
20
|
+
|
21
|
+
P_bar = to_float(P_bar)
|
22
|
+
|
23
|
+
H2O_Liq = to_float(H2O_Liq)
|
24
|
+
CO2_Liq = to_float(CO2_Liq)
|
25
|
+
Fe3Fet_Liq = to_float(Fe3Fet_Liq)
|
26
|
+
fO2_offset = to_float(fO2_offset)
|
27
|
+
|
28
|
+
if fO2_buffer is not None:
|
29
|
+
if fO2_buffer != "NNO":
|
30
|
+
if fO2_buffer != "FMQ":
|
31
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
32
|
+
|
33
|
+
if "MELTS" not in Model:
|
34
|
+
if fO2_buffer == "FMQ":
|
35
|
+
fO2_buffer = "qfm"
|
36
|
+
if fO2_buffer == "NNO":
|
37
|
+
fO2_buffer = "nno"
|
38
|
+
|
19
39
|
if "MELTS" in Model:
|
20
40
|
try:
|
21
41
|
from meltsdynamic import MELTSdynamic
|
@@ -410,6 +430,17 @@ def findCO2_multi(cores = None, Model = None, bulk = None, T_initial_C = None, P
|
|
410
430
|
except:
|
411
431
|
Warning('alphaMELTS for Python files are not on the python path. \n Please add these files to the path running \n import sys \n sys.path.append(r"insert_your_path_to_melts_here") \n You are looking for the location of the meltsdynamic.py file')
|
412
432
|
|
433
|
+
if fO2_buffer is not None:
|
434
|
+
if fO2_buffer != "NNO":
|
435
|
+
if fO2_buffer != "FMQ":
|
436
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
437
|
+
|
438
|
+
if "MELTS" not in Model:
|
439
|
+
if fO2_buffer == "FMQ":
|
440
|
+
fO2_buffer = "qfm"
|
441
|
+
if fO2_buffer == "NNO":
|
442
|
+
fO2_buffer = "nno"
|
443
|
+
|
413
444
|
comp = bulk.copy()
|
414
445
|
|
415
446
|
if Model is None:
|
@@ -607,11 +638,22 @@ def findLiq_multi(cores = None, Model = None, bulk = None, T_initial_C = None, P
|
|
607
638
|
|
608
639
|
comp = bulk.copy()
|
609
640
|
|
641
|
+
if fO2_buffer is not None:
|
642
|
+
if fO2_buffer != "NNO":
|
643
|
+
if fO2_buffer != "FMQ":
|
644
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
645
|
+
|
646
|
+
if "MELTS" not in Model:
|
647
|
+
if fO2_buffer == "FMQ":
|
648
|
+
fO2_buffer = "qfm"
|
649
|
+
if fO2_buffer == "NNO":
|
650
|
+
fO2_buffer = "nno"
|
651
|
+
|
610
652
|
if Model is None:
|
611
653
|
Model = "MELTSv1.0.2"
|
612
654
|
|
613
|
-
if Model == "Holland":
|
614
|
-
|
655
|
+
# if Model == "Holland":
|
656
|
+
# import pyMAGEMINcalc as MM
|
615
657
|
|
616
658
|
# if comp is entered as a pandas series, it must first be converted to a dict
|
617
659
|
if type(comp) == pd.core.series.Series:
|
@@ -792,8 +834,8 @@ def findLiq_multi(cores = None, Model = None, bulk = None, T_initial_C = None, P
|
|
792
834
|
else:
|
793
835
|
return Res
|
794
836
|
else:
|
795
|
-
T_Liq = MM.findLiq_multi(P_bar = P_bar, T_initial_C = T_initial_C, comp = comp)
|
796
|
-
return
|
837
|
+
# T_Liq = MM.findLiq_multi(P_bar = P_bar, T_initial_C = T_initial_C, comp = comp)
|
838
|
+
return "find liquidus calculations are currently not available through the MAGEMin models. This is an issue I'm working to fix as soon as possible."
|
797
839
|
|
798
840
|
def findCO2(q, index, *, Model = None, P_bar = None, T_initial_C = None, comp = None, fO2_buffer = None, fO2_offset = None):
|
799
841
|
T_Liq = 0
|
PetThermoTools/MELTS.py
CHANGED
@@ -1572,7 +1572,9 @@ def findSatPressure_MELTS(Model = None, T_C_init = None, T_fixed_C = None, P_bar
|
|
1572
1572
|
else:
|
1573
1573
|
return out
|
1574
1574
|
|
1575
|
-
def AdiabaticDecompressionMelting_MELTS(Model = None, comp = None, Tp_C = None,
|
1575
|
+
def AdiabaticDecompressionMelting_MELTS(Model = None, comp = None, Tp_C = None, Tp_Method = None,
|
1576
|
+
P_path_bar = None, P_start_bar = None, P_end_bar = None, dp_bar = None,
|
1577
|
+
Frac = False, fO2_buffer = None, fO2_offset = None):
|
1576
1578
|
try:
|
1577
1579
|
import pyMelt as m
|
1578
1580
|
Lithologies = {'KLB-1': m.lithologies.matthews.klb1(),
|
PetThermoTools/Melting.py
CHANGED
@@ -10,37 +10,6 @@ from multiprocessing import Process
|
|
10
10
|
import time
|
11
11
|
import sys
|
12
12
|
from tqdm.notebook import tqdm, trange
|
13
|
-
# import pyMelt as m
|
14
|
-
|
15
|
-
def comp_check(comp_lith, Model, MELTS_filter, Fe3Fet):
|
16
|
-
if type(comp_lith) == str:
|
17
|
-
if Model != "pyMelt":
|
18
|
-
comp = Compositions[comp_lith]
|
19
|
-
else:
|
20
|
-
comp = comp_lith
|
21
|
-
else:
|
22
|
-
comp = comp_lith.copy()
|
23
|
-
|
24
|
-
# if comp is entered as a pandas series, it must first be converted to a dict
|
25
|
-
if Model != "pyMelt":
|
26
|
-
if type(comp) == pd.core.series.Series:
|
27
|
-
comp = comp.to_dict()
|
28
|
-
|
29
|
-
comp = comp_fix(Model = Model, comp = comp, Fe3Fet_Liq = Fe3Fet)
|
30
|
-
|
31
|
-
if "MELTS" in Model and MELTS_filter == True:
|
32
|
-
if type(comp) == pd.core.frame.DataFrame:
|
33
|
-
comp['K2O_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
34
|
-
comp['P2O5_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
35
|
-
comp['H2O_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
36
|
-
comp['CO2_Liq'] = np.zeros(len(comp['SiO2_Liq']))
|
37
|
-
else:
|
38
|
-
comp['K2O_Liq'] = 0
|
39
|
-
comp['P2O5_Liq'] = 0
|
40
|
-
comp['H2O_Liq'] = 0
|
41
|
-
comp['CO2_Liq'] = 0
|
42
|
-
|
43
|
-
return comp
|
44
13
|
|
45
14
|
def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
46
15
|
Model = "pMELTS", bulk = "KLB-1", comp_lith_1 = None,
|
@@ -48,6 +17,67 @@ def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
|
48
17
|
P_start_bar = 30000, P_end_bar = 2000, dp_bar = 200,
|
49
18
|
P_path_bar = None, Frac = False, prop = None,
|
50
19
|
fO2_buffer = None, fO2_offset = None, Fe3Fet = None, MELTS_filter = True):
|
20
|
+
"""
|
21
|
+
Perform adiabatic decompression melting calculations using MELTS, MAGEMin, or pyMelt.
|
22
|
+
|
23
|
+
Simulates mantle melting along an adiabatic upwelling path (e.g., ridge or plume) with user-defined
|
24
|
+
starting potential temperature, pressure range, and step size. Supports single-lithology mantle sources
|
25
|
+
(e.g., KLB-1) at present, with expansion to multi-lithology systems in development.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
cores : int, optional
|
30
|
+
Number of CPU cores to use for multiprocessing. Defaults to total available.
|
31
|
+
Model : str, optional
|
32
|
+
Thermodynamic model. MELTS variants: "MELTSv1.0.2", "MELTSv1.1.0", "MELTSv1.2.0", "pMELTS";
|
33
|
+
or MAGEMin: "Green2025", "Weller2024". Alternatively calculations can be performed using
|
34
|
+
pyMelt (Matthews et al. 2020): "pyMelt"
|
35
|
+
bulk : dict ot str, optional
|
36
|
+
Bulk composition name or composition dictionary.
|
37
|
+
Default is "KLB-1".
|
38
|
+
Tp_C : float or np.ndarray, optional
|
39
|
+
Mantle potential temperature(s) in °C. Default is 1350.
|
40
|
+
Tp_Method : str, optional
|
41
|
+
Method to calculate the starting pressure for adiabatic melting. Default is "pyMelt".
|
42
|
+
P_start_bar, P_end_bar, dp_bar : float or array, optional
|
43
|
+
Starting, ending, and step size pressures (in bar) for adiabatic decompression.
|
44
|
+
Defaults: 30000, 2000, and 200, respectively.
|
45
|
+
P_path_bar : np.ndarray, optional
|
46
|
+
User-specified pressure path (in bar). If given, overrides `P_start_bar`, `P_end_bar`, and `dp_bar`.
|
47
|
+
fO2_buffer : {"FMQ", "NNO"}, optional
|
48
|
+
Redox buffer for constraining oxygen fugacity.
|
49
|
+
fO2_offset : float, optional
|
50
|
+
Offset (log units) from the chosen fO2 buffer.
|
51
|
+
Fe3Fet : float, optional
|
52
|
+
Initial Fe³⁺/ΣFe ratio for the bulk composition. If None, values is taken from the "bulk" variable or set according to fO2 buffer positions.
|
53
|
+
MELTS_filter : bool, default=True
|
54
|
+
If True, filters oxide components to avoid issues in MELTS calculations (e.g., K2O content set to 0.0).
|
55
|
+
|
56
|
+
Returns
|
57
|
+
-------
|
58
|
+
Results : dict
|
59
|
+
Dictionary containing DataFrames for the system and phase compositions and properties.
|
60
|
+
|
61
|
+
Notes
|
62
|
+
-----
|
63
|
+
- Currently limited to single-lithology melting.
|
64
|
+
- Normalizes output mass so that total initial mass = 1.
|
65
|
+
|
66
|
+
Examples
|
67
|
+
--------
|
68
|
+
Run a single adiabatic decompression path from 3.0 GPa to 0.2 GPa:
|
69
|
+
|
70
|
+
>>> results = AdiabaticDecompressionMelting(Model="pMELTS", bulk="KLB-1",
|
71
|
+
... Tp_C=1350, P_start_bar=30000,
|
72
|
+
... P_end_bar=2000, dp_bar=200)
|
73
|
+
|
74
|
+
Run with an explicit pressure path:
|
75
|
+
|
76
|
+
>>> import numpy as np
|
77
|
+
>>> P_path = np.linspace(30000, 2000, 20)
|
78
|
+
>>> results = AdiabaticDecompressionMelting(Model="pMELTS", comp_lith_1="KLB-1",
|
79
|
+
... P_path_bar=P_path, Tp_C=1400)
|
80
|
+
"""
|
51
81
|
|
52
82
|
Tp_C = to_float(Tp_C)
|
53
83
|
|
@@ -59,22 +89,33 @@ def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
|
59
89
|
Fe3Fet = to_float(Fe3Fet)
|
60
90
|
fO2_offset = to_float(fO2_offset)
|
61
91
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
92
|
+
if fO2_buffer is not None:
|
93
|
+
if fO2_buffer != "NNO":
|
94
|
+
if fO2_buffer != "FMQ":
|
95
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
96
|
+
|
97
|
+
if "MELTS" not in Model:
|
98
|
+
if fO2_buffer == "FMQ":
|
99
|
+
fO2_buffer = "qfm"
|
100
|
+
if fO2_buffer == "NNO":
|
101
|
+
fO2_buffer = "nno"
|
102
|
+
|
103
|
+
# if Tp_Method == "pyMelt":
|
104
|
+
# try:
|
105
|
+
# import pyMelt as m
|
106
|
+
# Lithologies = {'KLB-1': m.lithologies.matthews.klb1(),
|
107
|
+
# 'KG1': m.lithologies.matthews.kg1(),
|
108
|
+
# 'G2': m.lithologies.matthews.eclogite(),
|
109
|
+
# 'hz': m.lithologies.shorttle.harzburgite()}
|
110
|
+
# except ImportError:
|
111
|
+
# raise RuntimeError('You havent installed pyMelt or there is an error when importing pyMelt. pyMelt is currently required to estimate the starting point for the melting calculations.')
|
72
112
|
|
73
113
|
if bulk is not None and comp_lith_1 is None:
|
74
114
|
comp_lith_1 = bulk
|
75
115
|
|
76
116
|
comp_1 = comp_check(comp_lith_1, Model, MELTS_filter, Fe3Fet)
|
77
117
|
|
118
|
+
# place holders for when code is expanded to account for multi-lithology mantle
|
78
119
|
if comp_lith_2 is not None:
|
79
120
|
comp_2 = comp_check(comp_lith_2, Model, MELTS_filter, Fe3Fet)
|
80
121
|
else:
|
@@ -85,6 +126,8 @@ def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
|
85
126
|
else:
|
86
127
|
comp_3 = None
|
87
128
|
|
129
|
+
|
130
|
+
# At present calculations only work for a single simulation - this represents a placeholder for when the code is expanded to account for multiple simulations
|
88
131
|
One = 0
|
89
132
|
if Model != "pyMelt":
|
90
133
|
if type(comp_1) == pd.core.frame.DataFrame: # simplest scenario - one calculation per bulk composition imported
|
@@ -125,7 +168,7 @@ def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
|
125
168
|
if One == 1:
|
126
169
|
p = Process(target = AdiabaticMelt, args = (q, 1),
|
127
170
|
kwargs = {'Model': Model, 'comp_1': comp_1, 'comp_2': comp_2, 'comp_3': comp_3,
|
128
|
-
'Tp_C': Tp_C, 'P_path_bar': P_path_bar,
|
171
|
+
'Tp_C': Tp_C, 'Tp_Method': Tp_Method, 'P_path_bar': P_path_bar,
|
129
172
|
'P_start_bar': P_start_bar, 'P_end_bar': P_end_bar, 'dp_bar': dp_bar,
|
130
173
|
'fO2_buffer': fO2_buffer, 'fO2_offset': fO2_offset, 'Frac': Frac, 'prop': prop})
|
131
174
|
|
@@ -167,7 +210,7 @@ def AdiabaticDecompressionMelting(cores = multiprocessing.cpu_count(),
|
|
167
210
|
|
168
211
|
return Results
|
169
212
|
|
170
|
-
def AdiabaticMelt(q, index, *, Model = None, comp_1 = None, comp_2 = None, comp_3 = None,
|
213
|
+
def AdiabaticMelt(q, index, *, Model = None, comp_1 = None, comp_2 = None, comp_3 = None, Tp_Method = "pyMelt",
|
171
214
|
Tp_C = None, P_start_bar = None, P_end_bar = None, dp_bar = None, P_path_bar = None,
|
172
215
|
Frac = None, fO2_buffer = None, fO2_offset = None, prop = None):
|
173
216
|
'''
|
@@ -177,7 +220,7 @@ def AdiabaticMelt(q, index, *, Model = None, comp_1 = None, comp_2 = None, comp_
|
|
177
220
|
Results = {}
|
178
221
|
if "MELTS" in Model:
|
179
222
|
try:
|
180
|
-
Results = AdiabaticDecompressionMelting_MELTS(Model = Model, comp = comp_1, Tp_C = Tp_C,
|
223
|
+
Results = AdiabaticDecompressionMelting_MELTS(Model = Model, comp = comp_1, Tp_C = Tp_C, Tp_Method = "pyMelt",
|
181
224
|
P_path_bar = P_path_bar, P_start_bar = P_start_bar, P_end_bar = P_end_bar, dp_bar = dp_bar,
|
182
225
|
fO2_buffer = fO2_buffer, fO2_offset = fO2_offset)
|
183
226
|
q.put([Results, index])
|
@@ -250,10 +293,6 @@ def AdiabaticMelt(q, index, *, Model = None, comp_1 = None, comp_2 = None, comp_
|
|
250
293
|
return
|
251
294
|
|
252
295
|
else:
|
253
|
-
# import pyMAGEMINcalc as MM
|
254
|
-
# Results = MM.AdiabaticDecompressionMelting(comp = comp_1, T_p_C = Tp_C, P_start_kbar = P_start_bar/1000, P_end_kbar = P_end_bar/1000, dp_kbar = dp_bar/1000, Frac = 0)
|
255
|
-
# print('Note that the ability to use MAGEMin to performed adiabatic decompression melting in PetThermoTools has been temporarily disabled. The underlying issue will be fixed soon and this funciton will once again become available.')
|
256
|
-
|
257
296
|
try:
|
258
297
|
import pyMelt as m
|
259
298
|
Lithologies = {'KLB-1': m.lithologies.matthews.klb1(),
|
@@ -263,10 +302,13 @@ def AdiabaticMelt(q, index, *, Model = None, comp_1 = None, comp_2 = None, comp_
|
|
263
302
|
except ImportError:
|
264
303
|
raise RuntimeError('You havent installed pyMelt or there is an error when importing pyMelt. pyMelt is currently required to estimate the starting point for the melting calculations.')
|
265
304
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
305
|
+
if Tp_Method == "pyMelt":
|
306
|
+
lz = m.lithologies.matthews.klb1()
|
307
|
+
mantle = m.mantle([lz], [1], ['Lz'])
|
308
|
+
T_start_C = mantle.adiabat(P_start_bar/10000.0, Tp_C)
|
309
|
+
else:
|
310
|
+
T_start_C = None
|
311
|
+
|
270
312
|
from juliacall import Main as jl, convert as jlconvert
|
271
313
|
|
272
314
|
jl.seval("using MAGEMinCalc")
|
PetThermoTools/Path.py
CHANGED
@@ -170,6 +170,8 @@ def multi_path(cores = None, Model = None, bulk = None, comp = None, Frac_solid
|
|
170
170
|
if "MELTS" not in Model:
|
171
171
|
if fO2_buffer == "FMQ":
|
172
172
|
fO2_buffer = "qfm"
|
173
|
+
if fO2_buffer == "NNO":
|
174
|
+
fO2_buffer = "nno"
|
173
175
|
|
174
176
|
# ensure the bulk composition has the correct headers etc.
|
175
177
|
comp = comp_fix(Model = Model, comp = comp, Fe3Fet_Liq = Fe3Fet_init, H2O_Liq = H2O_init, CO2_Liq = CO2_init)
|
PetThermoTools/PhaseDiagrams.py
CHANGED
@@ -103,6 +103,17 @@ def phaseDiagram_calc(cores = None, Model = None, bulk = None, T_C = None, P_bar
|
|
103
103
|
|
104
104
|
comp = bulk.copy()
|
105
105
|
|
106
|
+
if fO2_buffer is not None:
|
107
|
+
if fO2_buffer != "NNO":
|
108
|
+
if fO2_buffer != "FMQ":
|
109
|
+
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
110
|
+
|
111
|
+
if "MELTS" not in Model:
|
112
|
+
if fO2_buffer == "FMQ":
|
113
|
+
fO2_buffer = "qfm"
|
114
|
+
if fO2_buffer == "NNO":
|
115
|
+
fO2_buffer = "nno"
|
116
|
+
|
106
117
|
if cores is None:
|
107
118
|
cores = multiprocessing.cpu_count()
|
108
119
|
|
PetThermoTools/Plotting.py
CHANGED
@@ -3,128 +3,237 @@ import pandas as pd
|
|
3
3
|
from shapely.geometry import MultiPoint, Point, Polygon
|
4
4
|
import matplotlib.pyplot as plt
|
5
5
|
from matplotlib import cm
|
6
|
+
import matplotlib.colors as mc
|
6
7
|
from PetThermoTools.GenFuncs import *
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
Line
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
8
|
+
from itertools import zip_longest
|
9
|
+
|
10
|
+
def harker(Results=None, x_axis="MgO", y_axis=("SiO2", "TiO2", "Al2O3", "Cr2O3", "FeOt", "CaO", "Na2O", "K2O"),
|
11
|
+
phase="liquid1", line_color=None, data=None, d_color=None, d_marker=None,
|
12
|
+
legend=True, legend_loc=None,
|
13
|
+
xlim=None, ylim=None):
|
14
|
+
"""
|
15
|
+
Generates a Harker diagram (oxide vs. MgO or another oxide) from model results.
|
16
|
+
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
Results : dict or None, optional
|
20
|
+
MELTS or MAGEMin results dictionary. If multiple results, they are overlaid.
|
21
|
+
x_axis : str, default="MgO"
|
22
|
+
Oxide to use for the x-axis.
|
23
|
+
y_axis : tuple of str, default=("SiO2","TiO2","Al2O3","Cr2O3","FeOt","CaO","Na2O","K2O")
|
24
|
+
Oxides to plot on y-axes. Plotted in groups of three per row.
|
25
|
+
phase : str, default="liquid1"
|
26
|
+
Phase to plot (must be in PetThermoTools.GenFuncs.Names).
|
27
|
+
line_color : str or None, optional
|
28
|
+
Line color. If None, uses matplotlib's default cycle.
|
29
|
+
data : pd.DataFrame, dict of DataFrames, or str, optional
|
30
|
+
External data to plot for comparison. If str, treated as CSV path.
|
31
|
+
If dict, plots each DataFrame with different marker/color.
|
32
|
+
d_colors : list of str or None, optional
|
33
|
+
Colors for external datasets. Cycles if fewer than number of datasets.
|
34
|
+
d_markers : list of str or None, optional
|
35
|
+
Markers for external datasets. Cycles if fewer than number of datasets.
|
36
|
+
legend : bool, default=True
|
37
|
+
Whether to display a legend.
|
38
|
+
legend_loc : tuple(int, int) or None, optional
|
39
|
+
Location of legend in subplot grid, as (row, col). If None, placed on last axis.
|
40
|
+
xlim, ylim : tuple or None, optional
|
41
|
+
Limits for x and y axes.
|
42
|
+
|
43
|
+
Returns
|
44
|
+
-------
|
45
|
+
f : matplotlib.figure.Figure
|
46
|
+
Figure object.
|
47
|
+
a : np.ndarray of Axes
|
48
|
+
Array of Axes objects.
|
49
|
+
"""
|
41
50
|
if Results is None:
|
42
|
-
raise
|
43
|
-
|
44
|
-
if x_axis is None:
|
45
|
-
x_axis = "MgO"
|
46
|
-
|
47
|
-
if y_axis is None:
|
48
|
-
y_axis = ["SiO2", "TiO2", "Al2O3", "FeOt", "CaO", "Na2O"]
|
51
|
+
raise ValueError("Results cannot be None. Provide MELTS or MAGEMin results dictionary.")
|
49
52
|
|
50
|
-
if
|
51
|
-
|
53
|
+
if isinstance(y_axis, str):
|
54
|
+
y_axis = (y_axis,)
|
52
55
|
|
53
|
-
|
54
|
-
line_style = '-'
|
55
|
-
|
56
|
-
if line_color is None:
|
57
|
-
line_color = 'k'
|
56
|
+
y_axis = list(y_axis)
|
58
57
|
|
58
|
+
# Data loading
|
59
59
|
if data is not None:
|
60
|
-
if
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
60
|
+
if isinstance(data, str):
|
61
|
+
if data.contains('.csv'):
|
62
|
+
data = pd.read_csv(data)
|
63
|
+
else:
|
64
|
+
data = pd.read_excel(data)
|
65
|
+
elif isinstance(data, dict):
|
66
|
+
# validate
|
67
|
+
for k, v in data.items():
|
68
|
+
if not isinstance(v, pd.DataFrame):
|
69
|
+
raise ValueError(f"data['{k}'] must be a DataFrame, got {type(v)}")
|
70
|
+
|
71
|
+
# -------------------- filter y_axis to only include variables present --------------------
|
72
|
+
valid_y = []
|
73
|
+
for y in y_axis:
|
74
|
+
found = False
|
75
|
+
|
76
|
+
# Check Results
|
77
|
+
for r in (Results.keys() if isinstance(Results, dict) else [Results]):
|
78
|
+
Res = Results[r] if isinstance(Results, dict) else Results
|
79
|
+
if "All" in Res:
|
80
|
+
cols = Res["All"].columns
|
81
|
+
if (y + Names[phase]) in cols or y in cols:
|
82
|
+
if np.nanmax(Res["All"][y + Names[phase]]) > 0.0:
|
83
|
+
found = True
|
84
|
+
break
|
85
|
+
|
86
|
+
# Check data (dict or DataFrame)
|
87
|
+
if not found and data is not None:
|
88
|
+
if isinstance(data, dict):
|
89
|
+
for df in data.values():
|
90
|
+
cols = df.columns
|
91
|
+
if (y + Names[phase]) in cols or y in cols:
|
92
|
+
found = True
|
93
|
+
break
|
94
|
+
else:
|
95
|
+
cols = data.columns
|
96
|
+
if (y + Names[phase]) in cols or y in cols:
|
97
|
+
found = True
|
98
|
+
|
99
|
+
if found:
|
100
|
+
valid_y.append(y)
|
101
|
+
|
102
|
+
# Replace y_axis with only valid variables
|
103
|
+
y_axis = valid_y
|
104
|
+
|
105
|
+
# -------------------- subplot grid handling --------------------
|
106
|
+
n_vars = len(y_axis)
|
107
|
+
|
108
|
+
if n_vars == 4: # special case: 2x2 grid
|
109
|
+
ncols, nrows = 2, 2
|
110
|
+
rows = list(zip_longest(*[iter(y_axis)] * ncols, fillvalue=None))
|
71
111
|
else:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
112
|
+
ncols = 3
|
113
|
+
nrows = int(np.ceil(n_vars / ncols))
|
114
|
+
rows = list(zip_longest(*[iter(y_axis)] * ncols, fillvalue=None))
|
115
|
+
|
116
|
+
# scale figure size dynamically
|
117
|
+
fig_width = 3.2 * ncols
|
118
|
+
fig_height = 3.0 * nrows
|
119
|
+
f, a = plt.subplots(nrows, ncols, figsize=(fig_width, fig_height))
|
120
|
+
a = np.atleast_2d(a)
|
121
|
+
|
122
|
+
# Color handling for models
|
123
|
+
color_cycle = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
124
|
+
if line_color is None:
|
125
|
+
def pick_color(i): return color_cycle[i % len(color_cycle)]
|
126
|
+
else:
|
127
|
+
def pick_color(i): return line_color
|
128
|
+
|
129
|
+
# Color/marker cycles for user data
|
130
|
+
d_color_cycle = d_color if d_color else color_cycle
|
131
|
+
d_marker_cycle = d_marker if d_marker else ["o", "s", "^", "D", "v", "P", "*"]
|
132
|
+
|
133
|
+
# -------------------- helper functions --------------------
|
134
|
+
def plot_panel(ax, x_var, y_var, idx=None, Res = None):
|
135
|
+
"""Plot one panel (model + data) on given Axes."""
|
136
|
+
suffix = Names[phase] if phase in Names else ""
|
137
|
+
xcol, ycol = x_var + suffix, y_var + suffix
|
138
|
+
|
139
|
+
# plot model data
|
140
|
+
if Res is not None:
|
141
|
+
if xcol in Results[Res]["All"] and ycol in Results[Res]["All"]:
|
142
|
+
x = Results[Res]["All"][xcol].values
|
143
|
+
y = Results[Res]["All"][ycol].values
|
144
|
+
ax.plot(x, y, color=pick_color(idx), label=Res)
|
145
|
+
else:
|
146
|
+
if xcol in Results["All"] and ycol in Results["All"]:
|
147
|
+
x = Results["All"][xcol].values
|
148
|
+
y = Results["All"][ycol].values
|
149
|
+
ax.plot(x, y, color='k')
|
150
|
+
|
151
|
+
def plot_data(ax, x_var, y_var):
|
152
|
+
# plot external data
|
153
|
+
suffix = Names[phase] if phase in Names else ""
|
154
|
+
xcol, ycol = x_var + suffix, y_var + suffix
|
155
|
+
|
156
|
+
if data is not None:
|
157
|
+
if isinstance(data, pd.DataFrame):
|
158
|
+
datasets = {"data": data}
|
159
|
+
else:
|
160
|
+
datasets = data
|
161
|
+
|
162
|
+
for k, df in datasets.items():
|
163
|
+
c = d_color_cycle[list(datasets.keys()).index(k) % len(d_color_cycle)]
|
164
|
+
m = d_marker_cycle[list(datasets.keys()).index(k) % len(d_marker_cycle)]
|
165
|
+
|
166
|
+
# try with suffix first
|
167
|
+
if xcol in df.columns:
|
168
|
+
dx = df[xcol].values
|
169
|
+
elif x_var in df.columns:
|
170
|
+
dx = df[x_var].values
|
171
|
+
else:
|
172
|
+
dx = None
|
173
|
+
print(f"x axis variable {x_var} not found in data")
|
95
174
|
|
175
|
+
if ycol in df.columns:
|
176
|
+
dy = df[ycol].values
|
177
|
+
elif y_var in df.columns:
|
178
|
+
dy = df[y_var].values
|
179
|
+
else:
|
180
|
+
dy = None
|
181
|
+
print(f"y axis variable {y_var} not found in data")
|
182
|
+
|
183
|
+
if dx is not None and dy is not None:
|
184
|
+
ax.plot(dx, dy, m,
|
185
|
+
markerfacecolor=c,
|
186
|
+
markeredgecolor="k",
|
187
|
+
markersize=4,
|
188
|
+
linestyle="None",
|
189
|
+
label=k)
|
190
|
+
|
191
|
+
ax.set(xlabel=x_var, ylabel=y_var)
|
192
|
+
|
193
|
+
if xlim:
|
194
|
+
ax.set_xlim(xlim)
|
195
|
+
if ylim:
|
196
|
+
ax.set_ylim(ylim)
|
197
|
+
|
198
|
+
# -------------------- main plotting --------------------
|
199
|
+
for i, row in enumerate(rows):
|
200
|
+
for j, y in enumerate(row):
|
201
|
+
if y is None:
|
202
|
+
a[i, j].axis("off")
|
203
|
+
continue
|
204
|
+
if 'All' in Results.keys():
|
205
|
+
if data is not None:
|
206
|
+
plot_data(a[i,j], x_axis, y)
|
207
|
+
plot_panel(a[i,j], x_axis, y)
|
208
|
+
else:
|
209
|
+
if data is not None:
|
210
|
+
plot_data(a[i,j], x_axis, y)
|
211
|
+
for idx, Res in enumerate(Results):
|
212
|
+
plot_panel(a[i, j], x_axis, y, idx=idx, Res = Res)
|
213
|
+
|
214
|
+
# -------------------- legend --------------------
|
215
|
+
empty_axes = []
|
216
|
+
for i in range(a.shape[0]):
|
217
|
+
for j in range(a.shape[1]):
|
218
|
+
if i * a.shape[1] + j >= len(y_axis): # beyond valid y variables
|
219
|
+
empty_axes.append(a[i][j])
|
220
|
+
|
221
|
+
handles, labels = a[0][0].get_legend_handles_labels()
|
222
|
+
if empty_axes and handles:
|
223
|
+
empty_axes[0].legend(handles, labels, loc = "center")
|
224
|
+
empty_axes[0].axis("off")
|
96
225
|
else:
|
97
|
-
if
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
a[i][j].set_ylabel(y_axis[i][j] + " wt%")
|
105
|
-
if i != np.shape(y_axis)[0] - 1:
|
106
|
-
if i == np.shape(y_axis)[0] - 2:
|
107
|
-
if y_axis[i+1,j] == "None":
|
108
|
-
a[i][j].set_xlabel(x_axis + " wt%")
|
109
|
-
else:
|
110
|
-
a[i][j].set_xlabel(x_axis + " wt%")
|
111
|
-
|
112
|
-
else:
|
113
|
-
a[i][j].axis('off')
|
114
|
-
|
115
|
-
for r in Results:
|
116
|
-
Res = Results[r].copy()
|
117
|
-
if type(phase) == str:
|
118
|
-
for i in range(np.shape(y_axis)[0]):
|
119
|
-
for j in range(np.shape(y_axis)[1]):
|
120
|
-
if y_axis[i,j] != "None":
|
121
|
-
a[i][j].plot(Res['All'][x_axis + Names[phase]], Res['All'][y_axis[i,j] + Names[phase]], line_style, linewidth = 2, label = r)
|
122
|
-
|
123
|
-
if label is not None:
|
124
|
-
a[0][0].legend()
|
226
|
+
if legend:
|
227
|
+
if legend_loc is None:
|
228
|
+
loc_i, loc_j = len(rows) - 1, 0
|
229
|
+
else:
|
230
|
+
loc_i, loc_j = legend_loc
|
231
|
+
a[loc_i, loc_j].legend()
|
125
232
|
|
126
233
|
f.tight_layout()
|
127
234
|
|
235
|
+
return f, a
|
236
|
+
|
128
237
|
def plot_surfaces(Results = None, P_bar = None, phases = None, H2O_Liq = None):
|
129
238
|
if H2O_Liq is None:
|
130
239
|
f, a = plt.subplots(1,1, figsize = (5,4))
|
@@ -358,69 +467,191 @@ def residualT_plot(Results = None, P_bar = None, phases = None, H2O_Liq = None,
|
|
358
467
|
a[i][j].plot_surface(X_new, Y_new, z_plot, cmap = 'viridis')
|
359
468
|
a[i][j].set_zlim([0,50])
|
360
469
|
|
361
|
-
def phase_plot(Results = None, y_axis = None, x_axis = None,
|
362
|
-
phases = ['Liq', 'Ol', 'Opx', 'Cpx', 'Sp', 'Grt'], cmap = "Reds",
|
363
|
-
title = None, figsize = None):
|
364
|
-
|
365
|
-
if type(Results) != list:
|
366
|
-
f, a = plt.subplots(1,1, figsize = (5, 8))
|
367
|
-
c = cm.get_cmap(cmap, len(phases))
|
368
|
-
x = c(np.arange(0,1,1/len(phases)))
|
369
470
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
471
|
+
def phase_plot(Results, x_axis = None, y_axis = None, cmap = "Reds"):
|
472
|
+
"""
|
473
|
+
Create stacked phase mass-fraction plots from thermodynamic model results.
|
474
|
+
|
475
|
+
This function generates diagrams of phase proportions from the results of
|
476
|
+
crystallization or melting simulations. Mass fractions of crystalline and
|
477
|
+
liquid phases are stacked either along the x-axis or y-axis, depending on
|
478
|
+
user specification, to visualize how phase proportions evolve with pressure,
|
479
|
+
temperature, or another variable.
|
480
|
+
|
481
|
+
Parameters
|
482
|
+
----------
|
483
|
+
Results : dict
|
484
|
+
Dictionary containing model outputs. It should have one of the following structures:
|
485
|
+
- **Single-run results**:
|
486
|
+
```
|
487
|
+
Results = {
|
488
|
+
"Mass": pandas.DataFrame, # columns = phase names or phase_cumsum
|
489
|
+
"All": pandas.DataFrame # contains the axis variable (e.g., T_C, P_bar)
|
490
|
+
}
|
491
|
+
```
|
492
|
+
- **Multi-run results**:
|
493
|
+
```
|
494
|
+
Results = {
|
495
|
+
run_label: {
|
496
|
+
"Mass": pandas.DataFrame,
|
497
|
+
"All": pandas.DataFrame
|
498
|
+
},
|
499
|
+
...
|
500
|
+
}
|
501
|
+
```
|
502
|
+
|
503
|
+
The `"Mass"` DataFrame must include mass fractions for each phase
|
504
|
+
(either raw values or cumulative values with `"_cumsum"` suffix).
|
505
|
+
The `"All"` DataFrame must contain the column specified by `x_axis` or `y_axis`.
|
506
|
+
|
507
|
+
x_axis : str, optional
|
508
|
+
Column name in `Results["All"]` (or equivalent) to plot on the x-axis.
|
509
|
+
If provided, `y_axis` must be `None`. Typically something like `"T_C"`.
|
510
|
+
|
511
|
+
y_axis : str, optional
|
512
|
+
Column name in `Results["All"]` (or equivalent) to plot on the y-axis.
|
513
|
+
If provided, `x_axis` must be `None`. Typically `"P_bar"`.
|
514
|
+
|
515
|
+
cmap : str, default = "Reds"
|
516
|
+
Matplotlib colormap used to assign colors to phases.
|
517
|
+
|
518
|
+
Returns
|
519
|
+
-------
|
520
|
+
fig : matplotlib.figure.Figure or list of Figures
|
521
|
+
- If `Results` contains a single run: a single `Figure`.
|
522
|
+
- If `Results` contains multiple runs: a list of `Figure` objects, one per run.
|
523
|
+
|
524
|
+
axes : matplotlib.axes.Axes or list of Axes
|
525
|
+
- If `Results` contains a single run: a single `Axes` object.
|
526
|
+
- If `Results` contains multiple runs: a list of `Axes` objects, one per run.
|
527
|
+
|
528
|
+
Notes
|
529
|
+
-----
|
530
|
+
- If both `x_axis` and `y_axis` are provided, the function raises a `ValueError`.
|
531
|
+
- Phases are automatically ordered:
|
532
|
+
1. By the index where they first appear (highest pressure or temperature).
|
533
|
+
2. By the order specified in the petthermotools dictionaries `Names` and `Names_MM`,
|
534
|
+
if available.
|
535
|
+
- The liquid phase (`"liq1"` or `"liquid1"`) is always plotted last.
|
536
|
+
- A legend with readable phase labels is added outside
|
537
|
+
the plot area.
|
538
|
+
|
539
|
+
Examples
|
540
|
+
--------
|
541
|
+
>>> fig, ax = phase_plot(Results, y_axis="P_bar")
|
542
|
+
# Plots stacked phase proportions vs. pressure
|
543
|
+
|
544
|
+
>>> fig, axes = phase_plot(MultiRunResults, x_axis="T_C")
|
545
|
+
# Creates one stacked phase plot per run, vs. temperature
|
546
|
+
"""
|
547
|
+
if x_axis is not None and y_axis is not None:
|
548
|
+
raise ValueError("Please provide either a x-axis or y-axis parameter to plot the mass fractions against")
|
549
|
+
|
550
|
+
def makeplot(Mass, Res, title = None):
|
551
|
+
# --- Identify whether _cumsum columns exist ---
|
552
|
+
use_cumsum = any(col.endswith("_cumsum") for col in Mass.columns)
|
553
|
+
suffix = "_cumsum" if use_cumsum else ""
|
554
|
+
|
555
|
+
# --- Identify phases ---
|
556
|
+
exclude_cols = {'T_C', None}
|
557
|
+
phases = [
|
558
|
+
col.replace(suffix, "")
|
559
|
+
for col in Mass.columns
|
560
|
+
if col.endswith(suffix) or (not use_cumsum and not col.endswith("_cumsum") and col not in exclude_cols)
|
561
|
+
]
|
562
|
+
|
563
|
+
# --- Handle liquid phase ---
|
564
|
+
liquid_name = None
|
565
|
+
for liq_name in ["liq1", "liquid1"]:
|
566
|
+
if liq_name in Mass.columns:
|
567
|
+
liquid_name = liq_name
|
568
|
+
if liq_name in phases:
|
569
|
+
phases.remove(liq_name)
|
570
|
+
break # use the first match
|
571
|
+
|
572
|
+
# --- Create dictionary priority (order in Names/Names_MM) ---
|
573
|
+
phase_priority = {}
|
574
|
+
for order_dict in [Names, Names_MM]:
|
575
|
+
for i, key in enumerate(order_dict.keys()):
|
576
|
+
phase_priority[key] = i # smaller i = higher priority in tie
|
577
|
+
|
578
|
+
# --- Sort crystalline phases ---
|
579
|
+
phase_first_index = {}
|
580
|
+
for p in phases:
|
581
|
+
col = p + suffix if (use_cumsum and p + suffix in Mass.columns) else p
|
582
|
+
vals = Mass[col].values
|
583
|
+
nz = np.flatnonzero(vals > 0)
|
584
|
+
phase_first_index[p] = nz[0] if len(nz) > 0 else np.inf
|
585
|
+
|
586
|
+
def sort_key(p):
|
587
|
+
return (phase_first_index[p], phase_priority.get(p, 1e6))
|
588
|
+
|
589
|
+
phases = sorted(phases, key=sort_key)
|
590
|
+
|
591
|
+
# --- Append liquid last ---
|
592
|
+
if liquid_name is not None:
|
593
|
+
phases.append(liquid_name)
|
594
|
+
|
595
|
+
# --- Assign colors ---
|
392
596
|
c = cm.get_cmap(cmap, len(phases))
|
393
|
-
|
394
|
-
|
395
|
-
PhaseList = {}
|
396
|
-
for idx, p in enumerate(phases):
|
397
|
-
PhaseList[p] = x[idx]
|
597
|
+
PhaseColors = {p: c(i) for i, p in enumerate(phases)}
|
398
598
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
599
|
+
# --- Setup figure ---
|
600
|
+
if y_axis == "P_bar":
|
601
|
+
f, a = plt.subplots(figsize=(4, 6))
|
602
|
+
else:
|
603
|
+
f, a = plt.subplots(figsize=(4, 3.5))
|
604
|
+
|
605
|
+
# --- Determine orientation ---
|
606
|
+
coord = Res[y_axis] if y_axis else Res[x_axis]
|
607
|
+
horizontal = y_axis is not None
|
608
|
+
|
609
|
+
Stop = np.zeros(len(Mass))
|
610
|
+
for p in phases:
|
611
|
+
col = p + suffix if use_cumsum else p
|
612
|
+
vals = Mass[col].values
|
613
|
+
|
614
|
+
# --- Legend label mapping ---
|
615
|
+
label = Names.get(p, Names_MM.get(p, p))[1:]
|
616
|
+
|
617
|
+
if horizontal:
|
618
|
+
a.fill_betweenx(coord, Stop, Stop + vals, color=PhaseColors[p], alpha=0.75, lw=0, label=label)
|
619
|
+
else:
|
620
|
+
a.fill_between(coord, Stop, Stop + vals, color=PhaseColors[p], alpha=0.75, lw=0, label=label)
|
621
|
+
Stop += vals
|
622
|
+
|
623
|
+
# --- Labels ---
|
624
|
+
if horizontal:
|
625
|
+
a.set_ylabel(y_axis)
|
626
|
+
a.set_xlabel("Mass fraction")
|
627
|
+
else:
|
628
|
+
a.set_xlabel(x_axis)
|
629
|
+
a.set_ylabel("Mass fraction")
|
405
630
|
|
406
|
-
|
631
|
+
if title is not None:
|
632
|
+
a.set_title(title)
|
407
633
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
a[i].set_ylabel('Mass Fraction')
|
634
|
+
# --- Remove whitespace ---
|
635
|
+
a.margins(0)
|
636
|
+
f.tight_layout()
|
412
637
|
|
413
|
-
|
414
|
-
a[i].set_xlim([np.nanmin(Results[i]['All']['P_bar']), np.nanmax(Results[i]['All']['P_bar'])])
|
638
|
+
a.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
|
415
639
|
|
640
|
+
return f, a
|
641
|
+
fig = []
|
642
|
+
axes = []
|
416
643
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
644
|
+
if 'All' in Results.keys():
|
645
|
+
fig, axes = makeplot(Results['Mass'], Results['All'])
|
646
|
+
else:
|
647
|
+
for val in Results.keys():
|
648
|
+
f, a = makeplot(Results[val]['Mass'],
|
649
|
+
Results[val]['All'],
|
650
|
+
title = val)
|
651
|
+
fig.append(f)
|
652
|
+
axes.append(a)
|
653
|
+
return fig, axes
|
422
654
|
|
423
|
-
return f, a
|
424
655
|
|
425
656
|
def plot_phaseDiagram(Model = "Holland", Combined = None, P_units = "bar", T_units = "C",
|
426
657
|
lines = None, T_C = None, P_bar = None, label = True, colormap = None):
|
PetThermoTools/Saturation.py
CHANGED
@@ -412,6 +412,11 @@ def saturation_pressure(Model = "MELTSv1.2.0", cores = multiprocessing.cpu_count
|
|
412
412
|
if fO2_buffer != "FMQ":
|
413
413
|
raise Warning("fO2 buffer specified is not an allowed input. This argument can only be 'FMQ' or 'NNO' \n if you want to offset from these buffers use the 'fO2_offset' argument.")
|
414
414
|
|
415
|
+
if "MELTS" not in Model:
|
416
|
+
if fO2_buffer == "FMQ":
|
417
|
+
fO2_buffer = "qfm"
|
418
|
+
if fO2_buffer == "NNO":
|
419
|
+
fO2_buffer = "nno"
|
415
420
|
# ensure the bulk composition has the correct headers etc.
|
416
421
|
comp = comp_fix(Model = Model, comp = comp, Fe3Fet_Liq = Fe3Fet_init, H2O_Liq = H2O_init, CO2_Liq = CO2_init)
|
417
422
|
|
PetThermoTools/_version.py
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
PetThermoTools/Barom.py,sha256=HKd1TTHHl0FnM_SGb0qC7KqH7rybyVixVMH7Wph-nk8,46613
|
2
|
+
PetThermoTools/Compositions.py,sha256=65NzfduzWdfHJ8VmHBN1Cv7fMz7kF3QbDVLei-e4v00,1483
|
3
|
+
PetThermoTools/GenFuncs.py,sha256=oMV3FYIpfHNpGZloN5a6Vr8Sv_5IRsKt4hHIUNbDu24,19996
|
4
|
+
PetThermoTools/Holland.py,sha256=udBFeVUyTBpSfLIhx7Hy6o0I8ApNCDvwU_gZa0diY5w,7251
|
5
|
+
PetThermoTools/Installation.py,sha256=UfVOW1NZFdzMWPyID5u7t0KwvpJA0AqYohzidXIAwYs,6098
|
6
|
+
PetThermoTools/Liq.py,sha256=4tOnVROXr7V6cfvZceKidFT9J20TZE3Om2oem92QC4s,36842
|
7
|
+
PetThermoTools/MELTS.py,sha256=2WB4Y11i7yfTM5dRjEr-KrJY2_7f1kLaW4V0di6Qnew,76511
|
8
|
+
PetThermoTools/Melting.py,sha256=D4mXTrKkoUPK8dAuHPQRAnK79owI23W7JBmUMvm1DVU,15332
|
9
|
+
PetThermoTools/Path.py,sha256=jUAVvKSDuVSYRiIZbL1J21E8X4Loc2xCkRTYyl4PzG4,38179
|
10
|
+
PetThermoTools/Path_wrappers.py,sha256=gUxs_4Qbk4MLlLl4iySxfbfKU34588bIJAYyhHmhFdc,30177
|
11
|
+
PetThermoTools/PhaseDiagrams.py,sha256=sjjX84LK34m_OjsKCV78zpzRDAXG7oaLu_Z5BxwB_3I,30298
|
12
|
+
PetThermoTools/Plotting.py,sha256=KxBNT2nqqrVn9dsAIjGk1hMUVRq7r-WrG_b9quNcRMo,37295
|
13
|
+
PetThermoTools/Saturation.py,sha256=3liK4WDVtl5DWpMpE_tQ_fONBCZTctkiNeCXCuNmXoM,29961
|
14
|
+
PetThermoTools/__init__.py,sha256=PbiwQj_mNNEwuIZOLETmtMMshiXa50wjCA6mfvpOpOs,2393
|
15
|
+
PetThermoTools/_version.py,sha256=gEQfVmDQZosUq8hqgOF7-NXfujY6yVc6ya0YiW2W8ZM,296
|
16
|
+
PetThermoTools-0.2.42.dist-info/LICENSE.txt,sha256=-mkx4iEw8Pk1RZUvncBhGLW87Uur5JB7FBQtOmX-VP0,1752
|
17
|
+
PetThermoTools-0.2.42.dist-info/METADATA,sha256=yUKuTP6xbaOHpYQ3g4D5T5qA5XyqfsB0Gs1aBTJF0YA,794
|
18
|
+
PetThermoTools-0.2.42.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
19
|
+
PetThermoTools-0.2.42.dist-info/top_level.txt,sha256=IqK8iYBR3YJozzMOTRZ8x8mU2k6x8ycoMBxZTm-I06U,15
|
20
|
+
PetThermoTools-0.2.42.dist-info/RECORD,,
|
@@ -1,20 +0,0 @@
|
|
1
|
-
PetThermoTools/Barom.py,sha256=6uObJ8ZsHQD6r46ZAyBQGiuVKbmdIVMe4SFp5v-Vigg,46149
|
2
|
-
PetThermoTools/Compositions.py,sha256=65NzfduzWdfHJ8VmHBN1Cv7fMz7kF3QbDVLei-e4v00,1483
|
3
|
-
PetThermoTools/GenFuncs.py,sha256=qiDB0Tdxu4sErSZGCl_Hm6ZUginDI2LG0s9YGiiRqBw,18130
|
4
|
-
PetThermoTools/Holland.py,sha256=udBFeVUyTBpSfLIhx7Hy6o0I8ApNCDvwU_gZa0diY5w,7251
|
5
|
-
PetThermoTools/Installation.py,sha256=UfVOW1NZFdzMWPyID5u7t0KwvpJA0AqYohzidXIAwYs,6098
|
6
|
-
PetThermoTools/Liq.py,sha256=BxAiHTeVQSLgpJT1Ac71apOQYmY3LGTPrxXclwE4F6w,35145
|
7
|
-
PetThermoTools/MELTS.py,sha256=MIW1QUF-W76xjS4nD4tmFXKzYdh7ghY4W05kXU3hoBc,76412
|
8
|
-
PetThermoTools/Melting.py,sha256=O6BX_NEWsjLrdkpaZSwPYqQOzIvSHxfyOMKtg7HSmZc,13061
|
9
|
-
PetThermoTools/Path.py,sha256=9ijpgp91QNRUftnR80F7wWgenWr8bsZkaZ20OIdOD0M,38116
|
10
|
-
PetThermoTools/Path_wrappers.py,sha256=gUxs_4Qbk4MLlLl4iySxfbfKU34588bIJAYyhHmhFdc,30177
|
11
|
-
PetThermoTools/PhaseDiagrams.py,sha256=CqiZbP3R2DHTHXI6EYam91Lqz_CqrRjyxG01FlOUVHU,29847
|
12
|
-
PetThermoTools/Plotting.py,sha256=uvL_j2emMveGumLQ-IeJqyMXGUQT_PyInOpGnsWziAI,28992
|
13
|
-
PetThermoTools/Saturation.py,sha256=m014Wtcd6pT8qmQhUoQQczZEB6dX3iIdxsCGtxmuOh0,29806
|
14
|
-
PetThermoTools/__init__.py,sha256=PbiwQj_mNNEwuIZOLETmtMMshiXa50wjCA6mfvpOpOs,2393
|
15
|
-
PetThermoTools/_version.py,sha256=WvrDLHqvUFROTO0ipm6xZxsmiXhJ3Pibqtgbf_NAJkM,296
|
16
|
-
PetThermoTools-0.2.41.dist-info/LICENSE.txt,sha256=-mkx4iEw8Pk1RZUvncBhGLW87Uur5JB7FBQtOmX-VP0,1752
|
17
|
-
PetThermoTools-0.2.41.dist-info/METADATA,sha256=DQ0lx7W5xgUFICbzLIyjAzANmDoHfdU1bPe4Op9Jh2U,794
|
18
|
-
PetThermoTools-0.2.41.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
19
|
-
PetThermoTools-0.2.41.dist-info/top_level.txt,sha256=IqK8iYBR3YJozzMOTRZ8x8mU2k6x8ycoMBxZTm-I06U,15
|
20
|
-
PetThermoTools-0.2.41.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|