pychnosz 1.1.4__cp311-cp311-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.
- pychnosz/__init__.py +129 -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 +1700 -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/__init__.py +16 -0
- pychnosz/fortran/h2o92.dll +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 +222 -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.4.dist-info/METADATA +197 -0
- pychnosz-1.1.4.dist-info/RECORD +128 -0
- pychnosz-1.1.4.dist-info/WHEEL +5 -0
- pychnosz-1.1.4.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mineral equilibria and phase diagram calculations for CHNOSZ.
|
|
3
|
+
|
|
4
|
+
This module implements mineral stability calculations, phase diagrams,
|
|
5
|
+
and solid-solution equilibria for geological applications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from typing import Union, Dict, List, Optional, Tuple, Any
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
from ..core.subcrt import subcrt
|
|
14
|
+
from ..core.equilibrium import EquilibriumSolver
|
|
15
|
+
from ..core.speciation import SpeciationEngine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MineralEquilibria:
|
|
19
|
+
"""
|
|
20
|
+
Mineral equilibria calculator for geological systems.
|
|
21
|
+
|
|
22
|
+
This class handles mineral stability calculations, including:
|
|
23
|
+
- Mineral solubility equilibria
|
|
24
|
+
- Phase stability diagrams
|
|
25
|
+
- Mineral-water reactions
|
|
26
|
+
- Solid solution equilibria
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize the mineral equilibria calculator."""
|
|
31
|
+
self.equilibrium_solver = EquilibriumSolver()
|
|
32
|
+
self.speciation_engine = SpeciationEngine()
|
|
33
|
+
|
|
34
|
+
# Common mineral dissolution reactions (simplified)
|
|
35
|
+
self.dissolution_reactions = {
|
|
36
|
+
'quartz': {'SiO2': 1, 'H2O': 0, 'H4SiO4': -1},
|
|
37
|
+
'calcite': {'CaCO3': 1, 'H+': 2, 'Ca+2': -1, 'HCO3-': -1, 'H2O': -1},
|
|
38
|
+
'pyrite': {'FeS2': 1, 'H+': 8, 'SO4-2': -2, 'Fe+2': -1, 'H2O': -4},
|
|
39
|
+
'hematite': {'Fe2O3': 1, 'H+': 6, 'Fe+3': -2, 'H2O': -3},
|
|
40
|
+
'magnetite': {'Fe3O4': 1, 'H+': 8, 'Fe+2': -1, 'Fe+3': -2, 'H2O': -4},
|
|
41
|
+
'albite': {'NaAlSi3O8': 1, 'H+': 1, 'H2O': 4, 'Na+': -1, 'Al+3': -1, 'H4SiO4': -3},
|
|
42
|
+
'anorthite': {'CaAl2Si2O8': 1, 'H+': 8, 'H2O': 0, 'Ca+2': -1, 'Al+3': -2, 'H4SiO4': -2},
|
|
43
|
+
'kaolinite': {'Al2Si2O5(OH)4': 1, 'H+': 6, 'Al+3': -2, 'H4SiO4': -2, 'H2O': -1}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def mineral_solubility(self, mineral: str,
|
|
47
|
+
T: float = 298.15, P: float = 1.0,
|
|
48
|
+
pH_range: Optional[Tuple[float, float]] = None,
|
|
49
|
+
ionic_strength: float = 0.0) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Calculate mineral solubility as a function of pH.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
mineral : str
|
|
56
|
+
Mineral name
|
|
57
|
+
T : float, default 298.15
|
|
58
|
+
Temperature in Kelvin
|
|
59
|
+
P : float, default 1.0
|
|
60
|
+
Pressure in bar
|
|
61
|
+
pH_range : tuple, optional
|
|
62
|
+
pH range (min, max). Default: (0, 14)
|
|
63
|
+
ionic_strength : float, default 0.0
|
|
64
|
+
Solution ionic strength
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
dict
|
|
69
|
+
Solubility data vs pH
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if pH_range is None:
|
|
73
|
+
pH_range = (0, 14)
|
|
74
|
+
|
|
75
|
+
pH_values = np.linspace(pH_range[0], pH_range[1], 100)
|
|
76
|
+
|
|
77
|
+
if mineral.lower() not in self.dissolution_reactions:
|
|
78
|
+
warnings.warn(f"Dissolution reaction not defined for {mineral}")
|
|
79
|
+
return {'pH': pH_values, 'solubility': np.zeros_like(pH_values)}
|
|
80
|
+
|
|
81
|
+
reaction = self.dissolution_reactions[mineral.lower()]
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Get equilibrium constant for dissolution
|
|
85
|
+
species_names = list(reaction.keys())
|
|
86
|
+
coefficients = list(reaction.values())
|
|
87
|
+
|
|
88
|
+
# Calculate logK at T, P
|
|
89
|
+
result = subcrt(species_names, coefficients, T=T, P=P, show=False)
|
|
90
|
+
if result.out is not None and 'logK' in result.out.columns:
|
|
91
|
+
logK = result.out['logK'].iloc[0]
|
|
92
|
+
else:
|
|
93
|
+
logK = 0.0
|
|
94
|
+
warnings.warn(f"Could not calculate logK for {mineral} dissolution")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
warnings.warn(f"Error calculating equilibrium constant: {e}")
|
|
98
|
+
logK = 0.0
|
|
99
|
+
|
|
100
|
+
# Calculate solubility at each pH
|
|
101
|
+
solubility = np.zeros_like(pH_values)
|
|
102
|
+
|
|
103
|
+
for i, pH in enumerate(pH_values):
|
|
104
|
+
try:
|
|
105
|
+
# Simplified solubility calculation
|
|
106
|
+
# This would be more complex in a full implementation
|
|
107
|
+
|
|
108
|
+
if 'H+' in reaction:
|
|
109
|
+
h_coeff = reaction['H+']
|
|
110
|
+
# Basic pH dependence
|
|
111
|
+
log_solubility = logK - h_coeff * pH
|
|
112
|
+
else:
|
|
113
|
+
log_solubility = logK
|
|
114
|
+
|
|
115
|
+
# Convert to molality
|
|
116
|
+
solubility[i] = 10**log_solubility
|
|
117
|
+
|
|
118
|
+
# Apply ionic strength corrections if needed
|
|
119
|
+
if ionic_strength > 0:
|
|
120
|
+
# Simple activity coefficient correction
|
|
121
|
+
gamma = self._estimate_activity_coefficient(ionic_strength)
|
|
122
|
+
solubility[i] *= gamma
|
|
123
|
+
|
|
124
|
+
except:
|
|
125
|
+
solubility[i] = 1e-20 # Very low solubility
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
'pH': pH_values,
|
|
129
|
+
'solubility': solubility,
|
|
130
|
+
'logK': logK,
|
|
131
|
+
'mineral': mineral,
|
|
132
|
+
'T': T,
|
|
133
|
+
'P': P
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def stability_diagram(self, minerals: List[str],
|
|
137
|
+
x_axis: str, x_range: Tuple[float, float],
|
|
138
|
+
y_axis: str, y_range: Tuple[float, float],
|
|
139
|
+
T: float = 298.15, P: float = 1.0,
|
|
140
|
+
resolution: int = 50) -> Dict[str, Any]:
|
|
141
|
+
"""
|
|
142
|
+
Create mineral stability diagram.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
minerals : list of str
|
|
147
|
+
Mineral names to consider
|
|
148
|
+
x_axis : str
|
|
149
|
+
X-axis variable ('pH', 'log_activity_SiO2', etc.)
|
|
150
|
+
x_range : tuple
|
|
151
|
+
X-axis range (min, max)
|
|
152
|
+
y_axis : str
|
|
153
|
+
Y-axis variable
|
|
154
|
+
y_range : tuple
|
|
155
|
+
Y-axis range (min, max)
|
|
156
|
+
T : float, default 298.15
|
|
157
|
+
Temperature in Kelvin
|
|
158
|
+
P : float, default 1.0
|
|
159
|
+
Pressure in bar
|
|
160
|
+
resolution : int, default 50
|
|
161
|
+
Grid resolution
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
dict
|
|
166
|
+
Stability diagram data
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
x_values = np.linspace(x_range[0], x_range[1], resolution)
|
|
170
|
+
y_values = np.linspace(y_range[0], y_range[1], resolution)
|
|
171
|
+
X, Y = np.meshgrid(x_values, y_values)
|
|
172
|
+
|
|
173
|
+
# Initialize stability field array
|
|
174
|
+
stable_mineral = np.zeros_like(X, dtype=int)
|
|
175
|
+
|
|
176
|
+
# Calculate stability at each grid point
|
|
177
|
+
for i in range(len(y_values)):
|
|
178
|
+
for j in range(len(x_values)):
|
|
179
|
+
x_val, y_val = X[i, j], Y[i, j]
|
|
180
|
+
|
|
181
|
+
# Find most stable mineral at this point
|
|
182
|
+
min_energy = np.inf
|
|
183
|
+
stable_idx = 0
|
|
184
|
+
|
|
185
|
+
for k, mineral in enumerate(minerals):
|
|
186
|
+
try:
|
|
187
|
+
# Calculate relative stability
|
|
188
|
+
# This would involve full equilibrium calculations
|
|
189
|
+
# For now, use simplified energy estimates
|
|
190
|
+
|
|
191
|
+
energy = self._calculate_stability_energy(
|
|
192
|
+
mineral, x_axis, x_val, y_axis, y_val, T, P)
|
|
193
|
+
|
|
194
|
+
if energy < min_energy:
|
|
195
|
+
min_energy = energy
|
|
196
|
+
stable_idx = k
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
warnings.warn(f"Error calculating stability for {mineral}: {e}")
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
stable_mineral[i, j] = stable_idx
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
x_axis: x_values,
|
|
206
|
+
y_axis: y_values,
|
|
207
|
+
'stable_mineral': stable_mineral,
|
|
208
|
+
'mineral_names': minerals,
|
|
209
|
+
'T': T,
|
|
210
|
+
'P': P
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def phase_equilibrium(self, reaction: Dict[str, float],
|
|
214
|
+
T_range: Optional[Tuple[float, float]] = None,
|
|
215
|
+
P_range: Optional[Tuple[float, float]] = None) -> Dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Calculate phase equilibrium curve.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
reaction : dict
|
|
222
|
+
Reaction stoichiometry {species: coefficient}
|
|
223
|
+
T_range : tuple, optional
|
|
224
|
+
Temperature range (min, max) in K
|
|
225
|
+
P_range : tuple, optional
|
|
226
|
+
Pressure range (min, max) in bar
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
dict
|
|
231
|
+
Equilibrium curve data
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if T_range is None and P_range is None:
|
|
235
|
+
T_range = (273.15, 673.15) # 0-400°C
|
|
236
|
+
|
|
237
|
+
if T_range is not None:
|
|
238
|
+
# T-P curve at equilibrium
|
|
239
|
+
T_values = np.linspace(T_range[0], T_range[1], 50)
|
|
240
|
+
P_equilibrium = np.zeros_like(T_values)
|
|
241
|
+
|
|
242
|
+
for i, T in enumerate(T_values):
|
|
243
|
+
try:
|
|
244
|
+
# Calculate equilibrium pressure
|
|
245
|
+
# This would involve iterative solution
|
|
246
|
+
# For now, use simplified correlation
|
|
247
|
+
P_equilibrium[i] = self._calculate_equilibrium_pressure(reaction, T)
|
|
248
|
+
|
|
249
|
+
except:
|
|
250
|
+
P_equilibrium[i] = 1.0 # Default
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
'T': T_values,
|
|
254
|
+
'P': P_equilibrium,
|
|
255
|
+
'reaction': reaction
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
# P-T curve - similar logic
|
|
260
|
+
P_values = np.linspace(P_range[0], P_range[1], 50)
|
|
261
|
+
T_equilibrium = np.zeros_like(P_values)
|
|
262
|
+
|
|
263
|
+
for i, P in enumerate(P_values):
|
|
264
|
+
try:
|
|
265
|
+
T_equilibrium[i] = self._calculate_equilibrium_temperature(reaction, P)
|
|
266
|
+
except:
|
|
267
|
+
T_equilibrium[i] = 298.15
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
'P': P_values,
|
|
271
|
+
'T': T_equilibrium,
|
|
272
|
+
'reaction': reaction
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def solid_solution(self, end_members: List[str], compositions: List[float],
|
|
276
|
+
T: float = 298.15, P: float = 1.0,
|
|
277
|
+
model: str = 'ideal') -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Calculate solid solution thermodynamics.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
end_members : list of str
|
|
284
|
+
End-member mineral names
|
|
285
|
+
compositions : list of float
|
|
286
|
+
Mole fractions of end-members (must sum to 1)
|
|
287
|
+
T : float, default 298.15
|
|
288
|
+
Temperature in Kelvin
|
|
289
|
+
P : float, default 1.0
|
|
290
|
+
Pressure in bar
|
|
291
|
+
model : str, default 'ideal'
|
|
292
|
+
Mixing model ('ideal', 'regular', 'margules')
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
dict
|
|
297
|
+
Solid solution properties
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
if len(end_members) != len(compositions):
|
|
301
|
+
raise ValueError("Number of end-members must match number of compositions")
|
|
302
|
+
|
|
303
|
+
if abs(sum(compositions) - 1.0) > 1e-6:
|
|
304
|
+
raise ValueError("Compositions must sum to 1.0")
|
|
305
|
+
|
|
306
|
+
# Calculate end-member properties
|
|
307
|
+
end_member_props = []
|
|
308
|
+
for mineral in end_members:
|
|
309
|
+
try:
|
|
310
|
+
# This would get mineral properties from database
|
|
311
|
+
props = self._get_mineral_properties(mineral, T, P)
|
|
312
|
+
end_member_props.append(props)
|
|
313
|
+
except:
|
|
314
|
+
# Default properties
|
|
315
|
+
props = {'G': -1000000, 'H': -1200000, 'S': 100, 'V': 50, 'Cp': 100}
|
|
316
|
+
end_member_props.append(props)
|
|
317
|
+
|
|
318
|
+
# Calculate mixing properties
|
|
319
|
+
if model == 'ideal':
|
|
320
|
+
# Ideal mixing
|
|
321
|
+
G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
|
|
322
|
+
H_mix = 0.0
|
|
323
|
+
S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
|
|
324
|
+
|
|
325
|
+
elif model == 'regular':
|
|
326
|
+
# Regular solution (symmetric)
|
|
327
|
+
W = 10000 # Interaction parameter (J/mol) - would be mineral-specific
|
|
328
|
+
G_mix = W * compositions[0] * compositions[1] # Binary case
|
|
329
|
+
H_mix = G_mix
|
|
330
|
+
S_mix = 0.0
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
# Default to ideal
|
|
334
|
+
G_mix = 8.314 * T * sum(x * np.log(x) for x in compositions if x > 0)
|
|
335
|
+
H_mix = 0.0
|
|
336
|
+
S_mix = -8.314 * sum(x * np.log(x) for x in compositions if x > 0)
|
|
337
|
+
|
|
338
|
+
# Total properties
|
|
339
|
+
total_props = {}
|
|
340
|
+
for prop in ['G', 'H', 'S', 'V', 'Cp']:
|
|
341
|
+
total_props[prop] = sum(compositions[i] * end_member_props[i][prop]
|
|
342
|
+
for i in range(len(end_members)))
|
|
343
|
+
|
|
344
|
+
# Add mixing contributions
|
|
345
|
+
total_props['G'] += G_mix
|
|
346
|
+
total_props['H'] += H_mix
|
|
347
|
+
total_props['S'] += S_mix
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
'properties': total_props,
|
|
351
|
+
'mixing': {'G': G_mix, 'H': H_mix, 'S': S_mix},
|
|
352
|
+
'end_members': end_members,
|
|
353
|
+
'compositions': compositions,
|
|
354
|
+
'model': model
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def _estimate_activity_coefficient(self, ionic_strength: float) -> float:
|
|
358
|
+
"""Estimate activity coefficient from ionic strength."""
|
|
359
|
+
if ionic_strength <= 0:
|
|
360
|
+
return 1.0
|
|
361
|
+
else:
|
|
362
|
+
# Simple Debye-Hückel approximation
|
|
363
|
+
A = 0.509
|
|
364
|
+
return 10**(-A * np.sqrt(ionic_strength))
|
|
365
|
+
|
|
366
|
+
def _calculate_stability_energy(self, mineral: str, x_axis: str, x_val: float,
|
|
367
|
+
y_axis: str, y_val: float, T: float, P: float) -> float:
|
|
368
|
+
"""Calculate relative stability energy for mineral."""
|
|
369
|
+
|
|
370
|
+
# Simplified stability calculation
|
|
371
|
+
# Would involve full thermodynamic analysis
|
|
372
|
+
|
|
373
|
+
base_energy = np.random.random() * 1000 # Placeholder
|
|
374
|
+
|
|
375
|
+
# Add dependencies on axes
|
|
376
|
+
if 'pH' in x_axis:
|
|
377
|
+
base_energy += abs(x_val - 7) * 100 # pH dependence
|
|
378
|
+
|
|
379
|
+
if 'log' in y_axis:
|
|
380
|
+
base_energy += abs(y_val + 3) * 50 # Activity dependence
|
|
381
|
+
|
|
382
|
+
return base_energy
|
|
383
|
+
|
|
384
|
+
def _calculate_equilibrium_pressure(self, reaction: Dict[str, float], T: float) -> float:
|
|
385
|
+
"""Calculate equilibrium pressure for reaction at given T."""
|
|
386
|
+
|
|
387
|
+
# Simplified using Clapeyron equation approximation
|
|
388
|
+
# dP/dT = ΔS/ΔV
|
|
389
|
+
|
|
390
|
+
# Estimate volume and entropy changes
|
|
391
|
+
delta_V = 5.0e-6 # m³/mol (typical for mineral reactions)
|
|
392
|
+
delta_S = 20.0 # J/(mol·K) (typical)
|
|
393
|
+
|
|
394
|
+
# Integrate from reference conditions
|
|
395
|
+
T0, P0 = 298.15, 1.0
|
|
396
|
+
dP_dT = delta_S / delta_V * 1e-5 # Convert to bar/K
|
|
397
|
+
|
|
398
|
+
P_eq = P0 + dP_dT * (T - T0)
|
|
399
|
+
|
|
400
|
+
return max(P_eq, 0.1) # Minimum 0.1 bar
|
|
401
|
+
|
|
402
|
+
def _calculate_equilibrium_temperature(self, reaction: Dict[str, float], P: float) -> float:
|
|
403
|
+
"""Calculate equilibrium temperature for reaction at given P."""
|
|
404
|
+
|
|
405
|
+
# Inverse of pressure calculation
|
|
406
|
+
delta_V = 5.0e-6
|
|
407
|
+
delta_S = 20.0
|
|
408
|
+
|
|
409
|
+
T0, P0 = 298.15, 1.0
|
|
410
|
+
dT_dP = delta_V / delta_S * 1e5 # K/bar
|
|
411
|
+
|
|
412
|
+
T_eq = T0 + dT_dP * (P - P0)
|
|
413
|
+
|
|
414
|
+
return max(T_eq, 273.15) # Minimum 0°C
|
|
415
|
+
|
|
416
|
+
def _get_mineral_properties(self, mineral: str, T: float, P: float) -> Dict[str, float]:
|
|
417
|
+
"""Get thermodynamic properties of a mineral."""
|
|
418
|
+
|
|
419
|
+
# This would look up mineral in database
|
|
420
|
+
# For now, return typical values
|
|
421
|
+
|
|
422
|
+
typical_props = {
|
|
423
|
+
'quartz': {'G': -856300, 'H': -910700, 'S': 41.5, 'V': 22.7, 'Cp': 44.6},
|
|
424
|
+
'calcite': {'G': -1128800, 'H': -1207400, 'S': 91.7, 'V': 36.9, 'Cp': 83.5},
|
|
425
|
+
'pyrite': {'G': -160200, 'H': -171500, 'S': 52.9, 'V': 23.9, 'Cp': 62.2},
|
|
426
|
+
'hematite': {'G': -744400, 'H': -824200, 'S': 87.4, 'V': 30.3, 'Cp': 103.9}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return typical_props.get(mineral.lower(), {
|
|
430
|
+
'G': -500000, 'H': -600000, 'S': 50, 'V': 30, 'Cp': 80
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# Global mineral equilibria calculator
|
|
435
|
+
_mineral_calculator = MineralEquilibria()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def mineral_solubility(mineral: str, pH_range: Tuple[float, float] = (0, 14),
|
|
439
|
+
T: float = 298.15, P: float = 1.0) -> Dict[str, Any]:
|
|
440
|
+
"""
|
|
441
|
+
Calculate mineral solubility vs pH.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
mineral : str
|
|
446
|
+
Mineral name
|
|
447
|
+
pH_range : tuple, default (0, 14)
|
|
448
|
+
pH range for calculation
|
|
449
|
+
T : float, default 298.15
|
|
450
|
+
Temperature in Kelvin
|
|
451
|
+
P : float, default 1.0
|
|
452
|
+
Pressure in bar
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
dict
|
|
457
|
+
Solubility data
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
return _mineral_calculator.mineral_solubility(mineral, T, P, pH_range)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def stability_field(minerals: List[str],
|
|
464
|
+
variables: Tuple[str, str],
|
|
465
|
+
ranges: Tuple[Tuple[float, float], Tuple[float, float]],
|
|
466
|
+
T: float = 298.15, P: float = 1.0) -> Dict[str, Any]:
|
|
467
|
+
"""
|
|
468
|
+
Calculate mineral stability fields.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
minerals : list of str
|
|
473
|
+
Minerals to consider
|
|
474
|
+
variables : tuple of str
|
|
475
|
+
(x_variable, y_variable) for diagram
|
|
476
|
+
ranges : tuple of tuples
|
|
477
|
+
((x_min, x_max), (y_min, y_max))
|
|
478
|
+
T : float, default 298.15
|
|
479
|
+
Temperature in Kelvin
|
|
480
|
+
P : float, default 1.0
|
|
481
|
+
Pressure in bar
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
dict
|
|
486
|
+
Stability diagram data
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
x_var, y_var = variables
|
|
490
|
+
x_range, y_range = ranges
|
|
491
|
+
|
|
492
|
+
return _mineral_calculator.stability_diagram(
|
|
493
|
+
minerals, x_var, x_range, y_var, y_range, T, P)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def phase_boundary(reaction: Dict[str, float],
|
|
497
|
+
T_range: Tuple[float, float] = (273.15, 673.15)) -> Dict[str, Any]:
|
|
498
|
+
"""
|
|
499
|
+
Calculate phase boundary for a reaction.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
reaction : dict
|
|
504
|
+
Reaction stoichiometry {species: coefficient}
|
|
505
|
+
T_range : tuple, default (273.15, 673.15)
|
|
506
|
+
Temperature range in Kelvin
|
|
507
|
+
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
dict
|
|
511
|
+
Phase boundary data
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
return _mineral_calculator.phase_equilibrium(reaction, T_range=T_range)
|