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.
- 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.1.dist-info/METADATA +197 -0
- pychnosz-1.1.1.dist-info/RECORD +128 -0
- pychnosz-1.1.1.dist-info/WHEEL +5 -0
- pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chemical equilibrium solver for CHNOSZ.
|
|
3
|
+
|
|
4
|
+
This module implements equilibrium calculations including:
|
|
5
|
+
- Activity coefficient models
|
|
6
|
+
- Chemical speciation
|
|
7
|
+
- Equilibrium constants
|
|
8
|
+
- Activity-concentration relationships
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from typing import Union, Dict, List, Optional, Tuple, Any
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
# Simple optimization functions (fallback for scipy)
|
|
17
|
+
def _simple_fsolve(func, x0, args=()):
|
|
18
|
+
"""Simple Newton-Raphson solver as scipy.optimize.fsolve fallback."""
|
|
19
|
+
x = np.array(x0, dtype=float)
|
|
20
|
+
for i in range(50): # Maximum iterations
|
|
21
|
+
try:
|
|
22
|
+
f = func(x, *args)
|
|
23
|
+
if np.allclose(f, 0, atol=1e-8):
|
|
24
|
+
return x
|
|
25
|
+
|
|
26
|
+
# Simple gradient estimation
|
|
27
|
+
dx = 1e-8
|
|
28
|
+
grad = np.zeros((len(x), len(f)))
|
|
29
|
+
|
|
30
|
+
for j in range(len(x)):
|
|
31
|
+
x_plus = x.copy()
|
|
32
|
+
x_plus[j] += dx
|
|
33
|
+
f_plus = func(x_plus, *args)
|
|
34
|
+
grad[j] = (f_plus - f) / dx
|
|
35
|
+
|
|
36
|
+
# Newton step (simplified)
|
|
37
|
+
try:
|
|
38
|
+
delta = np.linalg.solve(grad.T, -f)
|
|
39
|
+
x += delta * 0.1 # Damped step
|
|
40
|
+
except:
|
|
41
|
+
# If singular, use simple step
|
|
42
|
+
x -= f * 0.01
|
|
43
|
+
|
|
44
|
+
except:
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
return x
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from scipy.optimize import fsolve, minimize
|
|
51
|
+
except ImportError:
|
|
52
|
+
fsolve = _simple_fsolve
|
|
53
|
+
minimize = None
|
|
54
|
+
|
|
55
|
+
from .subcrt import subcrt
|
|
56
|
+
from .thermo import thermo
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EquilibriumSolver:
|
|
60
|
+
"""
|
|
61
|
+
Chemical equilibrium solver for aqueous systems.
|
|
62
|
+
|
|
63
|
+
This class implements various equilibrium calculation methods:
|
|
64
|
+
- Activity coefficient corrections (Debye-Hückel, B-dot, Pitzer)
|
|
65
|
+
- Chemical speciation calculations
|
|
66
|
+
- Reaction equilibrium constants
|
|
67
|
+
- Mass balance constraints
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
"""Initialize the equilibrium solver."""
|
|
72
|
+
self.activity_models = {
|
|
73
|
+
'ideal': self._activity_ideal,
|
|
74
|
+
'debye_huckel': self._activity_debye_huckel,
|
|
75
|
+
'bdot': self._activity_bdot,
|
|
76
|
+
'pitzer': self._activity_pitzer
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Default parameters
|
|
80
|
+
self.ionic_strength_limit = 3.0 # mol/kg
|
|
81
|
+
self.max_iterations = 100
|
|
82
|
+
self.tolerance = 1e-8
|
|
83
|
+
|
|
84
|
+
def calculate_logK(self, reaction: Dict[str, float],
|
|
85
|
+
T: Union[float, np.ndarray] = 298.15,
|
|
86
|
+
P: Union[float, np.ndarray] = 1.0) -> np.ndarray:
|
|
87
|
+
"""
|
|
88
|
+
Calculate equilibrium constant for a reaction.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
reaction : dict
|
|
93
|
+
Reaction dictionary with species names as keys and
|
|
94
|
+
stoichiometric coefficients as values (negative for reactants)
|
|
95
|
+
T : float or array, default 298.15
|
|
96
|
+
Temperature in Kelvin
|
|
97
|
+
P : float or array, default 1.0
|
|
98
|
+
Pressure in bar
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
array
|
|
103
|
+
log K values at given T and P
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
# Get species names and coefficients
|
|
107
|
+
species_names = list(reaction.keys())
|
|
108
|
+
coefficients = list(reaction.values())
|
|
109
|
+
|
|
110
|
+
# Calculate standard properties
|
|
111
|
+
result = subcrt(species_names, coefficients, T=T, P=P, show=False)
|
|
112
|
+
|
|
113
|
+
if result.out is not None and 'logK' in result.out.columns:
|
|
114
|
+
return result.out['logK'].values
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError("Could not calculate reaction properties")
|
|
117
|
+
|
|
118
|
+
def calculate_speciation(self, total_concentrations: Dict[str, float],
|
|
119
|
+
reactions: Dict[str, Dict[str, float]],
|
|
120
|
+
T: float = 298.15, P: float = 1.0,
|
|
121
|
+
pH: Optional[float] = None,
|
|
122
|
+
ionic_strength: Optional[float] = None,
|
|
123
|
+
activity_model: str = 'debye_huckel') -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Calculate chemical speciation for an aqueous system.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
total_concentrations : dict
|
|
130
|
+
Total concentrations of components (mol/kg)
|
|
131
|
+
reactions : dict
|
|
132
|
+
Formation reactions for each species
|
|
133
|
+
T : float, default 298.15
|
|
134
|
+
Temperature in Kelvin
|
|
135
|
+
P : float, default 1.0
|
|
136
|
+
Pressure in bar
|
|
137
|
+
pH : float, optional
|
|
138
|
+
pH constraint (if provided)
|
|
139
|
+
ionic_strength : float, optional
|
|
140
|
+
Ionic strength (if known, otherwise calculated)
|
|
141
|
+
activity_model : str, default 'debye_huckel'
|
|
142
|
+
Activity coefficient model to use
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
dict
|
|
147
|
+
Speciation results with concentrations, activities, and properties
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
# Get equilibrium constants for all reactions
|
|
151
|
+
logK_values = {}
|
|
152
|
+
for species, reaction in reactions.items():
|
|
153
|
+
try:
|
|
154
|
+
logK = self.calculate_logK(reaction, T, P)
|
|
155
|
+
logK_values[species] = logK[0] if hasattr(logK, '__len__') else logK
|
|
156
|
+
except Exception as e:
|
|
157
|
+
warnings.warn(f"Could not calculate logK for {species}: {e}")
|
|
158
|
+
logK_values[species] = 0.0
|
|
159
|
+
|
|
160
|
+
# Initial guess for species concentrations
|
|
161
|
+
species_names = list(reactions.keys())
|
|
162
|
+
basis_species = set()
|
|
163
|
+
for reaction in reactions.values():
|
|
164
|
+
basis_species.update(reaction.keys())
|
|
165
|
+
basis_species = list(basis_species)
|
|
166
|
+
|
|
167
|
+
# Create initial guess (equal distribution)
|
|
168
|
+
n_species = len(species_names)
|
|
169
|
+
n_basis = len(basis_species)
|
|
170
|
+
|
|
171
|
+
if n_species == 0:
|
|
172
|
+
return {'concentrations': {}, 'activities': {}, 'ionic_strength': 0.0}
|
|
173
|
+
|
|
174
|
+
# Initial concentrations (log scale for stability)
|
|
175
|
+
x0 = np.ones(n_species + n_basis) * (-6.0) # log concentrations
|
|
176
|
+
|
|
177
|
+
if pH is not None:
|
|
178
|
+
# Find H+ index and set pH constraint
|
|
179
|
+
if 'H+' in basis_species:
|
|
180
|
+
h_idx = basis_species.index('H+')
|
|
181
|
+
x0[n_species + h_idx] = -pH
|
|
182
|
+
|
|
183
|
+
# Solve equilibrium system
|
|
184
|
+
try:
|
|
185
|
+
solution = fsolve(self._equilibrium_equations, x0,
|
|
186
|
+
args=(species_names, basis_species, reactions,
|
|
187
|
+
logK_values, total_concentrations, pH,
|
|
188
|
+
T, P, activity_model))
|
|
189
|
+
|
|
190
|
+
if not np.allclose(self._equilibrium_equations(solution, species_names, basis_species,
|
|
191
|
+
reactions, logK_values,
|
|
192
|
+
total_concentrations, pH,
|
|
193
|
+
T, P, activity_model), 0, atol=1e-6):
|
|
194
|
+
warnings.warn("Equilibrium solution may not have converged")
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
warnings.warn(f"Equilibrium calculation failed: {e}")
|
|
198
|
+
solution = x0 # Use initial guess
|
|
199
|
+
|
|
200
|
+
# Extract results
|
|
201
|
+
log_species_conc = solution[:n_species]
|
|
202
|
+
log_basis_conc = solution[n_species:]
|
|
203
|
+
|
|
204
|
+
species_conc = 10**log_species_conc
|
|
205
|
+
basis_conc = 10**log_basis_conc
|
|
206
|
+
|
|
207
|
+
# Calculate ionic strength
|
|
208
|
+
ionic_str = self._calculate_ionic_strength(species_names, species_conc,
|
|
209
|
+
basis_species, basis_conc)
|
|
210
|
+
|
|
211
|
+
# Calculate activity coefficients
|
|
212
|
+
gamma_species = {}
|
|
213
|
+
gamma_basis = {}
|
|
214
|
+
|
|
215
|
+
for i, species in enumerate(species_names):
|
|
216
|
+
gamma_species[species] = self._get_activity_coefficient(
|
|
217
|
+
species, ionic_str, T, P, activity_model)
|
|
218
|
+
|
|
219
|
+
for i, species in enumerate(basis_species):
|
|
220
|
+
gamma_basis[species] = self._get_activity_coefficient(
|
|
221
|
+
species, ionic_str, T, P, activity_model)
|
|
222
|
+
|
|
223
|
+
# Calculate activities
|
|
224
|
+
activities_species = {species: conc * gamma_species[species]
|
|
225
|
+
for species, conc in zip(species_names, species_conc)}
|
|
226
|
+
activities_basis = {species: conc * gamma_basis[species]
|
|
227
|
+
for species, conc in zip(basis_species, basis_conc)}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
'concentrations': {**dict(zip(species_names, species_conc)),
|
|
231
|
+
**dict(zip(basis_species, basis_conc))},
|
|
232
|
+
'activities': {**activities_species, **activities_basis},
|
|
233
|
+
'activity_coefficients': {**gamma_species, **gamma_basis},
|
|
234
|
+
'ionic_strength': ionic_str,
|
|
235
|
+
'pH': -np.log10(basis_conc[basis_species.index('H+')]) if 'H+' in basis_species else None
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def _equilibrium_equations(self, x: np.ndarray, species_names: List[str],
|
|
239
|
+
basis_species: List[str], reactions: Dict[str, Dict[str, float]],
|
|
240
|
+
logK_values: Dict[str, float],
|
|
241
|
+
total_concentrations: Dict[str, float],
|
|
242
|
+
pH: Optional[float], T: float, P: float,
|
|
243
|
+
activity_model: str) -> np.ndarray:
|
|
244
|
+
"""
|
|
245
|
+
Equilibrium equations to solve for speciation.
|
|
246
|
+
|
|
247
|
+
Returns array of residuals for:
|
|
248
|
+
1. Mass balance equations
|
|
249
|
+
2. Equilibrium constant equations
|
|
250
|
+
3. Charge balance (if applicable)
|
|
251
|
+
4. pH constraint (if provided)
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
n_species = len(species_names)
|
|
255
|
+
n_basis = len(basis_species)
|
|
256
|
+
|
|
257
|
+
# Extract log concentrations
|
|
258
|
+
log_species_conc = x[:n_species]
|
|
259
|
+
log_basis_conc = x[n_species:]
|
|
260
|
+
|
|
261
|
+
species_conc = 10**log_species_conc
|
|
262
|
+
basis_conc = 10**log_basis_conc
|
|
263
|
+
|
|
264
|
+
# Calculate ionic strength for activity coefficients
|
|
265
|
+
ionic_str = self._calculate_ionic_strength(species_names, species_conc,
|
|
266
|
+
basis_species, basis_conc)
|
|
267
|
+
|
|
268
|
+
equations = []
|
|
269
|
+
|
|
270
|
+
# 1. Equilibrium constant equations
|
|
271
|
+
for i, species in enumerate(species_names):
|
|
272
|
+
if species in reactions:
|
|
273
|
+
reaction = reactions[species]
|
|
274
|
+
logK = logK_values[species]
|
|
275
|
+
|
|
276
|
+
# log K = log(activity of products) - log(activity of reactants)
|
|
277
|
+
log_activity_ratio = 0
|
|
278
|
+
|
|
279
|
+
for reactant, coeff in reaction.items():
|
|
280
|
+
if reactant in basis_species:
|
|
281
|
+
idx = basis_species.index(reactant)
|
|
282
|
+
gamma = self._get_activity_coefficient(reactant, ionic_str, T, P, activity_model)
|
|
283
|
+
log_activity_ratio += coeff * (log_basis_conc[idx] + np.log10(gamma))
|
|
284
|
+
|
|
285
|
+
# Activity of species being formed
|
|
286
|
+
gamma_species = self._get_activity_coefficient(species, ionic_str, T, P, activity_model)
|
|
287
|
+
log_species_activity = log_species_conc[i] + np.log10(gamma_species)
|
|
288
|
+
|
|
289
|
+
# Equilibrium equation: logK - log_activity_ratio + log_species_activity = 0
|
|
290
|
+
equations.append(logK - log_activity_ratio + log_species_activity)
|
|
291
|
+
|
|
292
|
+
# 2. Mass balance equations
|
|
293
|
+
for component, total_conc in total_concentrations.items():
|
|
294
|
+
if total_conc <= 0:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
mass_balance = 0
|
|
298
|
+
|
|
299
|
+
# Contribution from basis species
|
|
300
|
+
if component in basis_species:
|
|
301
|
+
idx = basis_species.index(component)
|
|
302
|
+
mass_balance += basis_conc[idx]
|
|
303
|
+
|
|
304
|
+
# Contributions from formed species
|
|
305
|
+
for i, species in enumerate(species_names):
|
|
306
|
+
if species in reactions and component in reactions[species]:
|
|
307
|
+
coeff = abs(reactions[species][component]) # Take absolute value for mass balance
|
|
308
|
+
mass_balance += coeff * species_conc[i]
|
|
309
|
+
|
|
310
|
+
# Mass balance equation: (calculated - total) / total = 0
|
|
311
|
+
equations.append((mass_balance - total_conc) / total_conc)
|
|
312
|
+
|
|
313
|
+
# 3. pH constraint
|
|
314
|
+
if pH is not None and 'H+' in basis_species:
|
|
315
|
+
h_idx = basis_species.index('H+')
|
|
316
|
+
equations.append(log_basis_conc[h_idx] + pH) # log[H+] + pH = 0
|
|
317
|
+
|
|
318
|
+
return np.array(equations)
|
|
319
|
+
|
|
320
|
+
def _calculate_ionic_strength(self, species_names: List[str], species_conc: np.ndarray,
|
|
321
|
+
basis_species: List[str], basis_conc: np.ndarray) -> float:
|
|
322
|
+
"""Calculate ionic strength of the solution."""
|
|
323
|
+
|
|
324
|
+
ionic_strength = 0
|
|
325
|
+
|
|
326
|
+
# Contributions from basis species (assume they have charges)
|
|
327
|
+
for i, species in enumerate(basis_species):
|
|
328
|
+
charge = self._get_species_charge(species)
|
|
329
|
+
ionic_strength += 0.5 * basis_conc[i] * charge**2
|
|
330
|
+
|
|
331
|
+
# Contributions from formed species
|
|
332
|
+
for i, species in enumerate(species_names):
|
|
333
|
+
charge = self._get_species_charge(species)
|
|
334
|
+
ionic_strength += 0.5 * species_conc[i] * charge**2
|
|
335
|
+
|
|
336
|
+
return ionic_strength
|
|
337
|
+
|
|
338
|
+
def _get_species_charge(self, species: str) -> int:
|
|
339
|
+
"""Extract charge from species name (simplified)."""
|
|
340
|
+
|
|
341
|
+
if '+' in species:
|
|
342
|
+
charge_str = species.split('+')[-1]
|
|
343
|
+
try:
|
|
344
|
+
return int(charge_str) if charge_str.isdigit() else 1
|
|
345
|
+
except:
|
|
346
|
+
return 1
|
|
347
|
+
elif '-' in species:
|
|
348
|
+
charge_str = species.split('-')[-1]
|
|
349
|
+
try:
|
|
350
|
+
return -int(charge_str) if charge_str.isdigit() else -1
|
|
351
|
+
except:
|
|
352
|
+
return -1
|
|
353
|
+
else:
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
def _get_activity_coefficient(self, species: str, ionic_strength: float,
|
|
357
|
+
T: float, P: float, model: str) -> float:
|
|
358
|
+
"""Calculate activity coefficient for a species."""
|
|
359
|
+
|
|
360
|
+
if model in self.activity_models:
|
|
361
|
+
return self.activity_models[model](species, ionic_strength, T, P)
|
|
362
|
+
else:
|
|
363
|
+
warnings.warn(f"Unknown activity model: {model}, using ideal")
|
|
364
|
+
return 1.0
|
|
365
|
+
|
|
366
|
+
def _activity_ideal(self, species: str, I: float, T: float, P: float) -> float:
|
|
367
|
+
"""Ideal activity coefficient (γ = 1)."""
|
|
368
|
+
return 1.0
|
|
369
|
+
|
|
370
|
+
def _activity_debye_huckel(self, species: str, I: float, T: float, P: float) -> float:
|
|
371
|
+
"""Debye-Hückel activity coefficient."""
|
|
372
|
+
|
|
373
|
+
charge = self._get_species_charge(species)
|
|
374
|
+
|
|
375
|
+
if charge == 0:
|
|
376
|
+
return 1.0 # Neutral species
|
|
377
|
+
|
|
378
|
+
# Debye-Hückel parameters (approximate)
|
|
379
|
+
A = 0.509 # at 25°C, valid for higher T too approximately
|
|
380
|
+
|
|
381
|
+
# Extended Debye-Hückel equation
|
|
382
|
+
if I <= 0.1:
|
|
383
|
+
log_gamma = -A * charge**2 * np.sqrt(I) / (1 + np.sqrt(I))
|
|
384
|
+
else:
|
|
385
|
+
# For higher ionic strengths, use extended form
|
|
386
|
+
B = 0.328 # approximate
|
|
387
|
+
log_gamma = -A * charge**2 * np.sqrt(I) / (1 + B * np.sqrt(I))
|
|
388
|
+
|
|
389
|
+
return 10**log_gamma
|
|
390
|
+
|
|
391
|
+
def _activity_bdot(self, species: str, I: float, T: float, P: float) -> float:
|
|
392
|
+
"""B-dot activity coefficient model."""
|
|
393
|
+
|
|
394
|
+
charge = self._get_species_charge(species)
|
|
395
|
+
|
|
396
|
+
if charge == 0:
|
|
397
|
+
return 1.0
|
|
398
|
+
|
|
399
|
+
# B-dot equation parameters (simplified)
|
|
400
|
+
A = 0.509 # Debye-Hückel A parameter
|
|
401
|
+
B = 0.328 # B parameter
|
|
402
|
+
bdot = 0.041 # B-dot parameter (approximate)
|
|
403
|
+
|
|
404
|
+
sqrt_I = np.sqrt(I)
|
|
405
|
+
log_gamma = -A * charge**2 * sqrt_I / (1 + B * sqrt_I) + bdot * I
|
|
406
|
+
|
|
407
|
+
return 10**log_gamma
|
|
408
|
+
|
|
409
|
+
def _activity_pitzer(self, species: str, I: float, T: float, P: float) -> float:
|
|
410
|
+
"""Pitzer activity coefficient model (simplified)."""
|
|
411
|
+
|
|
412
|
+
# For now, fall back to B-dot model
|
|
413
|
+
return self._activity_bdot(species, I, T, P)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Global equilibrium solver instance
|
|
417
|
+
_equilibrium_solver = EquilibriumSolver()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def affinity(species: Optional[Union[str, List[str]]] = None,
|
|
421
|
+
property: str = 'A', T: float = 298.15, P: float = 1.0) -> pd.DataFrame:
|
|
422
|
+
"""
|
|
423
|
+
Calculate chemical affinity for formation reactions.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
species : str, list, or None
|
|
428
|
+
Species to calculate affinity for. If None, uses current thermo species.
|
|
429
|
+
property : str, default 'A'
|
|
430
|
+
Property to calculate ('A' for affinity, 'logK' for log K, 'logQ' for log Q)
|
|
431
|
+
T : float, default 298.15
|
|
432
|
+
Temperature in Kelvin
|
|
433
|
+
P : float, default 1.0
|
|
434
|
+
Pressure in bar
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
DataFrame
|
|
439
|
+
Affinities and related properties
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
# This would interface with the basis system to calculate formation reaction affinities
|
|
443
|
+
# For now, return a placeholder
|
|
444
|
+
|
|
445
|
+
if species is None:
|
|
446
|
+
if thermo.species is None:
|
|
447
|
+
raise ValueError("No species defined. Use species() function first.")
|
|
448
|
+
species_list = thermo.species['name'].tolist()
|
|
449
|
+
elif isinstance(species, str):
|
|
450
|
+
species_list = [species]
|
|
451
|
+
else:
|
|
452
|
+
species_list = species
|
|
453
|
+
|
|
454
|
+
results = []
|
|
455
|
+
for sp in species_list:
|
|
456
|
+
# Calculate formation reaction from basis species
|
|
457
|
+
# This requires implementing the basis system and formation reactions
|
|
458
|
+
result = {
|
|
459
|
+
'species': sp,
|
|
460
|
+
'T': T,
|
|
461
|
+
'P': P,
|
|
462
|
+
'A': 0.0, # Placeholder - would calculate actual affinity
|
|
463
|
+
'logK': 0.0, # Placeholder
|
|
464
|
+
'logQ': 0.0 # Placeholder
|
|
465
|
+
}
|
|
466
|
+
results.append(result)
|
|
467
|
+
|
|
468
|
+
return pd.DataFrame(results)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def equilibrate(aout: Optional[pd.DataFrame] = None,
|
|
472
|
+
balance: Optional[Union[str, int]] = None,
|
|
473
|
+
normalize: bool = False,
|
|
474
|
+
as_residue: bool = False) -> Dict[str, Any]:
|
|
475
|
+
"""
|
|
476
|
+
Find chemical equilibrium using an optimization approach.
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
aout : DataFrame, optional
|
|
481
|
+
Output from affinity() function
|
|
482
|
+
balance : str or int, optional
|
|
483
|
+
Balanced chemical component
|
|
484
|
+
normalize : bool, default False
|
|
485
|
+
Normalize activities to sum to 1
|
|
486
|
+
as_residue : bool, default False
|
|
487
|
+
Return residue of optimization
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
dict
|
|
492
|
+
Equilibrium results
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
# Placeholder implementation
|
|
496
|
+
# This would implement the equilibrium optimization using affinity calculations
|
|
497
|
+
|
|
498
|
+
if aout is None:
|
|
499
|
+
raise ValueError("affinity output required")
|
|
500
|
+
|
|
501
|
+
# For now, return equal distribution
|
|
502
|
+
n_species = len(aout)
|
|
503
|
+
activities = np.ones(n_species) / n_species
|
|
504
|
+
|
|
505
|
+
result = {
|
|
506
|
+
'activities': activities,
|
|
507
|
+
'residual': 0.0,
|
|
508
|
+
'converged': True
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return result
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def solubility(species: Union[str, List[str]],
|
|
515
|
+
mineral: str,
|
|
516
|
+
T: float = 298.15, P: float = 1.0,
|
|
517
|
+
pH_range: Optional[Tuple[float, float]] = None) -> Dict[str, Any]:
|
|
518
|
+
"""
|
|
519
|
+
Calculate mineral solubility in aqueous solution.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
species : str or list
|
|
524
|
+
Aqueous species in equilibrium with mineral
|
|
525
|
+
mineral : str
|
|
526
|
+
Mineral name
|
|
527
|
+
T : float, default 298.15
|
|
528
|
+
Temperature in Kelvin
|
|
529
|
+
P : float, default 1.0
|
|
530
|
+
Pressure in bar
|
|
531
|
+
pH_range : tuple, optional
|
|
532
|
+
pH range for calculation (min_pH, max_pH)
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
dict
|
|
537
|
+
Solubility results
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
if pH_range is None:
|
|
541
|
+
pH_range = (0, 14)
|
|
542
|
+
|
|
543
|
+
pH_values = np.linspace(pH_range[0], pH_range[1], 100)
|
|
544
|
+
|
|
545
|
+
# Calculate dissolution reaction
|
|
546
|
+
# This requires implementing mineral dissolution reactions
|
|
547
|
+
|
|
548
|
+
results = {
|
|
549
|
+
'pH': pH_values,
|
|
550
|
+
'solubility': np.zeros_like(pH_values), # Placeholder
|
|
551
|
+
'species_distribution': {} # Placeholder
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return results
|