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