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 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.
@@ -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