pychnosz 1.1.11__cp312-cp312-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 +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/__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 +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.11.dist-info/METADATA +197 -0
- pychnosz-1.1.11.dist-info/RECORD +128 -0
- pychnosz-1.1.11.dist-info/WHEEL +5 -0
- pychnosz-1.1.11.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redox reactions and Eh-pH diagram calculations for CHNOSZ.
|
|
3
|
+
|
|
4
|
+
This module implements redox equilibria calculations, Eh-pH diagrams,
|
|
5
|
+
and electron activity (pe) calculations for environmental geochemistry.
|
|
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
|
+
|
|
16
|
+
|
|
17
|
+
class RedoxCalculator:
|
|
18
|
+
"""
|
|
19
|
+
Redox equilibria calculator for geochemical systems.
|
|
20
|
+
|
|
21
|
+
This class handles redox reactions, pe-pH diagrams, and
|
|
22
|
+
electron activity calculations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize the redox calculator."""
|
|
27
|
+
self.equilibrium_solver = EquilibriumSolver()
|
|
28
|
+
|
|
29
|
+
# Standard electrode potentials (V) at 25°C
|
|
30
|
+
self.standard_potentials = {
|
|
31
|
+
'O2/H2O': 1.229,
|
|
32
|
+
'H+/H2': 0.000,
|
|
33
|
+
'Fe+3/Fe+2': 0.771,
|
|
34
|
+
'NO3-/NO2-': 0.835,
|
|
35
|
+
'SO4-2/HS-': -0.217,
|
|
36
|
+
'CO2/CH4': -0.244,
|
|
37
|
+
'N2/NH4+': -0.277,
|
|
38
|
+
'Fe+2/Fe': -0.447,
|
|
39
|
+
'S/HS-': -0.065,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Common redox couples and their reactions
|
|
43
|
+
self.redox_reactions = {
|
|
44
|
+
# Oxygen reduction
|
|
45
|
+
'O2_H2O': {'O2': 1, 'H+': 4, 'e-': 4, 'H2O': -2},
|
|
46
|
+
'H2O_H2': {'H2O': 2, 'e-': 2, 'H2': -1, 'OH-': -2},
|
|
47
|
+
|
|
48
|
+
# Iron redox
|
|
49
|
+
'Fe3_Fe2': {'Fe+3': 1, 'e-': 1, 'Fe+2': -1},
|
|
50
|
+
'Fe2_Fe': {'Fe+2': 1, 'e-': 2, 'Fe': -1},
|
|
51
|
+
'Fe2O3_Fe2': {'Fe2O3': 1, 'H+': 6, 'e-': 2, 'Fe+2': -2, 'H2O': -3},
|
|
52
|
+
|
|
53
|
+
# Nitrogen redox
|
|
54
|
+
'NO3_NO2': {'NO3-': 1, 'H+': 2, 'e-': 2, 'NO2-': -1, 'H2O': -1},
|
|
55
|
+
'NO2_NH4': {'NO2-': 1, 'H+': 8, 'e-': 6, 'NH4+': -1, 'H2O': -2},
|
|
56
|
+
|
|
57
|
+
# Sulfur redox
|
|
58
|
+
'SO4_HS': {'SO4-2': 1, 'H+': 9, 'e-': 8, 'HS-': -1, 'H2O': -4},
|
|
59
|
+
'S_HS': {'S': 1, 'H+': 1, 'e-': 2, 'HS-': -1},
|
|
60
|
+
|
|
61
|
+
# Carbon redox
|
|
62
|
+
'CO2_CH4': {'CO2': 1, 'H+': 8, 'e-': 8, 'CH4': -1, 'H2O': -2},
|
|
63
|
+
'HCO3_CH4': {'HCO3-': 1, 'H+': 9, 'e-': 8, 'CH4': -1, 'H2O': -3}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def eh_ph_diagram(self, element: str,
|
|
67
|
+
pH_range: Tuple[float, float] = (0, 14),
|
|
68
|
+
pe_range: Tuple[float, float] = (-10, 15),
|
|
69
|
+
T: float = 298.15, P: float = 1.0,
|
|
70
|
+
total_concentration: float = 1e-6,
|
|
71
|
+
resolution: int = 100) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Create Eh-pH (pe-pH) diagram for an element.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
element : str
|
|
78
|
+
Element symbol (e.g., 'Fe', 'S', 'N', 'C')
|
|
79
|
+
pH_range : tuple, default (0, 14)
|
|
80
|
+
pH range for diagram
|
|
81
|
+
pe_range : tuple, default (-10, 15)
|
|
82
|
+
pe (electron activity) range
|
|
83
|
+
T : float, default 298.15
|
|
84
|
+
Temperature in Kelvin
|
|
85
|
+
P : float, default 1.0
|
|
86
|
+
Pressure in bar
|
|
87
|
+
total_concentration : float, default 1e-6
|
|
88
|
+
Total element concentration (molal)
|
|
89
|
+
resolution : int, default 100
|
|
90
|
+
Grid resolution
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
dict
|
|
95
|
+
Eh-pH diagram data
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
pH_grid = np.linspace(pH_range[0], pH_range[1], resolution)
|
|
99
|
+
pe_grid = np.linspace(pe_range[0], pe_range[1], resolution)
|
|
100
|
+
pH_mesh, pe_mesh = np.meshgrid(pH_grid, pe_grid)
|
|
101
|
+
|
|
102
|
+
# Get species for this element
|
|
103
|
+
element_species = self._get_element_species(element)
|
|
104
|
+
|
|
105
|
+
if not element_species:
|
|
106
|
+
raise ValueError(f"No species found for element {element}")
|
|
107
|
+
|
|
108
|
+
# Calculate predominance at each point
|
|
109
|
+
predominant = np.zeros_like(pH_mesh, dtype=int)
|
|
110
|
+
activities = {species: np.zeros_like(pH_mesh) for species in element_species}
|
|
111
|
+
|
|
112
|
+
for i in range(len(pe_grid)):
|
|
113
|
+
for j in range(len(pH_grid)):
|
|
114
|
+
pH, pe = pH_mesh[i, j], pe_mesh[i, j]
|
|
115
|
+
|
|
116
|
+
# Calculate speciation at this point
|
|
117
|
+
spec_result = self._calculate_redox_speciation(
|
|
118
|
+
element_species, pH, pe, T, P, total_concentration)
|
|
119
|
+
|
|
120
|
+
# Find predominant species
|
|
121
|
+
max_activity = -np.inf
|
|
122
|
+
max_idx = 0
|
|
123
|
+
|
|
124
|
+
for k, species in enumerate(element_species):
|
|
125
|
+
activity = spec_result.get(species, 1e-20)
|
|
126
|
+
activities[species][i, j] = activity
|
|
127
|
+
|
|
128
|
+
if np.log10(activity) > max_activity:
|
|
129
|
+
max_activity = np.log10(activity)
|
|
130
|
+
max_idx = k
|
|
131
|
+
|
|
132
|
+
predominant[i, j] = max_idx
|
|
133
|
+
|
|
134
|
+
# Add water stability limits
|
|
135
|
+
water_limits = self._water_stability_lines(pH_grid, T, P)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
'pH': pH_grid,
|
|
139
|
+
'pe': pe_grid,
|
|
140
|
+
'pH_mesh': pH_mesh,
|
|
141
|
+
'pe_mesh': pe_mesh,
|
|
142
|
+
'predominant': predominant,
|
|
143
|
+
'activities': activities,
|
|
144
|
+
'species_names': element_species,
|
|
145
|
+
'water_limits': water_limits,
|
|
146
|
+
'element': element,
|
|
147
|
+
'T': T,
|
|
148
|
+
'P': P,
|
|
149
|
+
'total_concentration': total_concentration
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def pe_calculation(self, redox_couple: Union[str, Dict[str, float]],
|
|
153
|
+
concentrations: Dict[str, float],
|
|
154
|
+
pH: float = 7.0, T: float = 298.15, P: float = 1.0) -> float:
|
|
155
|
+
"""
|
|
156
|
+
Calculate pe (electron activity) for a redox couple.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
redox_couple : str or dict
|
|
161
|
+
Redox couple name or reaction dictionary
|
|
162
|
+
concentrations : dict
|
|
163
|
+
Species concentrations {species: concentration}
|
|
164
|
+
pH : float, default 7.0
|
|
165
|
+
Solution pH
|
|
166
|
+
T : float, default 298.15
|
|
167
|
+
Temperature in Kelvin
|
|
168
|
+
P : float, default 1.0
|
|
169
|
+
Pressure in bar
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
float
|
|
174
|
+
pe value
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
if isinstance(redox_couple, str):
|
|
178
|
+
if redox_couple not in self.redox_reactions:
|
|
179
|
+
raise ValueError(f"Unknown redox couple: {redox_couple}")
|
|
180
|
+
reaction = self.redox_reactions[redox_couple]
|
|
181
|
+
else:
|
|
182
|
+
reaction = redox_couple
|
|
183
|
+
|
|
184
|
+
# Calculate equilibrium constant
|
|
185
|
+
try:
|
|
186
|
+
species_names = [sp for sp in reaction.keys() if sp != 'e-']
|
|
187
|
+
coefficients = [reaction[sp] for sp in species_names]
|
|
188
|
+
|
|
189
|
+
result = subcrt(species_names, coefficients, T=T, P=P, show=False)
|
|
190
|
+
if result.out is not None and 'logK' in result.out.columns:
|
|
191
|
+
logK = result.out['logK'].iloc[0]
|
|
192
|
+
else:
|
|
193
|
+
logK = 0.0
|
|
194
|
+
except Exception as e:
|
|
195
|
+
warnings.warn(f"Could not calculate logK: {e}")
|
|
196
|
+
logK = 0.0
|
|
197
|
+
|
|
198
|
+
# Apply Nernst equation
|
|
199
|
+
n_electrons = abs(reaction.get('e-', 1)) # Number of electrons
|
|
200
|
+
|
|
201
|
+
# Calculate activity quotient
|
|
202
|
+
log_Q = 0.0
|
|
203
|
+
for species, coeff in reaction.items():
|
|
204
|
+
if species == 'e-':
|
|
205
|
+
continue
|
|
206
|
+
elif species == 'H+':
|
|
207
|
+
activity = 10**(-pH)
|
|
208
|
+
elif species in concentrations:
|
|
209
|
+
activity = concentrations[species]
|
|
210
|
+
# Apply activity coefficients if needed
|
|
211
|
+
gamma = self._get_activity_coefficient(species, concentrations, T)
|
|
212
|
+
activity *= gamma
|
|
213
|
+
else:
|
|
214
|
+
activity = 1.0 # Default for species not specified
|
|
215
|
+
|
|
216
|
+
if activity > 0:
|
|
217
|
+
log_Q += coeff * np.log10(activity)
|
|
218
|
+
|
|
219
|
+
# Nernst equation: pe = pe° + (1/n) * log(Q)
|
|
220
|
+
# where pe° = logK/n for the half-reaction
|
|
221
|
+
pe = logK / n_electrons + log_Q / n_electrons
|
|
222
|
+
|
|
223
|
+
return pe
|
|
224
|
+
|
|
225
|
+
def eh_from_pe(self, pe: float, T: float = 298.15) -> float:
|
|
226
|
+
"""
|
|
227
|
+
Convert pe to Eh (redox potential).
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
pe : float
|
|
232
|
+
Electron activity (pe)
|
|
233
|
+
T : float, default 298.15
|
|
234
|
+
Temperature in Kelvin
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
float
|
|
239
|
+
Redox potential (Eh) in Volts
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
# Eh = (RT/F) * ln(10) * pe
|
|
243
|
+
# where R = 8.314 J/(mol·K), F = 96485 C/mol
|
|
244
|
+
RT_F = 8.314 * T / 96485
|
|
245
|
+
Eh = RT_F * 2.302585 * pe # ln(10) = 2.302585
|
|
246
|
+
|
|
247
|
+
return Eh
|
|
248
|
+
|
|
249
|
+
def pe_from_eh(self, eh: float, T: float = 298.15) -> float:
|
|
250
|
+
"""
|
|
251
|
+
Convert Eh (redox potential) to pe.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
eh : float
|
|
256
|
+
Redox potential (Eh) in Volts
|
|
257
|
+
T : float, default 298.15
|
|
258
|
+
Temperature in Kelvin
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
float
|
|
263
|
+
Electron activity (pe)
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
# pe = F * Eh / (RT * ln(10))
|
|
267
|
+
RT_F = 8.314 * T / 96485
|
|
268
|
+
pe = eh / (RT_F * 2.302585)
|
|
269
|
+
|
|
270
|
+
return pe
|
|
271
|
+
|
|
272
|
+
def oxygen_fugacity(self, pe: float, pH: float = 7.0,
|
|
273
|
+
T: float = 298.15, P: float = 1.0) -> float:
|
|
274
|
+
"""
|
|
275
|
+
Calculate oxygen fugacity from pe and pH.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
pe : float
|
|
280
|
+
Electron activity
|
|
281
|
+
pH : float, default 7.0
|
|
282
|
+
Solution pH
|
|
283
|
+
T : float, default 298.15
|
|
284
|
+
Temperature in Kelvin
|
|
285
|
+
P : float, default 1.0
|
|
286
|
+
Pressure in bar
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
float
|
|
291
|
+
log fO2 (log oxygen fugacity)
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
# O2 + 4H+ + 4e- = 2H2O
|
|
295
|
+
# At equilibrium: log fO2 = 4*pe + 4*pH - logK
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# Calculate logK for oxygen-water reaction
|
|
299
|
+
result = subcrt(['O2', 'H+', 'H2O'], [1, 4, -2], T=T, P=P, show=False)
|
|
300
|
+
if result.out is not None and 'logK' in result.out.columns:
|
|
301
|
+
logK = result.out['logK'].iloc[0]
|
|
302
|
+
else:
|
|
303
|
+
logK = 83.1 # Approximate value at 25°C
|
|
304
|
+
except:
|
|
305
|
+
logK = 83.1
|
|
306
|
+
|
|
307
|
+
log_fO2 = 4 * pe + 4 * pH - logK
|
|
308
|
+
|
|
309
|
+
return log_fO2
|
|
310
|
+
|
|
311
|
+
def _get_element_species(self, element: str) -> List[str]:
|
|
312
|
+
"""Get list of species containing the specified element."""
|
|
313
|
+
|
|
314
|
+
# Simplified species lists for common elements
|
|
315
|
+
element_species = {
|
|
316
|
+
'Fe': ['Fe+2', 'Fe+3', 'Fe2O3', 'FeOH+', 'Fe(OH)2', 'Fe(OH)3'],
|
|
317
|
+
'S': ['SO4-2', 'SO3-2', 'S2O3-2', 'HS-', 'S0', 'S-2'],
|
|
318
|
+
'N': ['NO3-', 'NO2-', 'NH4+', 'NH3', 'N2O', 'N2'],
|
|
319
|
+
'C': ['CO2', 'HCO3-', 'CO3-2', 'CH4', 'HCOOH', 'CH3COO-'],
|
|
320
|
+
'Mn': ['Mn+2', 'MnO4-', 'MnO2', 'Mn+3', 'MnOH+'],
|
|
321
|
+
'As': ['AsO4-3', 'AsO3-3', 'H3AsO4', 'H3AsO3', 'As0']
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return element_species.get(element, [f'{element}+2', f'{element}+3'])
|
|
325
|
+
|
|
326
|
+
def _calculate_redox_speciation(self, species: List[str], pH: float, pe: float,
|
|
327
|
+
T: float, P: float, total_conc: float) -> Dict[str, float]:
|
|
328
|
+
"""Calculate speciation at given pH and pe."""
|
|
329
|
+
|
|
330
|
+
# Simplified speciation calculation
|
|
331
|
+
# Full implementation would solve equilibrium system
|
|
332
|
+
|
|
333
|
+
activities = {}
|
|
334
|
+
|
|
335
|
+
for sp in species:
|
|
336
|
+
# Simplified pe-pH dependence
|
|
337
|
+
if '+' in sp: # Cations (more stable at low pH, high pe)
|
|
338
|
+
charge = self._extract_charge(sp)
|
|
339
|
+
log_activity = -6 + charge * (14 - pH) / 14 + pe / 10
|
|
340
|
+
elif '-' in sp: # Anions (more stable at high pH, high pe)
|
|
341
|
+
charge = abs(self._extract_charge(sp))
|
|
342
|
+
log_activity = -6 + charge * pH / 14 + pe / 10
|
|
343
|
+
else: # Neutral (less pH/pe dependence)
|
|
344
|
+
log_activity = -6 + (pe - pH) / 20
|
|
345
|
+
|
|
346
|
+
activities[sp] = 10**log_activity
|
|
347
|
+
|
|
348
|
+
# Normalize to total concentration
|
|
349
|
+
total_calculated = sum(activities.values())
|
|
350
|
+
if total_calculated > 0:
|
|
351
|
+
factor = total_conc / total_calculated
|
|
352
|
+
activities = {sp: act * factor for sp, act in activities.items()}
|
|
353
|
+
|
|
354
|
+
return activities
|
|
355
|
+
|
|
356
|
+
def _extract_charge(self, species: str) -> int:
|
|
357
|
+
"""Extract charge from species name."""
|
|
358
|
+
|
|
359
|
+
if '+' in species:
|
|
360
|
+
parts = species.split('+')
|
|
361
|
+
if len(parts) > 1 and parts[-1].isdigit():
|
|
362
|
+
return int(parts[-1])
|
|
363
|
+
else:
|
|
364
|
+
return 1
|
|
365
|
+
elif '-' in species:
|
|
366
|
+
parts = species.split('-')
|
|
367
|
+
if len(parts) > 1 and parts[-1].isdigit():
|
|
368
|
+
return -int(parts[-1])
|
|
369
|
+
else:
|
|
370
|
+
return -1
|
|
371
|
+
else:
|
|
372
|
+
return 0
|
|
373
|
+
|
|
374
|
+
def _water_stability_lines(self, pH_values: np.ndarray,
|
|
375
|
+
T: float, P: float) -> Dict[str, np.ndarray]:
|
|
376
|
+
"""Calculate water stability limits (H2O/H2 and O2/H2O)."""
|
|
377
|
+
|
|
378
|
+
# Upper limit: O2/H2O
|
|
379
|
+
# O2 + 4H+ + 4e- = 2H2O
|
|
380
|
+
# pe = 20.75 - pH (at 25°C, 1 atm O2)
|
|
381
|
+
pe_upper = 20.75 - pH_values
|
|
382
|
+
|
|
383
|
+
# Lower limit: H2O/H2
|
|
384
|
+
# 2H2O + 2e- = H2 + 2OH-
|
|
385
|
+
# pe = -pH (at 25°C, 1 atm H2)
|
|
386
|
+
pe_lower = -pH_values
|
|
387
|
+
|
|
388
|
+
# Temperature corrections (simplified)
|
|
389
|
+
if T != 298.15:
|
|
390
|
+
dT = T - 298.15
|
|
391
|
+
pe_upper += dT * 0.001 # Small temperature dependence
|
|
392
|
+
pe_lower -= dT * 0.001
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
'upper': pe_upper, # O2/H2O line
|
|
396
|
+
'lower': pe_lower # H2O/H2 line
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
def _get_activity_coefficient(self, species: str, composition: Dict[str, float],
|
|
400
|
+
T: float) -> float:
|
|
401
|
+
"""Get activity coefficient for species."""
|
|
402
|
+
|
|
403
|
+
# Simplified - would use proper activity models
|
|
404
|
+
charge = abs(self._extract_charge(species))
|
|
405
|
+
|
|
406
|
+
if charge == 0:
|
|
407
|
+
return 1.0
|
|
408
|
+
else:
|
|
409
|
+
# Simple ionic strength correction
|
|
410
|
+
I = 0.001 # Assume low ionic strength
|
|
411
|
+
return 10**(-0.509 * charge**2 * np.sqrt(I))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# Global redox calculator
|
|
415
|
+
_redox_calculator = RedoxCalculator()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def eh_ph(element: str, **kwargs) -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Create Eh-pH diagram for an element.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
element : str
|
|
425
|
+
Element symbol
|
|
426
|
+
**kwargs
|
|
427
|
+
Additional parameters for eh_ph_diagram()
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
dict
|
|
432
|
+
Eh-pH diagram data
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
return _redox_calculator.eh_ph_diagram(element, **kwargs)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def pe(redox_couple: Union[str, Dict[str, float]],
|
|
439
|
+
concentrations: Dict[str, float], **kwargs) -> float:
|
|
440
|
+
"""
|
|
441
|
+
Calculate pe for a redox couple.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
redox_couple : str or dict
|
|
446
|
+
Redox couple or reaction
|
|
447
|
+
concentrations : dict
|
|
448
|
+
Species concentrations
|
|
449
|
+
**kwargs
|
|
450
|
+
Additional parameters
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
float
|
|
455
|
+
pe value
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
return _redox_calculator.pe_calculation(redox_couple, concentrations, **kwargs)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def eh(pe_value: float, T: float = 298.15) -> float:
|
|
462
|
+
"""
|
|
463
|
+
Convert pe to Eh.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
pe_value : float
|
|
468
|
+
Electron activity
|
|
469
|
+
T : float, default 298.15
|
|
470
|
+
Temperature in Kelvin
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
float
|
|
475
|
+
Eh in Volts
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
return _redox_calculator.eh_from_pe(pe_value, T)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def logfO2(pe_value: float, pH: float = 7.0, **kwargs) -> float:
|
|
482
|
+
"""
|
|
483
|
+
Calculate log oxygen fugacity.
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
pe_value : float
|
|
488
|
+
Electron activity
|
|
489
|
+
pH : float, default 7.0
|
|
490
|
+
Solution pH
|
|
491
|
+
**kwargs
|
|
492
|
+
Additional parameters
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
float
|
|
497
|
+
log fO2
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
return _redox_calculator.oxygen_fugacity(pe_value, pH, **kwargs)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Equation of state models and water property models for CHNOSZ."""
|
|
2
|
+
|
|
3
|
+
from .water import water, available_properties, get_water_models, compare_models, WaterModelError
|
|
4
|
+
|
|
5
|
+
# Import Fortran-backed SUPCRT92 (falls back to Python if Fortran unavailable)
|
|
6
|
+
from .supcrt92_fortran import water_SUPCRT92, SUPCRT92Water
|
|
7
|
+
|
|
8
|
+
# Import HKF equation of state functions
|
|
9
|
+
from .hkf import hkf, gfun, convert_cm3bar
|
|
10
|
+
|
|
11
|
+
# Import CGL equation of state functions
|
|
12
|
+
from .cgl import cgl, quartz_coesite
|
|
13
|
+
|
|
14
|
+
# Import HKF helper functions
|
|
15
|
+
from .hkf_helpers import calc_logK, calc_G_TP, G2logK, dissrxn2logK, OBIGT2eos
|
|
16
|
+
|
|
17
|
+
# Optional imports for modules that may not exist yet
|
|
18
|
+
try:
|
|
19
|
+
from .hkf import HKF
|
|
20
|
+
except ImportError:
|
|
21
|
+
HKF = None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .cgl import CGL
|
|
25
|
+
except ImportError:
|
|
26
|
+
CGL = None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from .berman import Berman
|
|
30
|
+
except ImportError:
|
|
31
|
+
Berman = None
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'water', 'available_properties', 'get_water_models', 'compare_models', 'WaterModelError',
|
|
35
|
+
'water_SUPCRT92', 'SUPCRT92Water',
|
|
36
|
+
'hkf', 'gfun', 'convert_cm3bar',
|
|
37
|
+
'cgl', 'quartz_coesite',
|
|
38
|
+
'calc_logK', 'calc_G_TP', 'G2logK', 'dissrxn2logK', 'OBIGT2eos'
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Add optional functions if they exist
|
|
42
|
+
if HKF is not None:
|
|
43
|
+
__all__.append('HKF')
|
|
44
|
+
if CGL is not None:
|
|
45
|
+
__all__.append('CGL')
|
|
46
|
+
if Berman is not None:
|
|
47
|
+
__all__.append('Berman')
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Archer & Wang (1990) dielectric constant correlation for water.
|
|
3
|
+
|
|
4
|
+
This module implements the accurate dielectric constant calculation
|
|
5
|
+
used in the original R version of CHNOSZ.
|
|
6
|
+
|
|
7
|
+
Reference:
|
|
8
|
+
Archer, D. G., and Wang, P. (1990) The dielectric constant of water
|
|
9
|
+
and Debye-Hückel limiting law slopes. Journal of Physical and Chemical
|
|
10
|
+
Reference Data, 19, 371-411.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import warnings
|
|
15
|
+
from typing import Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def water_AW90(T: Union[float, np.ndarray] = 298.15,
|
|
19
|
+
rho: Union[float, np.ndarray] = 1000.0,
|
|
20
|
+
P: Union[float, np.ndarray] = 0.1) -> Union[float, np.ndarray]:
|
|
21
|
+
"""
|
|
22
|
+
Calculate dielectric constant of water using Archer & Wang (1990) correlation.
|
|
23
|
+
|
|
24
|
+
This is a direct Python translation of the R function water.AW90() from
|
|
25
|
+
the original CHNOSZ package.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
T : float or array
|
|
30
|
+
Temperature in Kelvin
|
|
31
|
+
rho : float or array
|
|
32
|
+
Density in kg/m³
|
|
33
|
+
P : float or array
|
|
34
|
+
Pressure in MPa
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
float or array
|
|
39
|
+
Dielectric constant (dimensionless)
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> # Water at 25°C, 1000 kg/m³, 0.1 MPa
|
|
44
|
+
>>> eps = water_AW90(298.15, 1000.0, 0.1)
|
|
45
|
+
>>> print(f"Dielectric constant: {eps:.1f}") # Should be ~78.4
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Convert inputs to arrays
|
|
49
|
+
T = np.atleast_1d(np.asarray(T, dtype=float))
|
|
50
|
+
rho = np.atleast_1d(np.asarray(rho, dtype=float))
|
|
51
|
+
P = np.atleast_1d(np.asarray(P, dtype=float))
|
|
52
|
+
|
|
53
|
+
# Make all arrays the same length
|
|
54
|
+
max_len = max(len(T), len(rho), len(P))
|
|
55
|
+
if len(T) < max_len:
|
|
56
|
+
T = np.resize(T, max_len)
|
|
57
|
+
if len(rho) < max_len:
|
|
58
|
+
rho = np.resize(rho, max_len)
|
|
59
|
+
if len(P) < max_len:
|
|
60
|
+
P = np.resize(P, max_len)
|
|
61
|
+
|
|
62
|
+
# Table 2 coefficients from Archer & Wang (1990)
|
|
63
|
+
b = np.array([
|
|
64
|
+
-4.044525E-2, 103.6180, 75.32165,
|
|
65
|
+
-23.23778, -3.548184, -1246.311,
|
|
66
|
+
263307.7, -6.928953E-1, -204.4473
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
# Physical constants
|
|
70
|
+
alpha = 18.1458392E-30 # polarizability, m³
|
|
71
|
+
mu = 6.1375776E-30 # dipole moment, C·m
|
|
72
|
+
N_A = 6.0221367E23 # Avogadro's number, mol⁻¹
|
|
73
|
+
k = 1.380658E-23 # Boltzmann constant, J·K⁻¹
|
|
74
|
+
M = 0.0180153 # molar mass of water, kg/mol
|
|
75
|
+
rho_0 = 1000.0 # reference density, kg/m³
|
|
76
|
+
epsilon_0 = 8.8541878E-12 # permittivity of vacuum, C²·J⁻¹·m⁻¹
|
|
77
|
+
|
|
78
|
+
# Initialize output
|
|
79
|
+
epsilon = np.full_like(T, np.nan)
|
|
80
|
+
|
|
81
|
+
for i in range(len(T)):
|
|
82
|
+
T_i = T[i]
|
|
83
|
+
rho_i = rho[i]
|
|
84
|
+
P_i = P[i]
|
|
85
|
+
|
|
86
|
+
# Skip invalid conditions
|
|
87
|
+
if np.isnan(T_i) or np.isnan(rho_i) or np.isnan(P_i):
|
|
88
|
+
continue
|
|
89
|
+
if T_i <= 0 or rho_i <= 0 or P_i < 0:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Equation 3: rho function
|
|
93
|
+
def rhofun():
|
|
94
|
+
return (b[0]*P_i/T_i +
|
|
95
|
+
b[1]/np.sqrt(T_i) +
|
|
96
|
+
b[2]/(T_i-215) +
|
|
97
|
+
b[3]/np.sqrt(T_i-215) +
|
|
98
|
+
b[4]/(T_i-215)**0.25 +
|
|
99
|
+
np.exp(b[5]/T_i + b[6]/T_i**2 + b[7]*P_i/T_i + b[8]*P_i/T_i**2))
|
|
100
|
+
|
|
101
|
+
# g function
|
|
102
|
+
def gfun():
|
|
103
|
+
return rhofun() * rho_i/rho_0 + 1.0
|
|
104
|
+
|
|
105
|
+
# mu function
|
|
106
|
+
def mufun():
|
|
107
|
+
return gfun() * mu**2
|
|
108
|
+
|
|
109
|
+
# Right-hand side of Equation 1
|
|
110
|
+
V_m = M / rho_i # molar volume, m³/mol
|
|
111
|
+
epsfun_rhs = N_A * (alpha + mufun()/(3*epsilon_0*k*T_i)) / (3*V_m)
|
|
112
|
+
|
|
113
|
+
# Solve quadratic equation (Equation 1 rearranged)
|
|
114
|
+
# Original: (ε-1)(2ε+1)/(9ε) = rhs
|
|
115
|
+
# Rearranged to: 2ε² - (9*rhs + 1)ε + 1 = 0
|
|
116
|
+
# Using quadratic formula with positive root
|
|
117
|
+
discriminant = (9*epsfun_rhs + 1)**2 + 8
|
|
118
|
+
if discriminant < 0:
|
|
119
|
+
warnings.warn(f'water_AW90: negative discriminant at T={T_i:.1f} K, '
|
|
120
|
+
f'rho={rho_i:.0f} kg/m3', stacklevel=2)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
epsilon_calc = (9*epsfun_rhs + 1 + np.sqrt(discriminant)) / 4.0
|
|
124
|
+
|
|
125
|
+
# Check for reasonable result
|
|
126
|
+
if epsilon_calc < 1.0 or epsilon_calc > 200.0:
|
|
127
|
+
warnings.warn(f'water_AW90: unrealistic dielectric constant {epsilon_calc:.1f} '
|
|
128
|
+
f'at T={T_i:.1f} K, rho={rho_i:.0f} kg/m3', stacklevel=2)
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
epsilon[i] = epsilon_calc
|
|
132
|
+
|
|
133
|
+
# Return scalar if input was scalar
|
|
134
|
+
if len(epsilon) == 1:
|
|
135
|
+
return epsilon[0]
|
|
136
|
+
else:
|
|
137
|
+
return epsilon
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
# Test the function
|
|
142
|
+
print("Testing Archer & Wang (1990) dielectric constant correlation")
|
|
143
|
+
print("=" * 60)
|
|
144
|
+
|
|
145
|
+
# Test conditions from R CHNOSZ
|
|
146
|
+
test_conditions = [
|
|
147
|
+
(298.15, 997.0, 0.1), # 25°C, ~997 kg/m³, 0.1 MPa
|
|
148
|
+
(373.15, 958.0, 0.1), # 100°C
|
|
149
|
+
(273.15, 1000.0, 0.1), # 0°C
|
|
150
|
+
(473.15, 800.0, 1.0), # 200°C, 1 MPa
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for T, rho, P in test_conditions:
|
|
154
|
+
eps = water_AW90(T, rho, P)
|
|
155
|
+
print(f"T = {T:6.1f} K, ρ = {rho:6.0f} kg/m³, P = {P:5.1f} MPa: ε = {eps:6.1f}")
|
|
156
|
+
|
|
157
|
+
# Test array input
|
|
158
|
+
print("\nTesting array input:")
|
|
159
|
+
T_array = np.array([273.15, 298.15, 373.15])
|
|
160
|
+
rho_array = np.array([1000.0, 997.0, 958.0])
|
|
161
|
+
P_array = np.array([0.1, 0.1, 0.1])
|
|
162
|
+
|
|
163
|
+
eps_array = water_AW90(T_array, rho_array, P_array)
|
|
164
|
+
for i in range(len(T_array)):
|
|
165
|
+
print(f"T = {T_array[i]:6.1f} K: ε = {eps_array[i]:6.1f}")
|