pychnosz 1.1.12__cp310-cp310-macosx_15_0_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.
- pychnosz/.dylibs/libgcc_s.1.1.dylib +0 -0
- pychnosz/.dylibs/libgfortran.5.dylib +0 -0
- pychnosz/.dylibs/libquadmath.0.dylib +0 -0
- pychnosz/__init__.py +129 -0
- pychnosz/_version.py +34 -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/.gitignore +6 -0
- pychnosz/fortran/__init__.py +16 -0
- pychnosz/fortran/h2o92.dylib +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.12.dist-info/METADATA +197 -0
- pychnosz-1.1.12.dist-info/RECORD +133 -0
- pychnosz-1.1.12.dist-info/WHEEL +5 -0
- pychnosz-1.1.12.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.12.dist-info/top_level.txt +1 -0
pychnosz/core/balance.py
ADDED
|
@@ -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']
|