pychnosz 1.1.12__cp310-cp310-macosx_15_0_x86_64.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.
- pychnosz/.dylibs/libgcc_s.1.1.dylib +0 -0
- pychnosz/.dylibs/libgfortran.5.dylib +0 -0
- pychnosz/.dylibs/libquadmath.0.dylib +0 -0
- pychnosz/__init__.py +129 -0
- pychnosz/_version.py +34 -0
- pychnosz/biomolecules/__init__.py +29 -0
- pychnosz/biomolecules/ionize_aa.py +197 -0
- pychnosz/biomolecules/proteins.py +595 -0
- pychnosz/core/__init__.py +46 -0
- pychnosz/core/affinity.py +1256 -0
- pychnosz/core/animation.py +593 -0
- pychnosz/core/balance.py +334 -0
- pychnosz/core/basis.py +716 -0
- pychnosz/core/diagram.py +3336 -0
- pychnosz/core/equilibrate.py +813 -0
- pychnosz/core/equilibrium.py +554 -0
- pychnosz/core/info.py +821 -0
- pychnosz/core/retrieve.py +364 -0
- pychnosz/core/speciation.py +580 -0
- pychnosz/core/species.py +599 -0
- pychnosz/core/subcrt.py +1696 -0
- pychnosz/core/thermo.py +593 -0
- pychnosz/core/unicurve.py +1226 -0
- pychnosz/data/__init__.py +11 -0
- pychnosz/data/add_obigt.py +327 -0
- pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
- pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
- pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
- pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
- pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
- pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
- pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
- pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
- pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
- pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
- pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
- pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
- pychnosz/data/extdata/Berman/sympy.R +99 -0
- pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
- pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
- pychnosz/data/extdata/OBIGT/AD.csv +25 -0
- pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
- pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
- pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
- pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
- pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
- pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
- pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
- pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
- pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
- pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
- pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
- pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
- pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
- pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
- pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
- pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
- pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
- pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
- pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
- pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
- pychnosz/data/extdata/misc/BZA10.csv +5 -0
- pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
- pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
- pychnosz/data/extdata/misc/LA19_test.csv +7 -0
- pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
- pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
- pychnosz/data/extdata/misc/PM90.csv +7 -0
- pychnosz/data/extdata/misc/RH95.csv +23 -0
- pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
- pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
- pychnosz/data/extdata/misc/SK95.csv +55 -0
- pychnosz/data/extdata/misc/SOJSH.csv +61 -0
- pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
- pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
- pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
- pychnosz/data/extdata/misc/bluered.txt +1000 -0
- pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
- pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
- pychnosz/data/extdata/protein/Cas/download.R +34 -0
- pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
- pychnosz/data/extdata/protein/POLG.csv +12 -0
- pychnosz/data/extdata/protein/TBD+05.csv +393 -0
- pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
- pychnosz/data/extdata/protein/rubisco.csv +28 -0
- pychnosz/data/extdata/protein/rubisco.fasta +239 -0
- pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
- pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
- pychnosz/data/extdata/src/README.txt +5 -0
- pychnosz/data/extdata/taxonomy/names.dmp +215 -0
- pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
- pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
- pychnosz/data/extdata/thermo/buffer.csv +40 -0
- pychnosz/data/extdata/thermo/element.csv +135 -0
- pychnosz/data/extdata/thermo/groups.csv +6 -0
- pychnosz/data/extdata/thermo/opt.csv +2 -0
- pychnosz/data/extdata/thermo/protein.csv +506 -0
- pychnosz/data/extdata/thermo/refs.csv +343 -0
- pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
- pychnosz/data/loader.py +431 -0
- pychnosz/data/mod_obigt.py +322 -0
- pychnosz/data/obigt.py +471 -0
- pychnosz/data/worm.py +228 -0
- pychnosz/fortran/.gitignore +6 -0
- pychnosz/fortran/__init__.py +16 -0
- pychnosz/fortran/h2o92.dylib +0 -0
- pychnosz/fortran/h2o92_interface.py +527 -0
- pychnosz/geochemistry/__init__.py +21 -0
- pychnosz/geochemistry/minerals.py +514 -0
- pychnosz/geochemistry/redox.py +500 -0
- pychnosz/models/__init__.py +47 -0
- pychnosz/models/archer_wang.py +165 -0
- pychnosz/models/berman.py +309 -0
- pychnosz/models/cgl.py +381 -0
- pychnosz/models/dew.py +997 -0
- pychnosz/models/hkf.py +523 -0
- pychnosz/models/hkf_helpers.py +231 -0
- pychnosz/models/iapws95.py +1113 -0
- pychnosz/models/supcrt92_fortran.py +238 -0
- pychnosz/models/water.py +480 -0
- pychnosz/utils/__init__.py +27 -0
- pychnosz/utils/expression.py +1074 -0
- pychnosz/utils/formula.py +830 -0
- pychnosz/utils/formula_ox.py +227 -0
- pychnosz/utils/reset.py +33 -0
- pychnosz/utils/units.py +259 -0
- pychnosz-1.1.12.dist-info/METADATA +197 -0
- pychnosz-1.1.12.dist-info/RECORD +133 -0
- pychnosz-1.1.12.dist-info/WHEEL +5 -0
- pychnosz-1.1.12.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Berman mineral equations of state implementation.
|
|
3
|
+
|
|
4
|
+
Calculate thermodynamic properties of minerals using equations from:
|
|
5
|
+
Berman, R. G. (1988) Internally-consistent thermodynamic data for minerals
|
|
6
|
+
in the system Na2O-K2O-CaO-MgO-FeO-Fe2O3-Al2O3-SiO2-TiO2-H2O-CO2.
|
|
7
|
+
J. Petrol. 29, 445-522. https://doi.org/10.1093/petrology/29.2.445
|
|
8
|
+
|
|
9
|
+
This is a 1:1 Python replica of CHNOSZ-main/R/Berman.R
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import math
|
|
15
|
+
from typing import Union, List, Optional
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
from ..core.thermo import thermo
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def Berman(name: str, T: Union[float, List[float]] = 298.15, P: Union[float, List[float]] = 1,
|
|
22
|
+
check_G: bool = False, calc_transition: bool = True, calc_disorder: bool = True) -> pd.DataFrame:
|
|
23
|
+
"""
|
|
24
|
+
Calculate thermodynamic properties of minerals using Berman equations.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
name : str
|
|
29
|
+
Name of the mineral
|
|
30
|
+
T : float or list, optional
|
|
31
|
+
Temperature in Kelvin (default: 298.15)
|
|
32
|
+
P : float or list, optional
|
|
33
|
+
Pressure in bar (default: 1)
|
|
34
|
+
check_G : bool, optional
|
|
35
|
+
Check consistency of G in data file (default: False)
|
|
36
|
+
calc_transition : bool, optional
|
|
37
|
+
Calculate polymorphic transition contributions (default: True)
|
|
38
|
+
calc_disorder : bool, optional
|
|
39
|
+
Calculate disorder contributions (default: True)
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
pd.DataFrame
|
|
44
|
+
DataFrame with columns T, P, G, H, S, Cp, V
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Reference temperature and pressure
|
|
48
|
+
Pr = 1
|
|
49
|
+
Tr = 298.15
|
|
50
|
+
|
|
51
|
+
# Make T and P the same length
|
|
52
|
+
if isinstance(T, (int, float)):
|
|
53
|
+
T = [T]
|
|
54
|
+
if isinstance(P, (int, float)):
|
|
55
|
+
P = [P]
|
|
56
|
+
|
|
57
|
+
# Convert to list if numpy array (to avoid element-wise multiplication bug)
|
|
58
|
+
if isinstance(T, np.ndarray):
|
|
59
|
+
T = T.tolist()
|
|
60
|
+
if isinstance(P, np.ndarray):
|
|
61
|
+
P = P.tolist()
|
|
62
|
+
|
|
63
|
+
ncond = max(len(T), len(P))
|
|
64
|
+
T = np.array(T * (ncond // len(T) + 1), dtype=float)[:ncond]
|
|
65
|
+
P = np.array(P * (ncond // len(P) + 1), dtype=float)[:ncond]
|
|
66
|
+
|
|
67
|
+
# Get parameters in the Berman equations
|
|
68
|
+
# Start with thermodynamic parameters provided with CHNOSZ
|
|
69
|
+
thermo_sys = thermo()
|
|
70
|
+
if thermo_sys.Berman is None:
|
|
71
|
+
raise RuntimeError("Berman data not loaded. Please run pychnosz.reset() first.")
|
|
72
|
+
|
|
73
|
+
dat = thermo_sys.Berman.copy()
|
|
74
|
+
|
|
75
|
+
# TODO: Handle user-supplied data file (thermo()$opt$Berman)
|
|
76
|
+
# For now, just use the default data
|
|
77
|
+
|
|
78
|
+
# Remove duplicates (only the first, i.e. most recent entry is kept)
|
|
79
|
+
dat = dat.drop_duplicates(subset=['name'], keep='first')
|
|
80
|
+
|
|
81
|
+
# Remove the multipliers on volume parameters
|
|
82
|
+
vcols = ['v1', 'v2', 'v3', 'v4'] # columns with v1, v2, v3, v4
|
|
83
|
+
multexp = [5, 5, 5, 8]
|
|
84
|
+
for i, col in enumerate(vcols):
|
|
85
|
+
if col in dat.columns:
|
|
86
|
+
dat[col] = dat[col] / (10 ** multexp[i])
|
|
87
|
+
|
|
88
|
+
# Which row has data for this mineral?
|
|
89
|
+
matching_rows = dat[dat['name'] == name]
|
|
90
|
+
if len(matching_rows) == 0:
|
|
91
|
+
raise ValueError(f"Data for {name} not available in Berman database")
|
|
92
|
+
|
|
93
|
+
dat_mineral = matching_rows.iloc[0]
|
|
94
|
+
|
|
95
|
+
# Extract parameters for easier access
|
|
96
|
+
GfPrTr = dat_mineral['GfPrTr']
|
|
97
|
+
HfPrTr = dat_mineral['HfPrTr']
|
|
98
|
+
SPrTr = dat_mineral['SPrTr']
|
|
99
|
+
VPrTr = dat_mineral['VPrTr']
|
|
100
|
+
|
|
101
|
+
k0 = dat_mineral['k0']
|
|
102
|
+
k1 = dat_mineral['k1']
|
|
103
|
+
k2 = dat_mineral['k2']
|
|
104
|
+
k3 = dat_mineral['k3']
|
|
105
|
+
k4 = dat_mineral['k4'] if not pd.isna(dat_mineral['k4']) else 0
|
|
106
|
+
k5 = dat_mineral['k5'] if not pd.isna(dat_mineral['k5']) else 0
|
|
107
|
+
k6 = dat_mineral['k6'] if not pd.isna(dat_mineral['k6']) else 0
|
|
108
|
+
|
|
109
|
+
v1 = dat_mineral['v1'] if not pd.isna(dat_mineral['v1']) else 0
|
|
110
|
+
v2 = dat_mineral['v2'] if not pd.isna(dat_mineral['v2']) else 0
|
|
111
|
+
v3 = dat_mineral['v3'] if not pd.isna(dat_mineral['v3']) else 0
|
|
112
|
+
v4 = dat_mineral['v4'] if not pd.isna(dat_mineral['v4']) else 0
|
|
113
|
+
|
|
114
|
+
# Transition parameters
|
|
115
|
+
Tlambda = dat_mineral['Tlambda'] if not pd.isna(dat_mineral['Tlambda']) else None
|
|
116
|
+
Tref = dat_mineral['Tref'] if not pd.isna(dat_mineral['Tref']) else None
|
|
117
|
+
dTdP = dat_mineral['dTdP'] if not pd.isna(dat_mineral['dTdP']) else None
|
|
118
|
+
l1 = dat_mineral['l1'] if not pd.isna(dat_mineral['l1']) else None
|
|
119
|
+
l2 = dat_mineral['l2'] if not pd.isna(dat_mineral['l2']) else None
|
|
120
|
+
|
|
121
|
+
# Disorder parameters
|
|
122
|
+
Tmin = dat_mineral['Tmin'] if not pd.isna(dat_mineral['Tmin']) else None
|
|
123
|
+
Tmax = dat_mineral['Tmax'] if not pd.isna(dat_mineral['Tmax']) else None
|
|
124
|
+
d0 = dat_mineral['d0'] if not pd.isna(dat_mineral['d0']) else None
|
|
125
|
+
d1 = dat_mineral['d1'] if not pd.isna(dat_mineral['d1']) else None
|
|
126
|
+
d2 = dat_mineral['d2'] if not pd.isna(dat_mineral['d2']) else None
|
|
127
|
+
d3 = dat_mineral['d3'] if not pd.isna(dat_mineral['d3']) else None
|
|
128
|
+
d4 = dat_mineral['d4'] if not pd.isna(dat_mineral['d4']) else None
|
|
129
|
+
Vad = dat_mineral['Vad'] if not pd.isna(dat_mineral['Vad']) else None
|
|
130
|
+
|
|
131
|
+
# Get the entropy of the elements using the chemical formula
|
|
132
|
+
# Get formula from OBIGT and calculate using entropy() function like in R CHNOSZ
|
|
133
|
+
SPrTr_elements = 0
|
|
134
|
+
if thermo_sys.obigt is not None:
|
|
135
|
+
obigt_match = thermo_sys.obigt[thermo_sys.obigt['name'] == name]
|
|
136
|
+
if len(obigt_match) > 0:
|
|
137
|
+
formula = obigt_match.iloc[0]['formula']
|
|
138
|
+
# Import entropy function and calculate SPrTr_elements properly
|
|
139
|
+
from ..utils.formula import entropy
|
|
140
|
+
SPrTr_elements = entropy(formula)
|
|
141
|
+
|
|
142
|
+
# Check that G in data file follows Benson-Helgeson convention
|
|
143
|
+
if check_G and not pd.isna(GfPrTr):
|
|
144
|
+
GfPrTr_calc = HfPrTr - Tr * (SPrTr - SPrTr_elements)
|
|
145
|
+
Gdiff = GfPrTr_calc - GfPrTr
|
|
146
|
+
if abs(Gdiff) >= 1000:
|
|
147
|
+
warnings.warn(f"{name}: GfPrTr(calc) - GfPrTr(table) is too big! == {round(Gdiff)} J/mol")
|
|
148
|
+
|
|
149
|
+
### Thermodynamic properties ###
|
|
150
|
+
# Calculate Cp and V (Berman, 1988 Eqs. 4 and 5)
|
|
151
|
+
# k4, k5, k6 terms from winTWQ documentation (doi:10.4095/223425)
|
|
152
|
+
Cp = k0 + k1 * T**(-0.5) + k2 * T**(-2) + k3 * T**(-3) + k4 * T**(-1) + k5 * T + k6 * T**2
|
|
153
|
+
|
|
154
|
+
P_Pr = P - Pr
|
|
155
|
+
T_Tr = T - Tr
|
|
156
|
+
V = VPrTr * (1 + v1 * T_Tr + v2 * T_Tr**2 + v3 * P_Pr + v4 * P_Pr**2)
|
|
157
|
+
|
|
158
|
+
# Calculate Ha (symbolically integrated using sympy - expressions not simplified)
|
|
159
|
+
intCp = (T*k0 - Tr*k0 + k2/Tr - k2/T + k3/(2*Tr**2) - k3/(2*T**2) + 2.0*k1*T**0.5 - 2.0*k1*Tr**0.5 +
|
|
160
|
+
k4*np.log(T) - k4*np.log(Tr) + k5*T**2/2 - k5*Tr**2/2 - k6*Tr**3/3 + k6*T**3/3)
|
|
161
|
+
|
|
162
|
+
intVminusTdVdT = (-VPrTr + P*(VPrTr + VPrTr*v4 - VPrTr*v3 - Tr*VPrTr*v1 + VPrTr*v2*Tr**2 - VPrTr*v2*T**2) +
|
|
163
|
+
P**2*(VPrTr*v3/2 - VPrTr*v4) + VPrTr*v3/2 - VPrTr*v4/3 + Tr*VPrTr*v1 +
|
|
164
|
+
VPrTr*v2*T**2 - VPrTr*v2*Tr**2 + VPrTr*v4*P**3/3)
|
|
165
|
+
|
|
166
|
+
Ha = HfPrTr + intCp + intVminusTdVdT
|
|
167
|
+
|
|
168
|
+
# Calculate S (also symbolically integrated)
|
|
169
|
+
intCpoverT = (k0*np.log(T) - k0*np.log(Tr) - k3/(3*T**3) + k3/(3*Tr**3) + k2/(2*Tr**2) - k2/(2*T**2) +
|
|
170
|
+
2.0*k1*Tr**(-0.5) - 2.0*k1*T**(-0.5) + k4/Tr - k4/T + T*k5 - Tr*k5 + k6*T**2/2 - k6*Tr**2/2)
|
|
171
|
+
|
|
172
|
+
intdVdT = -VPrTr*(v1 + v2*(-2*Tr + 2*T)) + P*VPrTr*(v1 + v2*(-2*Tr + 2*T))
|
|
173
|
+
|
|
174
|
+
S = SPrTr + intCpoverT - intdVdT
|
|
175
|
+
|
|
176
|
+
# Calculate Ga --> Berman-Brown convention (DG = DH - T*S, no S(element))
|
|
177
|
+
Ga = Ha - T * S
|
|
178
|
+
|
|
179
|
+
### Polymorphic transition properties ###
|
|
180
|
+
if (Tlambda is not None and Tref is not None and
|
|
181
|
+
not pd.isna(Tlambda) and not pd.isna(Tref) and
|
|
182
|
+
np.any(T > Tref) and calc_transition):
|
|
183
|
+
|
|
184
|
+
# Starting transition contributions are 0
|
|
185
|
+
Cptr = np.zeros(ncond)
|
|
186
|
+
Htr = np.zeros(ncond)
|
|
187
|
+
Str = np.zeros(ncond)
|
|
188
|
+
|
|
189
|
+
# Eq. 9: Tlambda at P
|
|
190
|
+
Tlambda_P = Tlambda + dTdP * (P - 1)
|
|
191
|
+
|
|
192
|
+
# Eq. 8a: Cp at P
|
|
193
|
+
Td = Tlambda - Tlambda_P
|
|
194
|
+
Tprime = T + Td
|
|
195
|
+
|
|
196
|
+
# With the condition that Tref < Tprime < Tlambda(1bar)
|
|
197
|
+
iTprime = (Tref < Tprime) & (Tprime < Tlambda)
|
|
198
|
+
# Handle NA values
|
|
199
|
+
iTprime = iTprime & ~np.isnan(Tprime)
|
|
200
|
+
|
|
201
|
+
if np.any(iTprime):
|
|
202
|
+
Tprime_valid = Tprime[iTprime]
|
|
203
|
+
Cptr[iTprime] = Tprime_valid * (l1 + l2 * Tprime_valid)**2
|
|
204
|
+
|
|
205
|
+
# We got Cp, now calculate the integrations for H and S
|
|
206
|
+
iTtr = T > Tref
|
|
207
|
+
if np.any(iTtr):
|
|
208
|
+
Ttr = T[iTtr].copy()
|
|
209
|
+
Tlambda_P_tr = Tlambda_P[iTtr].copy()
|
|
210
|
+
Td_tr = Td[iTtr] if hasattr(Td, '__len__') else np.full_like(Ttr, Td)
|
|
211
|
+
|
|
212
|
+
# Handle NA values
|
|
213
|
+
Tlambda_P_tr[np.isnan(Tlambda_P_tr)] = np.inf
|
|
214
|
+
|
|
215
|
+
# The upper integration limit is Tlambda_P
|
|
216
|
+
Ttr[Ttr >= Tlambda_P_tr] = Tlambda_P_tr[Ttr >= Tlambda_P_tr]
|
|
217
|
+
|
|
218
|
+
# Derived variables
|
|
219
|
+
tref = Tref - Td_tr
|
|
220
|
+
x1 = l1**2 * Td_tr + 2 * l1 * l2 * Td_tr**2 + l2**2 * Td_tr**3
|
|
221
|
+
x2 = l1**2 + 4 * l1 * l2 * Td_tr + 3 * l2**2 * Td_tr**2
|
|
222
|
+
x3 = 2 * l1 * l2 + 3 * l2**2 * Td_tr
|
|
223
|
+
x4 = l2**2
|
|
224
|
+
|
|
225
|
+
# Eqs. 10, 11, 12
|
|
226
|
+
Htr[iTtr] = (x1 * (Ttr - tref) + x2/2 * (Ttr**2 - tref**2) +
|
|
227
|
+
x3/3 * (Ttr**3 - tref**3) + x4/4 * (Ttr**4 - tref**4))
|
|
228
|
+
Str[iTtr] = (x1 * (np.log(Ttr) - np.log(tref)) + x2 * (Ttr - tref) +
|
|
229
|
+
x3/2 * (Ttr**2 - tref**2) + x4/3 * (Ttr**3 - tref**3))
|
|
230
|
+
|
|
231
|
+
Gtr = Htr - T * Str
|
|
232
|
+
|
|
233
|
+
# Apply the transition contributions
|
|
234
|
+
Ga = Ga + Gtr
|
|
235
|
+
Ha = Ha + Htr
|
|
236
|
+
S = S + Str
|
|
237
|
+
Cp = Cp + Cptr
|
|
238
|
+
|
|
239
|
+
### Disorder thermodynamic properties ###
|
|
240
|
+
if (Tmin is not None and Tmax is not None and
|
|
241
|
+
not pd.isna(Tmin) and not pd.isna(Tmax) and
|
|
242
|
+
np.any(T > Tmin) and calc_disorder):
|
|
243
|
+
|
|
244
|
+
# Starting disorder contributions are 0
|
|
245
|
+
Cpds = np.zeros(ncond)
|
|
246
|
+
Hds = np.zeros(ncond)
|
|
247
|
+
Sds = np.zeros(ncond)
|
|
248
|
+
Vds = np.zeros(ncond)
|
|
249
|
+
|
|
250
|
+
# The lower integration limit is Tmin
|
|
251
|
+
iTds = T > Tmin
|
|
252
|
+
if np.any(iTds):
|
|
253
|
+
Tds = T[iTds].copy()
|
|
254
|
+
# The upper integration limit is Tmax
|
|
255
|
+
Tds[Tds > Tmax] = Tmax
|
|
256
|
+
|
|
257
|
+
# Ber88 Eqs. 15, 16, 17
|
|
258
|
+
Cpds[iTds] = d0 + d1*Tds**(-0.5) + d2*Tds**(-2) + d3*Tds + d4*Tds**2
|
|
259
|
+
Hds[iTds] = (d0*(Tds - Tmin) + d1*(Tds**0.5 - Tmin**0.5)/0.5 +
|
|
260
|
+
d2*(Tds**(-1) - Tmin**(-1))/(-1) + d3*(Tds**2 - Tmin**2)/2 + d4*(Tds**3 - Tmin**3)/3)
|
|
261
|
+
Sds[iTds] = (d0*(np.log(Tds) - np.log(Tmin)) + d1*(Tds**(-0.5) - Tmin**(-0.5))/(-0.5) +
|
|
262
|
+
d2*(Tds**(-2) - Tmin**(-2))/(-2) + d3*(Tds - Tmin) + d4*(Tds**2 - Tmin**2)/2)
|
|
263
|
+
|
|
264
|
+
# Eq. 18; we can't do this if Vad == 0 (dolomite and gehlenite)
|
|
265
|
+
if Vad is not None and not pd.isna(Vad) and Vad != 0:
|
|
266
|
+
Vds = Hds / Vad
|
|
267
|
+
|
|
268
|
+
# Include the Vds term with Hds
|
|
269
|
+
Hds = Hds + Vds * (P - Pr)
|
|
270
|
+
|
|
271
|
+
# Disordering properties above Tmax (Eq. 20)
|
|
272
|
+
ihigh = T > Tmax
|
|
273
|
+
if np.any(ihigh):
|
|
274
|
+
Hds[ihigh] = Hds[ihigh] - (T[ihigh] - Tmax) * Sds[ihigh]
|
|
275
|
+
|
|
276
|
+
Gds = Hds - T * Sds
|
|
277
|
+
|
|
278
|
+
# Apply the disorder contributions
|
|
279
|
+
Ga = Ga + Gds
|
|
280
|
+
Ha = Ha + Hds
|
|
281
|
+
S = S + Sds
|
|
282
|
+
V = V + Vds
|
|
283
|
+
Cp = Cp + Cpds
|
|
284
|
+
|
|
285
|
+
### (for testing) Use G = H - TS to check that integrals for H and S are written correctly
|
|
286
|
+
Ga_fromHminusTS = Ha - T * S
|
|
287
|
+
if not np.allclose(Ga_fromHminusTS, Ga, atol=1e-6):
|
|
288
|
+
raise RuntimeError(f"{name}: incorrect integrals detected using DG = DH - T*S")
|
|
289
|
+
|
|
290
|
+
### Thermodynamic and unit conventions used in SUPCRT ###
|
|
291
|
+
# Use entropy of the elements in calculation of G --> Benson-Helgeson convention (DG = DH - T*DS)
|
|
292
|
+
Gf = Ga + Tr * SPrTr_elements
|
|
293
|
+
|
|
294
|
+
# The output will just have "G" and "H"
|
|
295
|
+
G = Gf
|
|
296
|
+
H = Ha
|
|
297
|
+
|
|
298
|
+
# Convert J/bar to cm^3/mol
|
|
299
|
+
V = V * 10
|
|
300
|
+
|
|
301
|
+
return pd.DataFrame({
|
|
302
|
+
'T': T,
|
|
303
|
+
'P': P,
|
|
304
|
+
'G': G,
|
|
305
|
+
'H': H,
|
|
306
|
+
'S': S,
|
|
307
|
+
'Cp': Cp,
|
|
308
|
+
'V': V
|
|
309
|
+
})
|
pychnosz/models/cgl.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CGL (Crystalline, Gas, Liquid) equation of state implementation.
|
|
3
|
+
|
|
4
|
+
This module implements equations of state for crystalline, gas, and liquid species
|
|
5
|
+
(except liquid water), based on the tested functions from HKF_cgl.py.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Helgeson, H.C. et al. (1978). Summary and critique of the thermodynamic properties
|
|
9
|
+
of rock-forming minerals. Am. J. Sci. 278-A.
|
|
10
|
+
- Berman, R.G. (1988). Internally-consistent thermodynamic data for minerals in the
|
|
11
|
+
system Na2O-K2O-CaO-MgO-FeO-Fe2O3-Al2O3-SiO2-TiO2-H2O-CO2. J. Petrol.
|
|
12
|
+
- R CHNOSZ cgl.R implementation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import numpy as np
|
|
17
|
+
import math
|
|
18
|
+
import copy
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
def convert_cm3bar(value):
|
|
22
|
+
return value*4.184 * 10
|
|
23
|
+
|
|
24
|
+
# CHNOSZ/cgl.R
|
|
25
|
+
# calculate standard thermodynamic properties of non-aqueous species
|
|
26
|
+
# 20060729 jmd
|
|
27
|
+
|
|
28
|
+
def cgl(property = None, parameters = None, T = 298.15, P = 1):
|
|
29
|
+
# calculate properties of crystalline, liquid (except H2O) and gas species
|
|
30
|
+
Tr = 298.15
|
|
31
|
+
Pr = 1
|
|
32
|
+
|
|
33
|
+
# Convert T and P to arrays for vectorized operations
|
|
34
|
+
T = np.atleast_1d(T)
|
|
35
|
+
P = np.atleast_1d(P)
|
|
36
|
+
|
|
37
|
+
# make T and P equal length
|
|
38
|
+
if P.size < T.size:
|
|
39
|
+
P = np.full_like(T, P[0] if P.size == 1 else P)
|
|
40
|
+
if T.size < P.size:
|
|
41
|
+
T = np.full_like(P, T[0] if T.size == 1 else T)
|
|
42
|
+
|
|
43
|
+
n_conditions = T.size
|
|
44
|
+
# initialize output dict
|
|
45
|
+
out_dict = dict()
|
|
46
|
+
# loop over each species
|
|
47
|
+
|
|
48
|
+
# Iterate over each row by position to handle duplicate indices properly
|
|
49
|
+
for i in range(len(parameters)):
|
|
50
|
+
# Get the index label for this row
|
|
51
|
+
k = parameters.index[i]
|
|
52
|
+
# Get the row data by position (iloc) to avoid duplicate index issues
|
|
53
|
+
PAR = parameters.iloc[i]
|
|
54
|
+
|
|
55
|
+
if PAR["state"] == "aq":
|
|
56
|
+
# For aqueous species processed by CGL, return NaN
|
|
57
|
+
# (they should be processed by HKF instead)
|
|
58
|
+
out_dict[k] = {p:float('NaN') for p in property}
|
|
59
|
+
else:
|
|
60
|
+
|
|
61
|
+
# OBIGT database stores G, H, S in calories (E_units = "cal")
|
|
62
|
+
# CGL calculations use calories (integrals intCpdT, intCpdlnT, intVdP are in cal)
|
|
63
|
+
# Results are output in calories and converted to J in subcrt.py at line 959
|
|
64
|
+
|
|
65
|
+
# Parameter scaling - SUPCRT92 data is already in correct units
|
|
66
|
+
# PAR["a2.b"] = copy.copy(PAR["a2.b"]*10**-3)
|
|
67
|
+
# PAR["a3.c"] = copy.copy(PAR["a3.c"]*10**5)
|
|
68
|
+
# PAR["c1.e"] = copy.copy(PAR["c1.e"]*10**-5)
|
|
69
|
+
|
|
70
|
+
# Check if this is a Berman mineral (columns 9-21 are all NA in R indexing)
|
|
71
|
+
# In Python/pandas, we check the relevant thermodynamic parameter columns
|
|
72
|
+
# NOTE: A mineral is only Berman if it LACKS standard thermodynamic data (G,H,S)
|
|
73
|
+
# If G,H,S are present, use regular CGL even if heat capacity coefficients are all zero
|
|
74
|
+
berman_cols = ['a1.a', 'a2.b', 'a3.c', 'a4.d', 'c1.e', 'c2.f', 'omega.lambda', 'z.T']
|
|
75
|
+
has_standard_thermo = pd.notna(PAR.get('G', np.nan)) and pd.notna(PAR.get('H', np.nan)) and pd.notna(PAR.get('S', np.nan))
|
|
76
|
+
all_coeffs_zero_or_na = all(pd.isna(PAR.get(col, np.nan)) or PAR.get(col, 0) == 0 for col in berman_cols)
|
|
77
|
+
is_berman_mineral = all_coeffs_zero_or_na and not has_standard_thermo
|
|
78
|
+
|
|
79
|
+
if is_berman_mineral:
|
|
80
|
+
# Use Berman equations (parameters not in thermo()$OBIGT)
|
|
81
|
+
from .berman import Berman
|
|
82
|
+
try:
|
|
83
|
+
# Berman is already vectorized - pass T and P arrays directly
|
|
84
|
+
properties_df = Berman(PAR["name"], T=T, P=P)
|
|
85
|
+
# Extract the requested properties as arrays
|
|
86
|
+
values = {}
|
|
87
|
+
for prop in property:
|
|
88
|
+
if prop in properties_df.columns:
|
|
89
|
+
# Get all values as an array
|
|
90
|
+
prop_values = properties_df[prop].values
|
|
91
|
+
|
|
92
|
+
# IMPORTANT: Berman function returns values in J/mol (Joules)
|
|
93
|
+
# but CGL returns values in cal/mol (calories)
|
|
94
|
+
# Convert Berman results from J/mol to cal/mol for consistency
|
|
95
|
+
# Energy properties that need conversion: G, H, S, Cp
|
|
96
|
+
# Volume (V) and other properties don't need conversion
|
|
97
|
+
energy_props = ['G', 'H', 'S', 'Cp']
|
|
98
|
+
if prop in energy_props:
|
|
99
|
+
# Convert J/mol to cal/mol by dividing by 4.184
|
|
100
|
+
prop_values = prop_values / 4.184
|
|
101
|
+
|
|
102
|
+
values[prop] = prop_values
|
|
103
|
+
else:
|
|
104
|
+
values[prop] = np.full(n_conditions, float('NaN'))
|
|
105
|
+
except Exception as e:
|
|
106
|
+
# If Berman calculation fails, fall back to NaN arrays
|
|
107
|
+
values = {prop: np.full(n_conditions, float('NaN')) for prop in property}
|
|
108
|
+
else:
|
|
109
|
+
# Use regular CGL equations
|
|
110
|
+
|
|
111
|
+
# in CHNOSZ, we have
|
|
112
|
+
# 1 cm^3 bar --> convert(1, "calories") == 0.02390057 cal
|
|
113
|
+
# but REAC92D.F in SUPCRT92 uses
|
|
114
|
+
cm3bar_to_cal = 0.023901488 # cal
|
|
115
|
+
# start with NA values
|
|
116
|
+
values = dict()
|
|
117
|
+
# a test for availability of heat capacity coefficients (a, b, c, d, e, f)
|
|
118
|
+
# based on the column assignments in thermo()$OBIGT
|
|
119
|
+
|
|
120
|
+
# Check for heat capacity coefficients, handling both NaN and non-numeric values
|
|
121
|
+
# Heat capacity coefficients are at positions 14-19 (a1.a through c2.f)
|
|
122
|
+
# Position 13 is V (volume), not a heat capacity coefficient
|
|
123
|
+
has_hc_coeffs = False
|
|
124
|
+
try:
|
|
125
|
+
hc_values = list(PAR.iloc[14:20])
|
|
126
|
+
has_hc_coeffs = any([pd.notna(p) and p != 0 for p in hc_values if pd.api.types.is_numeric_dtype(type(p))])
|
|
127
|
+
|
|
128
|
+
# DEBUG
|
|
129
|
+
if False and PAR["name"] == "rhomboclase":
|
|
130
|
+
print(f"DEBUG for rhomboclase:")
|
|
131
|
+
print(f" hc_values (iloc[14:20]): {hc_values}")
|
|
132
|
+
print(f" has_hc_coeffs: {has_hc_coeffs}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
has_hc_coeffs = False
|
|
135
|
+
|
|
136
|
+
if has_hc_coeffs:
|
|
137
|
+
# we have at least one of the heat capacity coefficients;
|
|
138
|
+
# zero out any NA's in the rest (leave lambda and T of transition (columns 20-21) alone)
|
|
139
|
+
for i in range(14, 20):
|
|
140
|
+
if pd.isna(PAR.iloc[i]) or not pd.api.types.is_numeric_dtype(type(PAR.iloc[i])):
|
|
141
|
+
PAR.iloc[i] = 0.0
|
|
142
|
+
# calculate the heat capacity and its integrals (vectorized)
|
|
143
|
+
Cp = PAR["a1.a"] + PAR["a2.b"]*T + PAR["a3.c"]*T**-2 + PAR["a4.d"]*T**-0.5 + PAR["c1.e"]*T**2 + PAR["c2.f"]*T**PAR["omega.lambda"]
|
|
144
|
+
intCpdT = PAR["a1.a"]*(T - Tr) + PAR["a2.b"]*(T**2 - Tr**2)/2 + PAR["a3.c"]*(1/T - 1/Tr)/-1 + PAR["a4.d"]*(T**0.5 - Tr**0.5)/0.5 + PAR["c1.e"]*(T**3-Tr**3)/3
|
|
145
|
+
intCpdlnT = PAR["a1.a"]*np.log(T / Tr) + PAR["a2.b"]*(T - Tr) + PAR["a3.c"]*(T**-2 - Tr**-2)/-2 + PAR["a4.d"]*(T**-0.5 - Tr**-0.5)/-0.5 + PAR["c1.e"]*(T**2 - Tr**2)/2
|
|
146
|
+
|
|
147
|
+
# do we also have the lambda parameter (Cp term with adjustable exponent on T)?
|
|
148
|
+
if pd.notna(PAR["omega.lambda"]) and PAR["omega.lambda"] != 0:
|
|
149
|
+
# equations for lambda adapted from Helgeson et al., 1998 (doi:10.1016/S0016-7037(97)00219-6)
|
|
150
|
+
if PAR["omega.lambda"] == -1:
|
|
151
|
+
intCpdT = intCpdT + PAR["c2.f"]*np.log(T/Tr)
|
|
152
|
+
else:
|
|
153
|
+
intCpdT = intCpdT - PAR["c2.f"]*( T**(PAR["omega.lambda"] + 1) - Tr**(PAR["omega.lambda"] + 1) ) / (PAR["omega.lambda"] + 1)
|
|
154
|
+
intCpdlnT = intCpdlnT + PAR["c2.f"]*(T**PAR["omega.lambda"] - Tr**PAR["omega.lambda"]) / PAR["omega.lambda"]
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
# use constant heat capacity if the coefficients are not available (vectorized)
|
|
158
|
+
# If Cp is NA/NaN, use 0 (matching R CHNOSZ behavior)
|
|
159
|
+
Cp_value = PAR["Cp"] if pd.notna(PAR["Cp"]) else 0.0
|
|
160
|
+
Cp = np.full(n_conditions, Cp_value)
|
|
161
|
+
intCpdT = Cp_value*(T - Tr)
|
|
162
|
+
intCpdlnT = Cp_value*np.log(T / Tr)
|
|
163
|
+
# in case Cp is listed as NA, set the integrals to 0 at Tr
|
|
164
|
+
at_Tr = (T == Tr)
|
|
165
|
+
intCpdT = np.where(at_Tr, 0, intCpdT)
|
|
166
|
+
intCpdlnT = np.where(at_Tr, 0, intCpdlnT)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# volume and its integrals (vectorized)
|
|
170
|
+
if PAR["name"] in ["quartz", "coesite"]:
|
|
171
|
+
# volume calculations for quartz and coesite
|
|
172
|
+
qtz = quartz_coesite(PAR, T, P)
|
|
173
|
+
V = qtz["V"]
|
|
174
|
+
intVdP = qtz["intVdP"]
|
|
175
|
+
intdVdTdP = qtz["intdVdTdP"]
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
# for other minerals, volume is constant (Helgeson et al., 1978)
|
|
179
|
+
V = np.full(n_conditions, PAR["V"])
|
|
180
|
+
# if the volume is NA, set its integrals to zero
|
|
181
|
+
if pd.isna(PAR["V"]):
|
|
182
|
+
intVdP = np.zeros(n_conditions)
|
|
183
|
+
intdVdTdP = np.zeros(n_conditions)
|
|
184
|
+
else:
|
|
185
|
+
intVdP = PAR["V"]*(P - Pr) * cm3bar_to_cal
|
|
186
|
+
intdVdTdP = np.zeros(n_conditions)
|
|
187
|
+
|
|
188
|
+
# get the values of each of the requested thermodynamic properties (vectorized)
|
|
189
|
+
for i,prop in enumerate(property):
|
|
190
|
+
if prop == "Cp": values["Cp"] = Cp
|
|
191
|
+
if prop == "V": values["V"] = V
|
|
192
|
+
if prop == "E": values["E"] = np.full(n_conditions, float('NaN'))
|
|
193
|
+
if prop == "kT": values["kT"] = np.full(n_conditions, float('NaN'))
|
|
194
|
+
if prop == "G":
|
|
195
|
+
# calculate S * (T - Tr), but set it to 0 at Tr (in case S is NA)
|
|
196
|
+
Sterm = PAR["S"]*(T - Tr)
|
|
197
|
+
Sterm = np.where(T == Tr, 0, Sterm)
|
|
198
|
+
|
|
199
|
+
# DEBUG
|
|
200
|
+
if False and PAR["name"] == "iron" and PAR.get("state") == "cr4":
|
|
201
|
+
print(f"DEBUG G calculation for {PAR['name']} {PAR.get('state', 'unknown')}:")
|
|
202
|
+
print(f" PAR['G'] = {PAR['G']}")
|
|
203
|
+
print(f" PAR['S'] = {PAR['S']}")
|
|
204
|
+
print(f" model = {PAR.get('model', 'unknown')}")
|
|
205
|
+
print(f" Sterm[0] = {Sterm[0] if hasattr(Sterm, '__len__') else Sterm}")
|
|
206
|
+
print(f" intCpdT[0] = {intCpdT[0] if hasattr(intCpdT, '__len__') else intCpdT}")
|
|
207
|
+
print(f" T[0]*intCpdlnT[0] = {(T[0]*intCpdlnT[0]) if hasattr(intCpdlnT, '__len__') else T*intCpdlnT}")
|
|
208
|
+
print(f" intVdP[0] = {intVdP[0] if hasattr(intVdP, '__len__') else intVdP}")
|
|
209
|
+
G_calc = PAR['G'] - Sterm + intCpdT - T*intCpdlnT + intVdP
|
|
210
|
+
print(f" G[0] (before subcrt conversion) = {G_calc[0] if hasattr(G_calc, '__len__') else G_calc}")
|
|
211
|
+
|
|
212
|
+
values["G"] = PAR["G"] - Sterm + intCpdT - T*intCpdlnT + intVdP
|
|
213
|
+
if prop == "H":
|
|
214
|
+
values["H"] = PAR["H"] + intCpdT + intVdP - T*intdVdTdP
|
|
215
|
+
if prop == "S": values["S"] = PAR["S"] + intCpdlnT - intdVdTdP
|
|
216
|
+
|
|
217
|
+
out_dict[k] = values # species have to be numbered instead of named because of name repeats (e.g., cr polymorphs)
|
|
218
|
+
|
|
219
|
+
return out_dict
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
### unexported function ###
|
|
223
|
+
|
|
224
|
+
# calculate GHS and V corrections for quartz and coesite 20170929
|
|
225
|
+
# (these are the only mineral phases for which SUPCRT92 uses an inconstant volume)
|
|
226
|
+
def quartz_coesite(PAR, T, P):
|
|
227
|
+
# the corrections are 0 for anything other than quartz and coesite
|
|
228
|
+
if not PAR["name"] in ["quartz", "coesite"]:
|
|
229
|
+
n = T.size if isinstance(T, np.ndarray) else 1
|
|
230
|
+
return(dict(G=np.zeros(n), H=np.zeros(n), S=np.zeros(n), V=np.zeros(n)))
|
|
231
|
+
|
|
232
|
+
# Vectorized version
|
|
233
|
+
T = np.atleast_1d(T)
|
|
234
|
+
P = np.atleast_1d(P)
|
|
235
|
+
|
|
236
|
+
# Tr, Pr and TtPr (transition temperature at Pr)
|
|
237
|
+
Pr = 1 # bar
|
|
238
|
+
Tr = 298.15 # K
|
|
239
|
+
TtPr = 848 # K
|
|
240
|
+
# constants from SUP92D.f
|
|
241
|
+
aa = 549.824
|
|
242
|
+
ba = 0.65995
|
|
243
|
+
ca = -0.4973e-4
|
|
244
|
+
VPtTta = 23.348
|
|
245
|
+
VPrTtb = 23.72
|
|
246
|
+
Stran = 0.342
|
|
247
|
+
# constants from REAC92D.f
|
|
248
|
+
VPrTra = 22.688 # VPrTr(a-quartz)
|
|
249
|
+
Vdiff = 2.047 # VPrTr(a-quartz) - VPrTr(coesite)
|
|
250
|
+
k = 38.5 # dPdTtr(a/b-quartz)
|
|
251
|
+
#k <- 38.45834 # calculated in CHNOSZ: dPdTtr(info("quartz"))
|
|
252
|
+
# code adapted from REAC92D.f
|
|
253
|
+
qphase = PAR["state"].replace("cr", "")
|
|
254
|
+
|
|
255
|
+
if qphase == "2":
|
|
256
|
+
Pstar = P.copy()
|
|
257
|
+
Sstar = np.zeros_like(T)
|
|
258
|
+
V = np.full_like(T, VPrTtb)
|
|
259
|
+
else:
|
|
260
|
+
Pstar = Pr + k * (T - TtPr)
|
|
261
|
+
Sstar = np.full_like(T, Stran)
|
|
262
|
+
V = VPrTra + ca*(P-Pr) + (VPtTta - VPrTra - ca*(P-Pr))*(T-Tr) / (TtPr + (P-Pr)/k - Tr)
|
|
263
|
+
|
|
264
|
+
# Apply condition: if T < TtPr
|
|
265
|
+
below_transition = T < TtPr
|
|
266
|
+
Pstar = np.where(below_transition, Pr, Pstar)
|
|
267
|
+
Sstar = np.where(below_transition, 0, Sstar)
|
|
268
|
+
|
|
269
|
+
if PAR["name"] == "coesite":
|
|
270
|
+
VPrTra = VPrTra - Vdiff
|
|
271
|
+
VPrTtb = VPrTtb - Vdiff
|
|
272
|
+
V = V - Vdiff
|
|
273
|
+
|
|
274
|
+
cm3bar_to_cal = 0.023901488
|
|
275
|
+
|
|
276
|
+
# Vectorized log calculation
|
|
277
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
278
|
+
log_term = np.log((aa + P/k) / (aa + Pstar/k))
|
|
279
|
+
log_term = np.where(np.isfinite(log_term), log_term, 0)
|
|
280
|
+
|
|
281
|
+
GVterm = cm3bar_to_cal * (VPrTra * (P - Pstar) + VPrTtb * (Pstar - Pr) - \
|
|
282
|
+
0.5 * ca * (2 * Pr * (P - Pstar) - (P**2 - Pstar**2)) - \
|
|
283
|
+
ca * k * (T - Tr) * (P - Pstar) + \
|
|
284
|
+
k * (ba + aa * ca * k) * (T - Tr) * log_term)
|
|
285
|
+
SVterm = cm3bar_to_cal * (-k * (ba + aa * ca * k) * log_term + ca * k * (P - Pstar)) - Sstar
|
|
286
|
+
|
|
287
|
+
# note the minus sign on "SVterm" in order that intdVdTdP has the correct sign
|
|
288
|
+
return dict(intVdP=GVterm, intdVdTdP=-SVterm, V=V)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def lambda_transition(T: np.ndarray, T_lambda: float, lambda_val: float,
|
|
292
|
+
sigma: float = 50.0):
|
|
293
|
+
"""
|
|
294
|
+
Calculate lambda transition contributions to thermodynamic properties.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
T : array
|
|
299
|
+
Temperature in Kelvin
|
|
300
|
+
T_lambda : float
|
|
301
|
+
Lambda transition temperature in Kelvin
|
|
302
|
+
lambda_val : float
|
|
303
|
+
Lambda transition parameter
|
|
304
|
+
sigma : float
|
|
305
|
+
Width parameter for transition (default: 50 K)
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
dict
|
|
310
|
+
Dictionary with lambda contributions to Cp, H, S, G
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
# Gaussian approximation for lambda transition
|
|
314
|
+
gaussian = np.exp(-(T - T_lambda)**2 / (2 * sigma**2))
|
|
315
|
+
|
|
316
|
+
# Heat capacity contribution
|
|
317
|
+
Cp_lambda = lambda_val * gaussian
|
|
318
|
+
|
|
319
|
+
# Enthalpy contribution (integrated Cp)
|
|
320
|
+
H_lambda = np.where(T > T_lambda,
|
|
321
|
+
lambda_val * sigma * np.sqrt(2*np.pi) * 0.5 * (1 + np.tanh((T-T_lambda)/sigma)),
|
|
322
|
+
0)
|
|
323
|
+
|
|
324
|
+
# Entropy contribution (integrated Cp/T)
|
|
325
|
+
S_lambda = np.where(T > T_lambda, lambda_val / T_lambda, 0)
|
|
326
|
+
|
|
327
|
+
# Gibbs energy contribution
|
|
328
|
+
G_lambda = H_lambda - T * S_lambda
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
'Cp': Cp_lambda,
|
|
332
|
+
'H': H_lambda,
|
|
333
|
+
'S': S_lambda,
|
|
334
|
+
'G': G_lambda
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def berman_properties(T: np.ndarray, P: np.ndarray, parameters: pd.Series):
|
|
339
|
+
"""
|
|
340
|
+
Calculate properties using Berman (1988) equations for minerals.
|
|
341
|
+
|
|
342
|
+
This is a simplified version of the Berman model - full implementation would
|
|
343
|
+
include all coefficients and corrections from Berman (1988).
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
# Standard state values
|
|
347
|
+
H0 = parameters.get('H', 0.0)
|
|
348
|
+
S0 = parameters.get('S', 0.0)
|
|
349
|
+
V0 = parameters.get('V', 0.0)
|
|
350
|
+
|
|
351
|
+
# Berman heat capacity coefficients (k0-k3)
|
|
352
|
+
k0 = parameters.get('k0', parameters.get('Cp', 0.0))
|
|
353
|
+
k1 = parameters.get('k1', 0.0)
|
|
354
|
+
k2 = parameters.get('k2', 0.0)
|
|
355
|
+
k3 = parameters.get('k3', 0.0)
|
|
356
|
+
|
|
357
|
+
# Heat capacity: Cp = k0 + k1/T^0.5 + k2/T^2 + k3/T^3
|
|
358
|
+
Cp_calc = k0 + k1 / np.sqrt(T) + k2 / T**2 + k3 / T**3
|
|
359
|
+
|
|
360
|
+
# Integrate for H and S (simplified)
|
|
361
|
+
Tr = 298.15
|
|
362
|
+
|
|
363
|
+
# Enthalpy integration (approximate)
|
|
364
|
+
H_calc = H0 + k0 * (T - Tr)
|
|
365
|
+
|
|
366
|
+
# Entropy integration (approximate)
|
|
367
|
+
S_calc = S0 + k0 * np.log(T/Tr)
|
|
368
|
+
|
|
369
|
+
# Gibbs energy
|
|
370
|
+
G_calc = H_calc - T * S_calc
|
|
371
|
+
|
|
372
|
+
# Volume (assume constant)
|
|
373
|
+
V_calc = np.full_like(T, V0)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
'G': G_calc,
|
|
377
|
+
'H': H_calc,
|
|
378
|
+
'S': S_calc,
|
|
379
|
+
'Cp': Cp_calc,
|
|
380
|
+
'V': V_calc
|
|
381
|
+
}
|