pychnosz 1.1.1__cp311-cp311-macosx_10_13_x86_64.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 (131) hide show
  1. pychnosz/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. pychnosz/.dylibs/libgfortran.5.dylib +0 -0
  3. pychnosz/.dylibs/libquadmath.0.dylib +0 -0
  4. pychnosz/__init__.py +129 -0
  5. pychnosz/biomolecules/__init__.py +29 -0
  6. pychnosz/biomolecules/ionize_aa.py +197 -0
  7. pychnosz/biomolecules/proteins.py +595 -0
  8. pychnosz/core/__init__.py +46 -0
  9. pychnosz/core/affinity.py +1256 -0
  10. pychnosz/core/animation.py +593 -0
  11. pychnosz/core/balance.py +334 -0
  12. pychnosz/core/basis.py +716 -0
  13. pychnosz/core/diagram.py +3336 -0
  14. pychnosz/core/equilibrate.py +813 -0
  15. pychnosz/core/equilibrium.py +554 -0
  16. pychnosz/core/info.py +821 -0
  17. pychnosz/core/retrieve.py +364 -0
  18. pychnosz/core/speciation.py +580 -0
  19. pychnosz/core/species.py +599 -0
  20. pychnosz/core/subcrt.py +1700 -0
  21. pychnosz/core/thermo.py +593 -0
  22. pychnosz/core/unicurve.py +1226 -0
  23. pychnosz/data/__init__.py +11 -0
  24. pychnosz/data/add_obigt.py +327 -0
  25. pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
  26. pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
  27. pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
  28. pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
  29. pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
  30. pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
  31. pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
  32. pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
  33. pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
  34. pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
  35. pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
  36. pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
  37. pychnosz/data/extdata/Berman/sympy.R +99 -0
  38. pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
  39. pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
  40. pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
  41. pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
  42. pychnosz/data/extdata/OBIGT/AD.csv +25 -0
  43. pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
  44. pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
  45. pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
  46. pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
  47. pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
  48. pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
  49. pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
  50. pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
  51. pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
  52. pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
  53. pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
  54. pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
  55. pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
  56. pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
  57. pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
  58. pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
  59. pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
  60. pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
  61. pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
  62. pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
  63. pychnosz/data/extdata/misc/BZA10.csv +5 -0
  64. pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
  65. pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
  66. pychnosz/data/extdata/misc/LA19_test.csv +7 -0
  67. pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
  68. pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
  69. pychnosz/data/extdata/misc/PM90.csv +7 -0
  70. pychnosz/data/extdata/misc/RH95.csv +23 -0
  71. pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
  72. pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
  73. pychnosz/data/extdata/misc/SK95.csv +55 -0
  74. pychnosz/data/extdata/misc/SOJSH.csv +61 -0
  75. pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
  76. pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
  77. pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
  78. pychnosz/data/extdata/misc/bluered.txt +1000 -0
  79. pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
  80. pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
  81. pychnosz/data/extdata/protein/Cas/download.R +34 -0
  82. pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
  83. pychnosz/data/extdata/protein/POLG.csv +12 -0
  84. pychnosz/data/extdata/protein/TBD+05.csv +393 -0
  85. pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
  86. pychnosz/data/extdata/protein/rubisco.csv +28 -0
  87. pychnosz/data/extdata/protein/rubisco.fasta +239 -0
  88. pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
  89. pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
  90. pychnosz/data/extdata/src/README.txt +5 -0
  91. pychnosz/data/extdata/taxonomy/names.dmp +215 -0
  92. pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
  93. pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
  94. pychnosz/data/extdata/thermo/buffer.csv +40 -0
  95. pychnosz/data/extdata/thermo/element.csv +135 -0
  96. pychnosz/data/extdata/thermo/groups.csv +6 -0
  97. pychnosz/data/extdata/thermo/opt.csv +2 -0
  98. pychnosz/data/extdata/thermo/protein.csv +506 -0
  99. pychnosz/data/extdata/thermo/refs.csv +343 -0
  100. pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
  101. pychnosz/data/loader.py +431 -0
  102. pychnosz/data/mod_obigt.py +322 -0
  103. pychnosz/data/obigt.py +471 -0
  104. pychnosz/data/worm.py +228 -0
  105. pychnosz/fortran/__init__.py +16 -0
  106. pychnosz/fortran/h2o92.dylib +0 -0
  107. pychnosz/fortran/h2o92_interface.py +527 -0
  108. pychnosz/geochemistry/__init__.py +21 -0
  109. pychnosz/geochemistry/minerals.py +514 -0
  110. pychnosz/geochemistry/redox.py +500 -0
  111. pychnosz/models/__init__.py +47 -0
  112. pychnosz/models/archer_wang.py +165 -0
  113. pychnosz/models/berman.py +309 -0
  114. pychnosz/models/cgl.py +381 -0
  115. pychnosz/models/dew.py +997 -0
  116. pychnosz/models/hkf.py +523 -0
  117. pychnosz/models/hkf_helpers.py +222 -0
  118. pychnosz/models/iapws95.py +1113 -0
  119. pychnosz/models/supcrt92_fortran.py +238 -0
  120. pychnosz/models/water.py +480 -0
  121. pychnosz/utils/__init__.py +27 -0
  122. pychnosz/utils/expression.py +1074 -0
  123. pychnosz/utils/formula.py +830 -0
  124. pychnosz/utils/formula_ox.py +227 -0
  125. pychnosz/utils/reset.py +33 -0
  126. pychnosz/utils/units.py +259 -0
  127. pychnosz-1.1.1.dist-info/METADATA +197 -0
  128. pychnosz-1.1.1.dist-info/RECORD +131 -0
  129. pychnosz-1.1.1.dist-info/WHEEL +5 -0
  130. pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
  131. pychnosz-1.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,334 @@
1
+ """
2
+ Reaction balancing utilities.
3
+
4
+ This module provides functions to balance chemical reactions using basis species,
5
+ without calculating thermodynamic properties. This is more efficient than subcrt()
6
+ when only the reaction stoichiometry is needed.
7
+ """
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from typing import List, Union, Optional, Tuple
12
+
13
+ from ..core.thermo import thermo
14
+ from ..core.info import info
15
+ from ..utils.formula import makeup
16
+
17
+
18
+ def balance_reaction(species: Union[str, List[str], int, List[int]],
19
+ coeff: Union[int, float, List[Union[int, float]]],
20
+ state: Optional[Union[str, List[str]]] = None,
21
+ basis: Optional[pd.DataFrame] = None,
22
+ messages: bool = False) -> Optional[Tuple[List, List]]:
23
+ """
24
+ Balance a chemical reaction using basis species.
25
+
26
+ This function checks if a reaction is balanced and, if not, attempts to
27
+ balance it by adding basis species. Unlike subcrt(), this function only
28
+ performs the balancing calculation without computing thermodynamic properties,
29
+ making it much more efficient for reaction generation.
30
+
31
+ Parameters
32
+ ----------
33
+ species : str, int, list of str, or list of int
34
+ Species names or indices in the reaction
35
+ coeff : int, float, or list
36
+ Stoichiometric coefficients for the species
37
+ state : str, list of str, or None
38
+ Physical states for species (optional)
39
+ basis : pd.DataFrame, optional
40
+ Basis species definition to use. If None, uses global basis from thermo()
41
+ messages : bool
42
+ Whether to print informational messages
43
+
44
+ Returns
45
+ -------
46
+ tuple or None
47
+ If reaction is balanced or can be balanced:
48
+ (balanced_species, balanced_coeffs) where both are lists
49
+ If reaction cannot be balanced:
50
+ None
51
+
52
+ Examples
53
+ --------
54
+ >>> import chnosz
55
+ >>> pychnosz.reset()
56
+ >>> pychnosz.basis(['H2O', 'H+', 'Fe+2'])
57
+ >>> # Balance reaction for Fe+3
58
+ >>> species, coeffs = balance_reaction('Fe+3', [-1])
59
+ >>> print(f"Species: {species}")
60
+ >>> print(f"Coefficients: {coeffs}")
61
+ """
62
+
63
+ # Convert inputs to lists
64
+ if not isinstance(species, list):
65
+ species = [species]
66
+ if not isinstance(coeff, list):
67
+ coeff = [coeff]
68
+ if state is not None and not isinstance(state, list):
69
+ state = [state]
70
+
71
+ # Validate lengths
72
+ if len(species) != len(coeff):
73
+ raise ValueError("Length of species and coeff must match")
74
+
75
+ # Get basis definition
76
+ thermo_sys = thermo()
77
+ if basis is None:
78
+ if hasattr(thermo_sys, 'basis') and thermo_sys.basis is not None:
79
+ basis = thermo_sys.basis
80
+ else:
81
+ raise RuntimeError("Basis species not defined. Call pychnosz.basis() first.")
82
+
83
+ # Look up species indices
84
+ ispecies = []
85
+ for i, sp in enumerate(species):
86
+ if isinstance(sp, (int, np.integer)):
87
+ ispecies.append(int(sp))
88
+ else:
89
+ sp_state = state[i] if state and i < len(state) else None
90
+ sp_idx = info(sp, sp_state, messages=messages)
91
+ if sp_idx is None or (isinstance(sp_idx, float) and np.isnan(sp_idx)):
92
+ raise ValueError(f"Species not found: {sp}")
93
+ ispecies.append(sp_idx)
94
+
95
+ # Calculate mass balance
96
+ try:
97
+ mass_balance = makeup(ispecies, coeff, sum_formulas=True)
98
+
99
+ # Check if balanced
100
+ tolerance = 1e-6
101
+ unbalanced_elements = {elem: val for elem, val in mass_balance.items()
102
+ if abs(val) > tolerance}
103
+
104
+ if not unbalanced_elements:
105
+ # Already balanced
106
+ if messages:
107
+ print("Reaction is already balanced")
108
+ return (species, coeff)
109
+
110
+ # Reaction is unbalanced - try to balance using basis species
111
+ missing_composition = {elem: -val for elem, val in unbalanced_elements.items()}
112
+
113
+ if messages:
114
+ print("Reaction is not balanced; missing composition:")
115
+ elem_names = list(missing_composition.keys())
116
+ elem_values = list(missing_composition.values())
117
+ print(" ".join(elem_names))
118
+ print(" ".join([f"{val:.4f}" for val in elem_values]))
119
+
120
+ # Get basis element columns
121
+ basis_elements = [col for col in basis.columns
122
+ if col not in ['ispecies', 'logact', 'state']]
123
+
124
+ # Check if all missing elements are in basis
125
+ missing_elements = set(missing_composition.keys())
126
+ if not missing_elements.issubset(set(basis_elements)):
127
+ if messages:
128
+ print(f"Cannot balance: elements {missing_elements - set(basis_elements)} not in basis")
129
+ return None
130
+
131
+ # Calculate coefficients for missing composition from basis species
132
+ missing_matrix = np.zeros((1, len(basis_elements)))
133
+ for i, elem in enumerate(basis_elements):
134
+ missing_matrix[0, i] = missing_composition.get(elem, 0)
135
+
136
+ # Get basis matrix
137
+ basis_matrix = basis[basis_elements].values.T # Transpose: (elements × basis_species)
138
+
139
+ try:
140
+ # Try to find simple integer solutions first
141
+ basis_coeffs = _find_simple_integer_solution(
142
+ basis_matrix.T,
143
+ missing_matrix.flatten(),
144
+ basis['ispecies'].tolist(),
145
+ missing_composition
146
+ )
147
+
148
+ if basis_coeffs is None:
149
+ # Fall back to linear algebra solution
150
+ basis_coeffs = np.linalg.solve(basis_matrix, missing_matrix.T).flatten()
151
+
152
+ # Apply zapsmall equivalent (digits=7)
153
+ basis_coeffs = np.around(basis_coeffs, decimals=7)
154
+
155
+ # Clean up very small numbers
156
+ basis_coeffs[np.abs(basis_coeffs) < 1e-7] = 0
157
+
158
+ # Get non-zero coefficients and corresponding basis species
159
+ nonzero_indices = np.abs(basis_coeffs) > 1e-6
160
+ if not np.any(nonzero_indices):
161
+ if messages:
162
+ print("No basis species needed to balance (coefficients are zero)")
163
+ return (species, coeff)
164
+
165
+ # Get basis species info
166
+ basis_indices = basis['ispecies'].values[nonzero_indices]
167
+ basis_coeffs_nz = basis_coeffs[nonzero_indices]
168
+
169
+ # Create new species list and coefficients
170
+ new_species = list(species) + [int(idx) for idx in basis_indices]
171
+ new_coeff = list(coeff) + list(basis_coeffs_nz)
172
+
173
+ if messages:
174
+ print("Balanced reaction by adding basis species:")
175
+ for sp_idx, cf in zip(basis_indices, basis_coeffs_nz):
176
+ sp_name = thermo_sys.obigt.loc[int(sp_idx)]['name']
177
+ print(f" {cf:.4f} {sp_name}")
178
+
179
+ # CRITICAL: Consolidate duplicate species by summing coefficients
180
+ # This prevents infinite recursion and matches subcrt's behavior
181
+ consolidated_species = []
182
+ consolidated_coeffs = []
183
+
184
+ # Convert all species to indices for consolidation
185
+ species_indices = []
186
+ for sp in new_species:
187
+ if isinstance(sp, (int, np.integer)):
188
+ species_indices.append(int(sp))
189
+ else:
190
+ sp_idx = info(sp, None, messages=False)
191
+ if sp_idx is None or (isinstance(sp_idx, float) and np.isnan(sp_idx)):
192
+ # Keep as string if not found
193
+ species_indices.append(sp)
194
+ else:
195
+ species_indices.append(sp_idx)
196
+
197
+ # Group by species index and sum coefficients
198
+ species_coeff_map = {}
199
+ for sp_idx, coeff in zip(species_indices, new_coeff):
200
+ if sp_idx in species_coeff_map:
201
+ species_coeff_map[sp_idx] += coeff
202
+ else:
203
+ species_coeff_map[sp_idx] = coeff
204
+
205
+ # Remove species with zero coefficient (cancelled out)
206
+ for sp_idx, coeff in species_coeff_map.items():
207
+ if abs(coeff) > tolerance:
208
+ consolidated_species.append(sp_idx)
209
+ consolidated_coeffs.append(coeff)
210
+
211
+ # Now check if consolidated reaction is balanced
212
+ # If not, recursively balance again
213
+ try:
214
+ final_mass_balance = makeup(consolidated_species, consolidated_coeffs, sum_formulas=True)
215
+ final_unbalanced = {elem: val for elem, val in final_mass_balance.items()
216
+ if abs(val) > tolerance}
217
+
218
+ if final_unbalanced:
219
+ # Still unbalanced after consolidation - recursively balance
220
+ if messages:
221
+ print(f"After consolidation, reaction still unbalanced: {final_unbalanced}")
222
+ print(f"Attempting recursive balance...")
223
+ return balance_reaction(consolidated_species, consolidated_coeffs, state=None,
224
+ basis=basis, messages=messages)
225
+ else:
226
+ # Balanced! Return consolidated result
227
+ if messages:
228
+ print(f"Reaction balanced after consolidation")
229
+ return (consolidated_species, consolidated_coeffs)
230
+
231
+ except Exception as e:
232
+ # If check fails, return consolidated result anyway
233
+ if messages:
234
+ print(f"Could not verify final balance: {e}")
235
+ return (consolidated_species, consolidated_coeffs)
236
+
237
+ except np.linalg.LinAlgError:
238
+ if messages:
239
+ print("Cannot balance: singular basis matrix")
240
+ return None
241
+
242
+ except Exception as e:
243
+ if messages:
244
+ print(f"Error checking reaction balance: {e}")
245
+ import traceback
246
+ traceback.print_exc()
247
+ return None
248
+
249
+
250
+ def _find_simple_integer_solution(basis_matrix, missing_vector, basis_species_indices, missing_composition):
251
+ """
252
+ Find simple integer solutions for basis species coefficients.
253
+
254
+ This tries to match R CHNOSZ behavior by preferring simple integer combinations
255
+ like 1 H2O + 1 NH3 over complex fractional solutions.
256
+ """
257
+ n_species = len(basis_species_indices)
258
+
259
+ # Try single species solutions first (coefficient 1-3)
260
+ for i in range(n_species):
261
+ for coeff in [1, 2, 3, -1, -2, -3]:
262
+ test_coeffs = np.zeros(n_species)
263
+ test_coeffs[i] = coeff
264
+ result = basis_matrix @ test_coeffs
265
+ if np.allclose(result, missing_vector, atol=1e-10):
266
+ return test_coeffs
267
+
268
+ # Try two-species solutions (coefficients ±1, ±2 each)
269
+ for i in range(n_species):
270
+ for j in range(i+1, n_species):
271
+ for coeff1 in [1, 2, -1, -2]:
272
+ for coeff2 in [1, 2, -1, -2]:
273
+ test_coeffs = np.zeros(n_species)
274
+ test_coeffs[i] = coeff1
275
+ test_coeffs[j] = coeff2
276
+ result = basis_matrix @ test_coeffs
277
+ if np.allclose(result, missing_vector, atol=1e-10):
278
+ return test_coeffs
279
+
280
+ # Try three-species solutions (coefficient ±1 each)
281
+ for i in range(n_species):
282
+ for j in range(i+1, n_species):
283
+ for k in range(j+1, n_species):
284
+ for coeff1 in [1, -1]:
285
+ for coeff2 in [1, -1]:
286
+ for coeff3 in [1, -1]:
287
+ test_coeffs = np.zeros(n_species)
288
+ test_coeffs[i] = coeff1
289
+ test_coeffs[j] = coeff2
290
+ test_coeffs[k] = coeff3
291
+ result = basis_matrix @ test_coeffs
292
+ if np.allclose(result, missing_vector, atol=1e-10):
293
+ return test_coeffs
294
+
295
+ return None # No simple solution found
296
+
297
+
298
+ def format_reaction(species: List[Union[str, int]], coeffs: List[float]) -> str:
299
+ """
300
+ Format a reaction as a string for EQ3/6 input.
301
+
302
+ Parameters
303
+ ----------
304
+ species : list
305
+ Species names or indices
306
+ coeffs : list
307
+ Stoichiometric coefficients
308
+
309
+ Returns
310
+ -------
311
+ str
312
+ Formatted reaction string like "-1.0000 Fe+3 1.0000 Fe+2 0.2500 O2(g)"
313
+ """
314
+ thermo_sys = thermo()
315
+ parts = []
316
+
317
+ for sp, coeff in zip(species, coeffs):
318
+ # Get species name if we have an index
319
+ if isinstance(sp, (int, np.integer)):
320
+ sp_name = thermo_sys.obigt.loc[int(sp)]['name']
321
+ else:
322
+ sp_name = sp
323
+
324
+ # Replace 'water' with 'H2O' for EQ3 compatibility
325
+ if sp_name == 'water':
326
+ sp_name = 'H2O'
327
+
328
+ parts.append(f"{coeff:.4f}")
329
+ parts.append(sp_name)
330
+
331
+ return " ".join(parts)
332
+
333
+
334
+ __all__ = ['balance_reaction', 'format_reaction']