peridoxia 2.1__tar.gz
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.
- peridoxia-2.1/LICENSE +21 -0
- peridoxia-2.1/PKG-INFO +32 -0
- peridoxia-2.1/README.md +7 -0
- peridoxia-2.1/peridoxia/PTF_functions.py +53 -0
- peridoxia-2.1/peridoxia/__init__.py +17 -0
- peridoxia-2.1/peridoxia/cation_equilibration_functions.py +154 -0
- peridoxia-2.1/peridoxia/data_structures.py +2143 -0
- peridoxia-2.1/peridoxia/input_composition.py +228 -0
- peridoxia-2.1/peridoxia/main.py +1102 -0
- peridoxia-2.1/peridoxia/plotting_and_saving.py +131 -0
- peridoxia-2.1/peridoxia/reactions.py +593 -0
- peridoxia-2.1/peridoxia/setup_composition_parameters.py +172 -0
- peridoxia-2.1/peridoxia/setup_partitioning_constraints.py +335 -0
- peridoxia-2.1/peridoxia.egg-info/PKG-INFO +32 -0
- peridoxia-2.1/peridoxia.egg-info/SOURCES.txt +19 -0
- peridoxia-2.1/peridoxia.egg-info/dependency_links.txt +1 -0
- peridoxia-2.1/peridoxia.egg-info/requires.txt +11 -0
- peridoxia-2.1/peridoxia.egg-info/top_level.txt +1 -0
- peridoxia-2.1/pyproject.toml +41 -0
- peridoxia-2.1/setup.cfg +4 -0
- peridoxia-2.1/tests/test_peridoxia.py +169 -0
peridoxia-2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [Suzanne Birner]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
peridoxia-2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: peridoxia
|
|
3
|
+
Version: 2.1
|
|
4
|
+
Summary: Package for assessing redox during mantle melting
|
|
5
|
+
Author-email: "Suzanne K. Birner" <birners@berea.edu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/skbirner/peridoxia
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: <3.11,>=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: deprecation>=2.1.0
|
|
14
|
+
Requires-Dist: matplotlib>=3.6.2
|
|
15
|
+
Requires-Dist: nptyping
|
|
16
|
+
Requires-Dist: numdifftools
|
|
17
|
+
Requires-Dist: openpyxl
|
|
18
|
+
Requires-Dist: pandas>=1.5.2
|
|
19
|
+
Requires-Dist: pytest>=7.2.0
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0.0
|
|
21
|
+
Requires-Dist: scipy>=1.9.3
|
|
22
|
+
Requires-Dist: seaborn>=0.12.1
|
|
23
|
+
Requires-Dist: thermoengine>=2.0.0.dev10
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Peridoxia
|
|
27
|
+
|
|
28
|
+
Peridoxia is an empirical partitioning model designed to assess redox evolution during mantle melting in the shallow garnet and spinel fields. It uses natural and experimental results to constrain ferric iron partitioning between mantle phases. It was first developed by Suzanne Birner (birners AT berea.edu) in 2024 for the manuscript "Deep, hot, ancient melting recorded by ultralow oxygen fugacity in peridotites" (https://www.nature.com/articles/s41586-024-07603-w)
|
|
29
|
+
|
|
30
|
+
Documentation and Examples may be foud here: https://peridoxia.readthedocs.io/en/latest/
|
|
31
|
+
|
|
32
|
+
This package requires a virtual environment with python 3.10. For help with installation in a virtual environment, please see the Installation Guide: https://peridoxia.readthedocs.io/en/latest/installation.html.
|
peridoxia-2.1/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Peridoxia
|
|
2
|
+
|
|
3
|
+
Peridoxia is an empirical partitioning model designed to assess redox evolution during mantle melting in the shallow garnet and spinel fields. It uses natural and experimental results to constrain ferric iron partitioning between mantle phases. It was first developed by Suzanne Birner (birners AT berea.edu) in 2024 for the manuscript "Deep, hot, ancient melting recorded by ultralow oxygen fugacity in peridotites" (https://www.nature.com/articles/s41586-024-07603-w)
|
|
4
|
+
|
|
5
|
+
Documentation and Examples may be foud here: https://peridoxia.readthedocs.io/en/latest/
|
|
6
|
+
|
|
7
|
+
This package requires a virtual environment with python 3.10. For help with installation in a virtual environment, please see the Installation Guide: https://peridoxia.readthedocs.io/en/latest/installation.html.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# PTF_Functions
|
|
2
|
+
|
|
3
|
+
# The model uses the equations of Langmuir, Klein, and Plank (1992) to calculate Pressure (P), Temperature (T), and Melt Fraction (F) along an adiabatic decompression path.
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
def PTF_geotherm(
|
|
8
|
+
TK_pot: float=1350+273.15,
|
|
9
|
+
PGPa_range: list[float]=[4.,0.1],
|
|
10
|
+
P_step: float=-0.01,
|
|
11
|
+
melting: bool=True):
|
|
12
|
+
|
|
13
|
+
'''Calculates a set of PT values along a geotherm from a given potential temperature
|
|
14
|
+
Equations are from Langmuir, Klein, and Plank (1992)'''
|
|
15
|
+
|
|
16
|
+
PGPa_all = np.arange(PGPa_range[0], PGPa_range[1], P_step)
|
|
17
|
+
|
|
18
|
+
TK_all = np.zeros_like(PGPa_all)
|
|
19
|
+
F_all = np.zeros_like(PGPa_all)
|
|
20
|
+
|
|
21
|
+
if melting==False:
|
|
22
|
+
|
|
23
|
+
# 1°C/kbar is the value given in LKP92
|
|
24
|
+
TK_all = 1*(PGPa_all*10) + TK_pot
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
|
|
28
|
+
# Calculate P at the solidus (intersection of solidus and adiabat)
|
|
29
|
+
PGPa_sol = ((TK_pot-273.15) - 1118)/117
|
|
30
|
+
if PGPa_sol > PGPa_range[0]:
|
|
31
|
+
raise ValueError(f"ERROR! Solidus pressure {PGPa_sol:.2f} is higher than starting pressure! {PGPa_range[0]:.2f}")
|
|
32
|
+
|
|
33
|
+
# Constants for calculating P
|
|
34
|
+
a = 88*12/3.5
|
|
35
|
+
k = 88*0.95/3.5
|
|
36
|
+
|
|
37
|
+
# Calculate T and F at each P
|
|
38
|
+
for idx, PGPa in enumerate(PGPa_all):
|
|
39
|
+
|
|
40
|
+
if PGPa >= PGPa_sol:
|
|
41
|
+
TK_all[idx] = 1*(PGPa*10) + TK_pot
|
|
42
|
+
F_all[idx] = 0.
|
|
43
|
+
|
|
44
|
+
else:
|
|
45
|
+
|
|
46
|
+
F = a*np.log((PGPa-k)/(PGPa_sol-k))
|
|
47
|
+
TC_sol_P = 1118 + 127*PGPa
|
|
48
|
+
TC = TC_sol_P + 3.5*F/(1+PGPa/4.5)
|
|
49
|
+
|
|
50
|
+
F_all[idx] = F
|
|
51
|
+
TK_all[idx] = TC + 273.15
|
|
52
|
+
|
|
53
|
+
return PGPa_all, TK_all, F_all
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from . import PTF_functions
|
|
2
|
+
from . import plotting_and_saving
|
|
3
|
+
|
|
4
|
+
from . import setup_composition_parameters
|
|
5
|
+
from . import data_structures
|
|
6
|
+
from . import setup_partitioning_constraints
|
|
7
|
+
from . import reactions
|
|
8
|
+
from . import cation_equilibration_functions
|
|
9
|
+
from . import input_composition
|
|
10
|
+
|
|
11
|
+
from . import main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
###-----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
### Equilibration Reactions
|
|
3
|
+
###-----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
### These functions adjust cations in each phase via exchange reactions in order to fulfill the partitioning constraints defined in setup_partitioning_constraints.py
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import copy
|
|
9
|
+
import scipy.optimize as opt
|
|
10
|
+
|
|
11
|
+
from peridoxia import setup_partitioning_constraints as partitioning
|
|
12
|
+
from peridoxia import data_structures as data_struct
|
|
13
|
+
from peridoxia import reactions
|
|
14
|
+
|
|
15
|
+
### Exchange Reactions
|
|
16
|
+
|
|
17
|
+
# Olv-Opx-Cpx exchange (included in all models)
|
|
18
|
+
olv_opx_cpx_exchange = [
|
|
19
|
+
# 3+ cation exchange
|
|
20
|
+
reactions.define_reaction(reactants={'opx_CaFeAlSiO6':1, 'cpx_CaAlAlSiO6':1}, products={'opx_CaAlAlSiO6':1,'cpx_CaFeAlSiO6':1}), #Fe3-Al, opx-cpx
|
|
21
|
+
reactions.define_reaction(reactants={'opx_CaCrAlSiO6':1, 'cpx_CaAlAlSiO6':1}, products={'opx_CaAlAlSiO6':1,'cpx_CaCrAlSiO6':1}), #Cr-Al, opx-cpx
|
|
22
|
+
# 2+ cation exchange
|
|
23
|
+
reactions.define_reaction(reactants={'olv_Mg2SiO4':1, 'opx_Fe2Si2O6':1}, products={'olv_Fe2SiO4':1, 'opx_Mg2Si2O6':1}), #Fe2-Mg, olv-opx
|
|
24
|
+
reactions.define_reaction(reactants={'olv_Mg2SiO4':1, 'cpx_Fe2Si2O6':1}, products={'olv_Fe2SiO4':1, 'cpx_Mg2Si2O6':1}), #Fe2-Mg, olv-cpx
|
|
25
|
+
reactions.define_reaction(reactants={'cpx_Mg2Si2O6':1, 'opx_CaMgSi2O6':1}, products={'cpx_CaMgSi2O6':1, 'opx_Mg2Si2O6':1}), #Ca-Mg, cpx-opx
|
|
26
|
+
# Ts exchange
|
|
27
|
+
reactions.define_reaction(reactants={'opx_CaAlAlSiO6':1, 'cpx_CaMgSi2O6':1}, products={'opx_CaMgSi2O6':1, 'cpx_CaAlAlSiO6':1}), #AlAl-MgSi, opx-cpx
|
|
28
|
+
reactions.define_reaction(reactants={'spl_MgAl2O4':1, 'opx_Mg2Si2O6':1}, products={'olv_Mg2SiO4':1, 'opx_MgAlAlSiO6':1}) #spl+opx <-> ol+Ts-opx
|
|
29
|
+
]
|
|
30
|
+
spl_exchange = [
|
|
31
|
+
# 3+ cation exchange
|
|
32
|
+
reactions.define_reaction(reactants={'spl_FeFe2O4':1,'opx_FeAlAlSiO6':2}, products={'spl_FeAl2O4':1,'opx_FeFeAlSiO6':2}), #Fe3-Al, spl-opx
|
|
33
|
+
reactions.define_reaction(reactants={'spl_FeCr2O4':1, 'opx_FeAlAlSiO6':2}, products={'spl_FeAl2O4':1,'opx_FeCrAlSiO6':2}), #Cr-Al, spl-opx
|
|
34
|
+
# 2+ cation exchange
|
|
35
|
+
reactions.define_reaction(reactants={'spl_FeAl2O4':1, 'opx_MgAlAlSiO6':1}, products={'spl_MgAl2O4':1, 'opx_FeAlAlSiO6':1}), #Fe2-Mg, spl-opx
|
|
36
|
+
]
|
|
37
|
+
gt_exchange = [
|
|
38
|
+
# 3+ cation exchange
|
|
39
|
+
reactions.define_reaction(reactants={'gt_Ca3Fe2Si3O12':1, 'opx_CaAlAlSiO6':2}, products={'gt_Ca3Al2Si3O12':1, 'opx_CaFeAlSiO6':2}), #Fe3-Al, gt-opx
|
|
40
|
+
reactions.define_reaction(reactants={'gt_Ca3Cr2Si3O12':1, 'opx_CaAlAlSiO6':2}, products={'gt_Ca3Al2Si3O12':1, 'opx_CaCrAlSiO6':2}), #Cr-Al, gt-cpx
|
|
41
|
+
# 2+ cation exchange
|
|
42
|
+
reactions.define_reaction(reactants={'gt_Fe3Al2Si3O12':2, 'opx_Mg2Si2O6':3}, products={'gt_Mg3Al2Si3O12':2, 'opx_Fe2Si2O6':3}), #Fe2-Mg, gt-opx
|
|
43
|
+
reactions.define_reaction(reactants={'gt_Ca3Al2Si3O12':2, 'opx_Mg2Si2O6':3}, products={'gt_Mg3Al2Si3O12':2, 'opx_Ca2Si2O6':3}), #Ca-Mg, gt-cpx
|
|
44
|
+
]
|
|
45
|
+
liq_exchange = [
|
|
46
|
+
reactions.define_reaction(reactants={'opx_CaFeAlSiO6':2, 'liq_Al2O3':1}, products={'opx_CaAlAlSiO6':2, 'liq_Fe2O3':1}), #Fe3-Al, liq-opx
|
|
47
|
+
reactions.define_reaction(reactants={'opx_CaCrAlSiO6':2, 'liq_Al2O3':1}, products={'opx_CaAlAlSiO6':2, 'liq_Cr2O3':1}), #Cr-Al, liq-opx
|
|
48
|
+
reactions.define_reaction(reactants={'opx_Mg2Si2O6':1, 'liq_FeO':2}, products={'opx_Fe2Si2O6':1, 'liq_MgO':2}), #Fe2-Mg, liq-opx
|
|
49
|
+
reactions.define_reaction(reactants={'opx_CaAlAlSiO6':1, 'liq_MgO':1, 'liq_SiO2':1}, products={'opx_CaMgSi2O6':1, 'liq_Al2O3':1}), #AlAl-MgSi, opx-liq
|
|
50
|
+
]
|
|
51
|
+
all_exchange = np.concatenate((olv_opx_cpx_exchange,
|
|
52
|
+
spl_exchange,
|
|
53
|
+
gt_exchange,
|
|
54
|
+
liq_exchange)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### Offset Function
|
|
59
|
+
def get_offset(partitioning_constraints: list, assemblage: data_struct.Assemblage, reactions: np.ndarray, amts: np.ndarray):
|
|
60
|
+
'''Calculates offset between actual partitioning and desired partitioning.
|
|
61
|
+
Goal of Solver function is to make each offset zero.
|
|
62
|
+
Input list is a list of partitioning constraint function names (strings)'''
|
|
63
|
+
|
|
64
|
+
EQ_assemblage = Exchange_Cations(assemblage, reactions, amts)
|
|
65
|
+
|
|
66
|
+
partitioning_calculator = partitioning.PartitioningConstraintCalculator(EQ_assemblage)
|
|
67
|
+
|
|
68
|
+
offset_array = np.zeros(len(partitioning_constraints))
|
|
69
|
+
for idx, constraint in enumerate(partitioning_constraints):
|
|
70
|
+
partitioning_result = getattr(partitioning_calculator, constraint)() # assesses each partitioning constraint
|
|
71
|
+
offset_array[idx] = partitioning_result[2]
|
|
72
|
+
|
|
73
|
+
return offset_array
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
### Equilibration/Exchange Functions
|
|
77
|
+
def Equilibrate_Cations(assemblage: data_struct.Assemblage, reactions, display=False):
|
|
78
|
+
'''This function finds a solution for the amount of exchange reactions between minerals necessary to reach the required partitioning.
|
|
79
|
+
Input reactions are a set of reaction coefficients for each extensive endmember.'''
|
|
80
|
+
|
|
81
|
+
def fT(amts):
|
|
82
|
+
# Allow amount of each reaction to vary
|
|
83
|
+
|
|
84
|
+
### Determine which partitioning constraints are necessary
|
|
85
|
+
|
|
86
|
+
constraints = []
|
|
87
|
+
|
|
88
|
+
# Olv-Opx-Cpx constraints (all models)
|
|
89
|
+
constraints+=partitioning.partitioning_constraints_ol_opx_cpx
|
|
90
|
+
|
|
91
|
+
### TODO: Think about these. Logic might not be correct for liq/spl/gt field interactions
|
|
92
|
+
|
|
93
|
+
# Liq constraints
|
|
94
|
+
if assemblage.get_phase_mass('liq') > 0:
|
|
95
|
+
constraints+=partitioning.partitioning_constraints_liq
|
|
96
|
+
# Garnet-field constraints
|
|
97
|
+
if assemblage.get_phase_mass('gt') > 0: # & (modes['spl'] == 0):
|
|
98
|
+
constraints+=partitioning.partitioning_constraints_gt
|
|
99
|
+
# Spinel-field constraints:
|
|
100
|
+
if assemblage.get_phase_mass('spl') > 0: #constraints.append(partitioning_constraints_liq)
|
|
101
|
+
constraints+=partitioning.partitioning_constraints_spl
|
|
102
|
+
|
|
103
|
+
offsets = get_offset(constraints, assemblage, reactions, amts)
|
|
104
|
+
|
|
105
|
+
if display==True:
|
|
106
|
+
print(offsets)
|
|
107
|
+
|
|
108
|
+
# Make sure number of variables = number of constraints
|
|
109
|
+
if len(amts) != len(offsets):
|
|
110
|
+
print("ERROR! Number of variables (exchange reactions) does not equal number of constraints (partitioning relationships)!")
|
|
111
|
+
print("Exchange Reactions:")
|
|
112
|
+
for amt in amts:
|
|
113
|
+
print(amt)
|
|
114
|
+
print("Offsets:")
|
|
115
|
+
for val in offsets:
|
|
116
|
+
print(val)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
return offsets.ravel()
|
|
120
|
+
|
|
121
|
+
# Set an initial guess of no reaction
|
|
122
|
+
initial_guess = np.zeros(len(reactions))
|
|
123
|
+
|
|
124
|
+
# Minimize fT() function
|
|
125
|
+
# (i.e., we want the D offsets to be as small as possible)
|
|
126
|
+
solution = opt.least_squares(fT, initial_guess)
|
|
127
|
+
|
|
128
|
+
amts_array = solution.x
|
|
129
|
+
residuals = solution.fun
|
|
130
|
+
|
|
131
|
+
for residual in residuals:
|
|
132
|
+
if abs(residual) > 1e-7:
|
|
133
|
+
print(f"Residual is not zero! Residual = {residual}")
|
|
134
|
+
|
|
135
|
+
return amts_array, residuals
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def Exchange_Cations(assemblage: data_struct.Assemblage, reactions: np.ndarray, amts: np.ndarray):
|
|
139
|
+
'''Applies the indicated amount of each reaction to the starting compositions.
|
|
140
|
+
Reactions are a set of reaction coefficients for each extensive endmember.
|
|
141
|
+
Amts is an array of the amount to apply each reaction'''
|
|
142
|
+
|
|
143
|
+
# Initial amount of each extensive endmember
|
|
144
|
+
ext_end_array = assemblage.extensive_endmember_array
|
|
145
|
+
|
|
146
|
+
adjustment = np.dot(amts,reactions)
|
|
147
|
+
|
|
148
|
+
# New amounts of each extensive endmember
|
|
149
|
+
new_endmember_array = ext_end_array + adjustment
|
|
150
|
+
|
|
151
|
+
new_assemblage = copy.deepcopy(assemblage)
|
|
152
|
+
new_assemblage.extensive_endmember_array = new_endmember_array
|
|
153
|
+
|
|
154
|
+
return new_assemblage
|