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.
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.4.dist-info/METADATA +197 -0
  125. pychnosz-1.1.4.dist-info/RECORD +128 -0
  126. pychnosz-1.1.4.dist-info/WHEEL +5 -0
  127. pychnosz-1.1.4.dist-info/licenses/LICENSE.txt +19 -0
  128. pychnosz-1.1.4.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