pychnosz 1.1.1__cp310-cp310-win_amd64.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.
Files changed (128) hide show
  1. pychnosz/__init__.py +129 -0
  2. pychnosz/biomolecules/__init__.py +29 -0
  3. pychnosz/biomolecules/ionize_aa.py +197 -0
  4. pychnosz/biomolecules/proteins.py +595 -0
  5. pychnosz/core/__init__.py +46 -0
  6. pychnosz/core/affinity.py +1256 -0
  7. pychnosz/core/animation.py +593 -0
  8. pychnosz/core/balance.py +334 -0
  9. pychnosz/core/basis.py +716 -0
  10. pychnosz/core/diagram.py +3336 -0
  11. pychnosz/core/equilibrate.py +813 -0
  12. pychnosz/core/equilibrium.py +554 -0
  13. pychnosz/core/info.py +821 -0
  14. pychnosz/core/retrieve.py +364 -0
  15. pychnosz/core/speciation.py +580 -0
  16. pychnosz/core/species.py +599 -0
  17. pychnosz/core/subcrt.py +1700 -0
  18. pychnosz/core/thermo.py +593 -0
  19. pychnosz/core/unicurve.py +1226 -0
  20. pychnosz/data/__init__.py +11 -0
  21. pychnosz/data/add_obigt.py +327 -0
  22. pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
  23. pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
  24. pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
  25. pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
  26. pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
  27. pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
  28. pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
  29. pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
  30. pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
  31. pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
  32. pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
  33. pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
  34. pychnosz/data/extdata/Berman/sympy.R +99 -0
  35. pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
  36. pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
  37. pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
  38. pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
  39. pychnosz/data/extdata/OBIGT/AD.csv +25 -0
  40. pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
  41. pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
  42. pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
  43. pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
  44. pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
  45. pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
  46. pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
  47. pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
  48. pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
  49. pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
  50. pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
  51. pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
  52. pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
  53. pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
  54. pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
  55. pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
  56. pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
  57. pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
  58. pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
  59. pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
  60. pychnosz/data/extdata/misc/BZA10.csv +5 -0
  61. pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
  62. pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
  63. pychnosz/data/extdata/misc/LA19_test.csv +7 -0
  64. pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
  65. pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
  66. pychnosz/data/extdata/misc/PM90.csv +7 -0
  67. pychnosz/data/extdata/misc/RH95.csv +23 -0
  68. pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
  69. pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
  70. pychnosz/data/extdata/misc/SK95.csv +55 -0
  71. pychnosz/data/extdata/misc/SOJSH.csv +61 -0
  72. pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
  73. pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
  74. pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
  75. pychnosz/data/extdata/misc/bluered.txt +1000 -0
  76. pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
  77. pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
  78. pychnosz/data/extdata/protein/Cas/download.R +34 -0
  79. pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
  80. pychnosz/data/extdata/protein/POLG.csv +12 -0
  81. pychnosz/data/extdata/protein/TBD+05.csv +393 -0
  82. pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
  83. pychnosz/data/extdata/protein/rubisco.csv +28 -0
  84. pychnosz/data/extdata/protein/rubisco.fasta +239 -0
  85. pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
  86. pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
  87. pychnosz/data/extdata/src/README.txt +5 -0
  88. pychnosz/data/extdata/taxonomy/names.dmp +215 -0
  89. pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
  90. pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
  91. pychnosz/data/extdata/thermo/buffer.csv +40 -0
  92. pychnosz/data/extdata/thermo/element.csv +135 -0
  93. pychnosz/data/extdata/thermo/groups.csv +6 -0
  94. pychnosz/data/extdata/thermo/opt.csv +2 -0
  95. pychnosz/data/extdata/thermo/protein.csv +506 -0
  96. pychnosz/data/extdata/thermo/refs.csv +343 -0
  97. pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
  98. pychnosz/data/loader.py +431 -0
  99. pychnosz/data/mod_obigt.py +322 -0
  100. pychnosz/data/obigt.py +471 -0
  101. pychnosz/data/worm.py +228 -0
  102. pychnosz/fortran/__init__.py +16 -0
  103. pychnosz/fortran/h2o92.dll +0 -0
  104. pychnosz/fortran/h2o92_interface.py +527 -0
  105. pychnosz/geochemistry/__init__.py +21 -0
  106. pychnosz/geochemistry/minerals.py +514 -0
  107. pychnosz/geochemistry/redox.py +500 -0
  108. pychnosz/models/__init__.py +47 -0
  109. pychnosz/models/archer_wang.py +165 -0
  110. pychnosz/models/berman.py +309 -0
  111. pychnosz/models/cgl.py +381 -0
  112. pychnosz/models/dew.py +997 -0
  113. pychnosz/models/hkf.py +523 -0
  114. pychnosz/models/hkf_helpers.py +222 -0
  115. pychnosz/models/iapws95.py +1113 -0
  116. pychnosz/models/supcrt92_fortran.py +238 -0
  117. pychnosz/models/water.py +480 -0
  118. pychnosz/utils/__init__.py +27 -0
  119. pychnosz/utils/expression.py +1074 -0
  120. pychnosz/utils/formula.py +830 -0
  121. pychnosz/utils/formula_ox.py +227 -0
  122. pychnosz/utils/reset.py +33 -0
  123. pychnosz/utils/units.py +259 -0
  124. pychnosz-1.1.1.dist-info/METADATA +197 -0
  125. pychnosz-1.1.1.dist-info/RECORD +128 -0
  126. pychnosz-1.1.1.dist-info/WHEEL +5 -0
  127. pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
  128. pychnosz-1.1.1.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
+ }