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,580 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chemical speciation calculation engine for CHNOSZ.
|
|
3
|
+
|
|
4
|
+
This module provides high-level functions for chemical speciation calculations,
|
|
5
|
+
including predominance diagrams, activity-pH diagrams, and Eh-pH diagrams.
|
|
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
|
+
# Optional matplotlib import
|
|
14
|
+
try:
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
HAS_MATPLOTLIB = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_MATPLOTLIB = False
|
|
19
|
+
plt = None
|
|
20
|
+
|
|
21
|
+
from .subcrt import subcrt
|
|
22
|
+
from .equilibrium import EquilibriumSolver
|
|
23
|
+
from .thermo import thermo
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpeciationEngine:
|
|
27
|
+
"""
|
|
28
|
+
Chemical speciation calculation engine.
|
|
29
|
+
|
|
30
|
+
This class provides methods for creating predominance diagrams,
|
|
31
|
+
activity-concentration diagrams, and other speciation plots.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Initialize the speciation engine."""
|
|
36
|
+
self.equilibrium_solver = EquilibriumSolver()
|
|
37
|
+
|
|
38
|
+
def predominance_diagram(self, element: str,
|
|
39
|
+
basis_species: List[str],
|
|
40
|
+
T: float = 298.15, P: float = 1.0,
|
|
41
|
+
pH_range: Tuple[float, float] = (0, 14),
|
|
42
|
+
pe_range: Optional[Tuple[float, float]] = None,
|
|
43
|
+
ionic_strength: float = 0.1,
|
|
44
|
+
resolution: int = 100) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Calculate predominance diagram for an element.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
element : str
|
|
51
|
+
Element symbol (e.g., 'Fe', 'S', 'C')
|
|
52
|
+
basis_species : list of str
|
|
53
|
+
Basis species for the calculation
|
|
54
|
+
T : float, default 298.15
|
|
55
|
+
Temperature in Kelvin
|
|
56
|
+
P : float, default 1.0
|
|
57
|
+
Pressure in bar
|
|
58
|
+
pH_range : tuple, default (0, 14)
|
|
59
|
+
pH range for diagram
|
|
60
|
+
pe_range : tuple, optional
|
|
61
|
+
pe (redox potential) range. If None, creates pH-only diagram
|
|
62
|
+
ionic_strength : float, default 0.1
|
|
63
|
+
Solution ionic strength (mol/kg)
|
|
64
|
+
resolution : int, default 100
|
|
65
|
+
Grid resolution for calculations
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
dict
|
|
70
|
+
Dictionary containing:
|
|
71
|
+
- 'pH': pH grid
|
|
72
|
+
- 'pe': pe grid (if pe_range provided)
|
|
73
|
+
- 'predominant': Predominant species grid
|
|
74
|
+
- 'activities': Activity grids for each species
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# Get all species containing the element
|
|
78
|
+
element_species = self._find_element_species(element)
|
|
79
|
+
|
|
80
|
+
if not element_species:
|
|
81
|
+
raise ValueError(f"No species found containing element {element}")
|
|
82
|
+
|
|
83
|
+
# Create grid
|
|
84
|
+
pH_grid = np.linspace(pH_range[0], pH_range[1], resolution)
|
|
85
|
+
|
|
86
|
+
if pe_range is not None:
|
|
87
|
+
pe_grid = np.linspace(pe_range[0], pe_range[1], resolution)
|
|
88
|
+
pH_mesh, pe_mesh = np.meshgrid(pH_grid, pe_grid)
|
|
89
|
+
predominant = np.zeros_like(pH_mesh, dtype=int)
|
|
90
|
+
activities = {species: np.zeros_like(pH_mesh) for species in element_species}
|
|
91
|
+
else:
|
|
92
|
+
pe_mesh = None
|
|
93
|
+
predominant = np.zeros_like(pH_grid, dtype=int)
|
|
94
|
+
activities = {species: np.zeros_like(pH_grid) for species in element_species}
|
|
95
|
+
|
|
96
|
+
# Calculate speciation at each grid point
|
|
97
|
+
for i, pH in enumerate(pH_grid):
|
|
98
|
+
if pe_range is not None:
|
|
99
|
+
for j, pe in enumerate(pe_grid):
|
|
100
|
+
spec_result = self._calculate_point_speciation(
|
|
101
|
+
element_species, basis_species, pH, pe, T, P, ionic_strength)
|
|
102
|
+
|
|
103
|
+
# Find predominant species
|
|
104
|
+
max_activity = -np.inf
|
|
105
|
+
max_idx = 0
|
|
106
|
+
for k, species in enumerate(element_species):
|
|
107
|
+
activity = spec_result['activities'].get(species, 1e-20)
|
|
108
|
+
activities[species][j, i] = activity
|
|
109
|
+
if activity > max_activity:
|
|
110
|
+
max_activity = activity
|
|
111
|
+
max_idx = k
|
|
112
|
+
|
|
113
|
+
predominant[j, i] = max_idx
|
|
114
|
+
else:
|
|
115
|
+
spec_result = self._calculate_point_speciation(
|
|
116
|
+
element_species, basis_species, pH, 0, T, P, ionic_strength)
|
|
117
|
+
|
|
118
|
+
# Find predominant species
|
|
119
|
+
max_activity = -np.inf
|
|
120
|
+
max_idx = 0
|
|
121
|
+
for k, species in enumerate(element_species):
|
|
122
|
+
activity = spec_result['activities'].get(species, 1e-20)
|
|
123
|
+
activities[species][i] = activity
|
|
124
|
+
if activity > max_activity:
|
|
125
|
+
max_activity = activity
|
|
126
|
+
max_idx = k
|
|
127
|
+
|
|
128
|
+
predominant[i] = max_idx
|
|
129
|
+
|
|
130
|
+
result = {
|
|
131
|
+
'pH': pH_grid,
|
|
132
|
+
'predominant': predominant,
|
|
133
|
+
'activities': activities,
|
|
134
|
+
'species_names': element_species
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if pe_range is not None:
|
|
138
|
+
result['pe'] = pe_grid
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
def activity_diagram(self, species: List[str],
|
|
143
|
+
x_variable: str, x_range: Tuple[float, float],
|
|
144
|
+
y_variable: str, y_range: Tuple[float, float],
|
|
145
|
+
T: float = 298.15, P: float = 1.0,
|
|
146
|
+
total_concentrations: Optional[Dict[str, float]] = None,
|
|
147
|
+
resolution: int = 100) -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Calculate activity diagram (e.g., activity vs pH, Eh-pH).
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
species : list of str
|
|
154
|
+
Species to include in diagram
|
|
155
|
+
x_variable : str
|
|
156
|
+
X-axis variable ('pH', 'pe', 'log_activity', etc.)
|
|
157
|
+
x_range : tuple
|
|
158
|
+
Range for x variable
|
|
159
|
+
y_variable : str
|
|
160
|
+
Y-axis variable
|
|
161
|
+
y_range : tuple
|
|
162
|
+
Range for y variable
|
|
163
|
+
T : float, default 298.15
|
|
164
|
+
Temperature in Kelvin
|
|
165
|
+
P : float, default 1.0
|
|
166
|
+
Pressure in bar
|
|
167
|
+
total_concentrations : dict, optional
|
|
168
|
+
Total concentrations for components
|
|
169
|
+
resolution : int, default 100
|
|
170
|
+
Grid resolution
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
dict
|
|
175
|
+
Activity diagram results
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
# Create grids
|
|
179
|
+
x_grid = np.linspace(x_range[0], x_range[1], resolution)
|
|
180
|
+
y_grid = np.linspace(y_range[0], y_range[1], resolution)
|
|
181
|
+
X, Y = np.meshgrid(x_grid, y_grid)
|
|
182
|
+
|
|
183
|
+
# Initialize result grids
|
|
184
|
+
activities = {species: np.zeros_like(X) for species in species}
|
|
185
|
+
|
|
186
|
+
# Calculate at each grid point
|
|
187
|
+
for i in range(len(y_grid)):
|
|
188
|
+
for j in range(len(x_grid)):
|
|
189
|
+
x_val, y_val = X[i, j], Y[i, j]
|
|
190
|
+
|
|
191
|
+
# Set up calculation conditions
|
|
192
|
+
conditions = {
|
|
193
|
+
'T': T,
|
|
194
|
+
'P': P,
|
|
195
|
+
x_variable: x_val,
|
|
196
|
+
y_variable: y_val
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Calculate speciation (simplified)
|
|
200
|
+
try:
|
|
201
|
+
for k, sp in enumerate(species):
|
|
202
|
+
# This would be a full speciation calculation
|
|
203
|
+
# For now, use placeholder values
|
|
204
|
+
activities[sp][i, j] = 1e-6 # Placeholder
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
warnings.warn(f"Calculation failed at {x_variable}={x_val}, {y_variable}={y_val}: {e}")
|
|
208
|
+
for sp in species:
|
|
209
|
+
activities[sp][i, j] = np.nan
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
x_variable: x_grid,
|
|
213
|
+
y_variable: y_grid,
|
|
214
|
+
'activities': activities,
|
|
215
|
+
'species': species
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def mosaic_diagram(self, basis1: str, basis2: str,
|
|
219
|
+
range1: Tuple[float, float], range2: Tuple[float, float],
|
|
220
|
+
T: float = 298.15, P: float = 1.0,
|
|
221
|
+
resolution: int = 50) -> Dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Calculate mosaic diagram (stability fields of basis species).
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
basis1, basis2 : str
|
|
228
|
+
Two basis species that define the diagram axes
|
|
229
|
+
range1, range2 : tuple
|
|
230
|
+
Activity ranges for the two basis species
|
|
231
|
+
T : float, default 298.15
|
|
232
|
+
Temperature in Kelvin
|
|
233
|
+
P : float, default 1.0
|
|
234
|
+
Pressure in bar
|
|
235
|
+
resolution : int, default 50
|
|
236
|
+
Grid resolution
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
dict
|
|
241
|
+
Mosaic diagram results
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Create grid
|
|
245
|
+
log_a1 = np.linspace(range1[0], range1[1], resolution)
|
|
246
|
+
log_a2 = np.linspace(range2[0], range2[1], resolution)
|
|
247
|
+
A1, A2 = np.meshgrid(log_a1, log_a2)
|
|
248
|
+
|
|
249
|
+
# Find all species that can be formed from these basis species
|
|
250
|
+
formed_species = self._find_formed_species([basis1, basis2])
|
|
251
|
+
|
|
252
|
+
# Calculate stability fields
|
|
253
|
+
stable_species = np.zeros_like(A1, dtype=int)
|
|
254
|
+
|
|
255
|
+
for i in range(len(log_a2)):
|
|
256
|
+
for j in range(len(log_a1)):
|
|
257
|
+
activities = {basis1: 10**A1[i, j], basis2: 10**A2[i, j]}
|
|
258
|
+
|
|
259
|
+
# Find most stable species at this point
|
|
260
|
+
min_energy = np.inf
|
|
261
|
+
stable_idx = 0
|
|
262
|
+
|
|
263
|
+
for k, species in enumerate(formed_species):
|
|
264
|
+
try:
|
|
265
|
+
# Calculate formation energy (simplified)
|
|
266
|
+
energy = self._calculate_formation_energy(species, activities, T, P)
|
|
267
|
+
if energy < min_energy:
|
|
268
|
+
min_energy = energy
|
|
269
|
+
stable_idx = k
|
|
270
|
+
except:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
stable_species[i, j] = stable_idx
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
basis1: log_a1,
|
|
277
|
+
basis2: log_a2,
|
|
278
|
+
'stable_species': stable_species,
|
|
279
|
+
'species_names': formed_species
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def solubility_diagram(self, mineral: str, aqueous_species: List[str],
|
|
283
|
+
pH_range: Tuple[float, float] = (0, 14),
|
|
284
|
+
T: float = 298.15, P: float = 1.0,
|
|
285
|
+
resolution: int = 100) -> Dict[str, Any]:
|
|
286
|
+
"""
|
|
287
|
+
Calculate mineral solubility diagram.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
mineral : str
|
|
292
|
+
Mineral name
|
|
293
|
+
aqueous_species : list of str
|
|
294
|
+
Aqueous species in equilibrium with mineral
|
|
295
|
+
pH_range : tuple, default (0, 14)
|
|
296
|
+
pH range for calculation
|
|
297
|
+
T : float, default 298.15
|
|
298
|
+
Temperature in Kelvin
|
|
299
|
+
P : float, default 1.0
|
|
300
|
+
Pressure in bar
|
|
301
|
+
resolution : int, default 100
|
|
302
|
+
Grid resolution
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
dict
|
|
307
|
+
Solubility diagram results
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
pH_grid = np.linspace(pH_range[0], pH_range[1], resolution)
|
|
311
|
+
|
|
312
|
+
# Calculate dissolution reaction
|
|
313
|
+
dissolution_reactions = self._get_dissolution_reactions(mineral, aqueous_species)
|
|
314
|
+
|
|
315
|
+
# Calculate solubility at each pH
|
|
316
|
+
total_solubility = np.zeros_like(pH_grid)
|
|
317
|
+
species_concentrations = {sp: np.zeros_like(pH_grid) for sp in aqueous_species}
|
|
318
|
+
|
|
319
|
+
for i, pH in enumerate(pH_grid):
|
|
320
|
+
# Calculate equilibrium with mineral
|
|
321
|
+
try:
|
|
322
|
+
# This would involve solving the dissolution equilibrium
|
|
323
|
+
# For now, use placeholder calculations
|
|
324
|
+
for j, species in enumerate(aqueous_species):
|
|
325
|
+
# Simplified pH dependence
|
|
326
|
+
if '+' in species: # Cation
|
|
327
|
+
conc = 1e-6 * 10**(-pH)
|
|
328
|
+
elif '-' in species: # Anion
|
|
329
|
+
conc = 1e-6 * 10**(pH - 14)
|
|
330
|
+
else: # Neutral
|
|
331
|
+
conc = 1e-6
|
|
332
|
+
|
|
333
|
+
species_concentrations[species][i] = conc
|
|
334
|
+
total_solubility[i] += conc
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
warnings.warn(f"Solubility calculation failed at pH {pH}: {e}")
|
|
338
|
+
total_solubility[i] = np.nan
|
|
339
|
+
for species in aqueous_species:
|
|
340
|
+
species_concentrations[species][i] = np.nan
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
'pH': pH_grid,
|
|
344
|
+
'total_solubility': total_solubility,
|
|
345
|
+
'species_concentrations': species_concentrations,
|
|
346
|
+
'mineral': mineral
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
def plot_predominance(self, diagram_data: Dict[str, Any],
|
|
350
|
+
title: Optional[str] = None,
|
|
351
|
+
figsize: Tuple[int, int] = (10, 8)):
|
|
352
|
+
"""
|
|
353
|
+
Plot predominance diagram.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
diagram_data : dict
|
|
358
|
+
Data from predominance_diagram()
|
|
359
|
+
title : str, optional
|
|
360
|
+
Plot title
|
|
361
|
+
figsize : tuple, default (10, 8)
|
|
362
|
+
Figure size
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
matplotlib Figure or None
|
|
367
|
+
The plotted figure (if matplotlib available)
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
if not HAS_MATPLOTLIB:
|
|
371
|
+
print("Matplotlib not available - cannot create plot")
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
375
|
+
|
|
376
|
+
pH = diagram_data['pH']
|
|
377
|
+
predominant = diagram_data['predominant']
|
|
378
|
+
species_names = diagram_data['species_names']
|
|
379
|
+
|
|
380
|
+
if 'pe' in diagram_data:
|
|
381
|
+
# 2D diagram (pH-pe)
|
|
382
|
+
pe = diagram_data['pe']
|
|
383
|
+
pH_mesh, pe_mesh = np.meshgrid(pH, pe)
|
|
384
|
+
|
|
385
|
+
# Create color map for species
|
|
386
|
+
n_species = len(species_names)
|
|
387
|
+
colors = plt.cm.tab10(np.linspace(0, 1, n_species))
|
|
388
|
+
|
|
389
|
+
contour = ax.contourf(pH_mesh, pe_mesh, predominant,
|
|
390
|
+
levels=np.arange(n_species + 1) - 0.5,
|
|
391
|
+
colors=colors[:n_species])
|
|
392
|
+
|
|
393
|
+
ax.set_xlabel('pH')
|
|
394
|
+
ax.set_ylabel('pe')
|
|
395
|
+
|
|
396
|
+
# Create legend
|
|
397
|
+
handles = [plt.Rectangle((0,0),1,1, color=colors[i])
|
|
398
|
+
for i in range(n_species)]
|
|
399
|
+
ax.legend(handles, species_names, loc='center left',
|
|
400
|
+
bbox_to_anchor=(1, 0.5))
|
|
401
|
+
|
|
402
|
+
else:
|
|
403
|
+
# 1D diagram (pH only)
|
|
404
|
+
# Convert to step plot showing predominance regions
|
|
405
|
+
pH_edges = np.diff(pH)
|
|
406
|
+
predominant_species = [species_names[i] for i in predominant]
|
|
407
|
+
|
|
408
|
+
# Find boundaries
|
|
409
|
+
boundaries = []
|
|
410
|
+
current_species = predominant[0]
|
|
411
|
+
|
|
412
|
+
for i, species_idx in enumerate(predominant[1:], 1):
|
|
413
|
+
if species_idx != current_species:
|
|
414
|
+
boundaries.append(pH[i])
|
|
415
|
+
current_species = species_idx
|
|
416
|
+
|
|
417
|
+
ax.step(pH, [species_names[i] for i in predominant], where='post')
|
|
418
|
+
ax.set_xlabel('pH')
|
|
419
|
+
ax.set_ylabel('Predominant Species')
|
|
420
|
+
|
|
421
|
+
# Add boundary lines
|
|
422
|
+
for boundary in boundaries:
|
|
423
|
+
ax.axvline(x=boundary, color='red', linestyle='--', alpha=0.7)
|
|
424
|
+
|
|
425
|
+
if title:
|
|
426
|
+
ax.set_title(title)
|
|
427
|
+
|
|
428
|
+
plt.tight_layout()
|
|
429
|
+
return fig
|
|
430
|
+
|
|
431
|
+
def _find_element_species(self, element: str) -> List[str]:
|
|
432
|
+
"""Find all species in database containing the specified element."""
|
|
433
|
+
|
|
434
|
+
if thermo.obigt is None:
|
|
435
|
+
raise ValueError("OBIGT database not loaded")
|
|
436
|
+
|
|
437
|
+
# Search for species containing the element in their formula
|
|
438
|
+
element_species = []
|
|
439
|
+
|
|
440
|
+
for _, row in thermo.obigt.iterrows():
|
|
441
|
+
formula = str(row['formula'])
|
|
442
|
+
if element in formula:
|
|
443
|
+
element_species.append(row['name'])
|
|
444
|
+
|
|
445
|
+
return element_species
|
|
446
|
+
|
|
447
|
+
def _calculate_point_speciation(self, species: List[str], basis_species: List[str],
|
|
448
|
+
pH: float, pe: float, T: float, P: float,
|
|
449
|
+
ionic_strength: float) -> Dict[str, Any]:
|
|
450
|
+
"""Calculate speciation at a single point."""
|
|
451
|
+
|
|
452
|
+
# This would involve full equilibrium calculation
|
|
453
|
+
# For now, return simplified results
|
|
454
|
+
|
|
455
|
+
activities = {}
|
|
456
|
+
for sp in species:
|
|
457
|
+
# Simplified activity calculation based on pH and pe
|
|
458
|
+
if '+' in sp: # Cation
|
|
459
|
+
activity = 1e-6 * 10**(-pH) * 10**(pe)
|
|
460
|
+
elif '-' in sp: # Anion
|
|
461
|
+
activity = 1e-6 * 10**(pH - 14) * 10**(-pe)
|
|
462
|
+
else: # Neutral
|
|
463
|
+
activity = 1e-6
|
|
464
|
+
|
|
465
|
+
activities[sp] = max(activity, 1e-20) # Avoid zero activities
|
|
466
|
+
|
|
467
|
+
return {'activities': activities}
|
|
468
|
+
|
|
469
|
+
def _find_formed_species(self, basis_species: List[str]) -> List[str]:
|
|
470
|
+
"""Find species that can be formed from the given basis species."""
|
|
471
|
+
|
|
472
|
+
# Placeholder - would search database for formation reactions
|
|
473
|
+
return basis_species # Simplified
|
|
474
|
+
|
|
475
|
+
def _calculate_formation_energy(self, species: str, activities: Dict[str, float],
|
|
476
|
+
T: float, P: float) -> float:
|
|
477
|
+
"""Calculate formation energy for a species."""
|
|
478
|
+
|
|
479
|
+
# Placeholder - would calculate actual formation reaction energy
|
|
480
|
+
return np.random.random() # Simplified
|
|
481
|
+
|
|
482
|
+
def _get_dissolution_reactions(self, mineral: str, aqueous_species: List[str]) -> Dict:
|
|
483
|
+
"""Get dissolution reactions for a mineral."""
|
|
484
|
+
|
|
485
|
+
# Placeholder - would look up actual dissolution reactions
|
|
486
|
+
return {}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Global speciation engine instance
|
|
490
|
+
_speciation_engine = SpeciationEngine()
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def diagram(basis: Optional[Union[str, List[str]]] = None,
|
|
494
|
+
species: Optional[Union[str, List[str]]] = None,
|
|
495
|
+
balance: Optional[Union[str, int]] = None,
|
|
496
|
+
loga_balance: Optional[Union[float, List[float]]] = None,
|
|
497
|
+
T: float = 298.15, P: float = 1.0,
|
|
498
|
+
res: int = 256,
|
|
499
|
+
**kwargs) -> Dict[str, Any]:
|
|
500
|
+
"""
|
|
501
|
+
Create chemical activity or predominance diagram.
|
|
502
|
+
|
|
503
|
+
Parameters
|
|
504
|
+
----------
|
|
505
|
+
basis : str, list, or None
|
|
506
|
+
Basis species for diagram axes
|
|
507
|
+
species : str, list, or None
|
|
508
|
+
Species to include in diagram
|
|
509
|
+
balance : str, int, or None
|
|
510
|
+
Balanced component
|
|
511
|
+
loga_balance : float, list, or None
|
|
512
|
+
Log activities for balance
|
|
513
|
+
T : float, default 298.15
|
|
514
|
+
Temperature in Kelvin
|
|
515
|
+
P : float, default 1.0
|
|
516
|
+
Pressure in bar
|
|
517
|
+
res : int, default 256
|
|
518
|
+
Diagram resolution
|
|
519
|
+
**kwargs
|
|
520
|
+
Additional parameters for diagram calculation
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
dict
|
|
525
|
+
Diagram calculation results
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
# This is a placeholder for the main diagram function
|
|
529
|
+
# It would coordinate with the basis system and species definitions
|
|
530
|
+
|
|
531
|
+
if basis is None or species is None:
|
|
532
|
+
raise ValueError("Both basis and species must be specified")
|
|
533
|
+
|
|
534
|
+
if isinstance(basis, str):
|
|
535
|
+
basis = [basis]
|
|
536
|
+
if isinstance(species, str):
|
|
537
|
+
species = [species]
|
|
538
|
+
|
|
539
|
+
# Create simple predominance diagram
|
|
540
|
+
result = _speciation_engine.predominance_diagram(
|
|
541
|
+
element=basis[0].split('+')[0].split('-')[0], # Extract element
|
|
542
|
+
basis_species=basis,
|
|
543
|
+
T=T, P=P,
|
|
544
|
+
resolution=res
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return result
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def mosaic(bases: List[str],
|
|
551
|
+
T: float = 298.15, P: float = 1.0,
|
|
552
|
+
resolution: int = 256) -> Dict[str, Any]:
|
|
553
|
+
"""
|
|
554
|
+
Create mosaic diagram showing stability fields.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
bases : list of str
|
|
559
|
+
Two basis species defining the diagram
|
|
560
|
+
T : float, default 298.15
|
|
561
|
+
Temperature in Kelvin
|
|
562
|
+
P : float, default 1.0
|
|
563
|
+
Pressure in bar
|
|
564
|
+
resolution : int, default 256
|
|
565
|
+
Diagram resolution
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
dict
|
|
570
|
+
Mosaic diagram results
|
|
571
|
+
"""
|
|
572
|
+
|
|
573
|
+
if len(bases) != 2:
|
|
574
|
+
raise ValueError("Exactly two basis species required for mosaic diagram")
|
|
575
|
+
|
|
576
|
+
return _speciation_engine.mosaic_diagram(
|
|
577
|
+
bases[0], bases[1],
|
|
578
|
+
range1=(-12, 0), range2=(-12, 0), # Default activity ranges
|
|
579
|
+
T=T, P=P, resolution=resolution
|
|
580
|
+
)
|