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.
- 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 +1700 -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 +222 -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.4.dist-info/METADATA +197 -0
- pychnosz-1.1.4.dist-info/RECORD +128 -0
- pychnosz-1.1.4.dist-info/WHEEL +5 -0
- pychnosz-1.1.4.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.4.dist-info/top_level.txt +1 -0
pychnosz/core/basis.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basis species management module.
|
|
3
|
+
|
|
4
|
+
This module provides Python equivalents of the R functions in basis.R:
|
|
5
|
+
- basis(): Set up and manage basis species for thermodynamic calculations
|
|
6
|
+
- Basis species validation and stoichiometric matrix construction
|
|
7
|
+
- Buffer system support and preset basis definitions
|
|
8
|
+
|
|
9
|
+
Author: CHNOSZ Python port
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import numpy as np
|
|
14
|
+
from typing import Union, List, Optional, Dict, Any, Tuple
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
from .thermo import thermo
|
|
18
|
+
from .info import info, find_species
|
|
19
|
+
from ..utils.formula import makeup
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BasisError(Exception):
|
|
23
|
+
"""Exception raised for basis-related errors."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def basis(species: Optional[Union[str, int, List[Union[str, int]]]] = None,
|
|
28
|
+
state: Optional[Union[str, List[str]]] = None,
|
|
29
|
+
logact: Optional[Union[float, List[float]]] = None,
|
|
30
|
+
delete: bool = False,
|
|
31
|
+
add: bool = False,
|
|
32
|
+
messages: bool = True,
|
|
33
|
+
global_state: bool = True) -> Optional[pd.DataFrame]:
|
|
34
|
+
"""
|
|
35
|
+
Set up the basis species of a thermodynamic system.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
species : str, int, list, or None
|
|
40
|
+
Species name(s), formula(s), or index(es), or preset keyword.
|
|
41
|
+
If None, returns current basis definition.
|
|
42
|
+
state : str, list of str, or None
|
|
43
|
+
Physical state(s) for the species
|
|
44
|
+
logact : float, list of float, or None
|
|
45
|
+
Log activities for the basis species
|
|
46
|
+
delete : bool, default False
|
|
47
|
+
If True, delete the basis definition
|
|
48
|
+
add : bool, default False
|
|
49
|
+
If True, add to existing basis instead of replacing
|
|
50
|
+
messages : bool, default True
|
|
51
|
+
If True, print informational messages about species lookup
|
|
52
|
+
If False, suppress all output (equivalent to R's suppressMessages())
|
|
53
|
+
global_state : bool, default True
|
|
54
|
+
If True, store basis definition in global thermo().basis (default behavior)
|
|
55
|
+
If False, return basis definition without storing globally (local state)
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
pd.DataFrame or None
|
|
60
|
+
Basis species definition DataFrame, or None if deleted
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
>>> # Set up a simple basis
|
|
65
|
+
>>> basis(["H2O", "CO2", "NH3"], logact=[0, -3, -4])
|
|
66
|
+
|
|
67
|
+
>>> # Use a preset basis
|
|
68
|
+
>>> basis("CHNOS")
|
|
69
|
+
|
|
70
|
+
>>> # Add species to existing basis
|
|
71
|
+
>>> basis("Fe2O3", add=True)
|
|
72
|
+
|
|
73
|
+
>>> # Delete basis
|
|
74
|
+
>>> basis(delete=True)
|
|
75
|
+
|
|
76
|
+
>>> # Suppress messages
|
|
77
|
+
>>> basis("CHNOS", messages=False)
|
|
78
|
+
"""
|
|
79
|
+
thermo_obj = thermo()
|
|
80
|
+
|
|
81
|
+
# Get current basis
|
|
82
|
+
old_basis = thermo_obj.basis
|
|
83
|
+
|
|
84
|
+
# Delete basis if requested
|
|
85
|
+
if delete or species == "":
|
|
86
|
+
thermo_obj.basis = None
|
|
87
|
+
thermo_obj.species = None
|
|
88
|
+
return old_basis
|
|
89
|
+
|
|
90
|
+
# Return current basis if no species specified
|
|
91
|
+
if species is None:
|
|
92
|
+
return old_basis
|
|
93
|
+
|
|
94
|
+
# Handle empty species list
|
|
95
|
+
if isinstance(species, list) and len(species) == 0:
|
|
96
|
+
raise ValueError("species argument is empty")
|
|
97
|
+
|
|
98
|
+
# Check for preset keywords
|
|
99
|
+
if isinstance(species, str) and species in _get_preset_basis_keywords():
|
|
100
|
+
return preset_basis(species, messages=messages, global_state=global_state)
|
|
101
|
+
|
|
102
|
+
# Ensure species names are unique
|
|
103
|
+
if isinstance(species, list):
|
|
104
|
+
if len(set([str(s) for s in species])) != len(species):
|
|
105
|
+
raise ValueError("species names are not unique")
|
|
106
|
+
|
|
107
|
+
# Process arguments
|
|
108
|
+
species, state, logact = _process_basis_arguments(species, state, logact)
|
|
109
|
+
|
|
110
|
+
# Handle special transformations
|
|
111
|
+
species, logact = _handle_special_species(species, logact)
|
|
112
|
+
|
|
113
|
+
# Check if we're modifying existing basis species
|
|
114
|
+
if (old_basis is not None and not add and
|
|
115
|
+
_all_species_in_basis(species, old_basis)):
|
|
116
|
+
if state is not None or logact is not None:
|
|
117
|
+
return mod_basis(species, state, logact, messages=messages)
|
|
118
|
+
|
|
119
|
+
# Create new basis definition or add to existing
|
|
120
|
+
if logact is None:
|
|
121
|
+
logact = [0.0] * len(species)
|
|
122
|
+
|
|
123
|
+
# Get species indices
|
|
124
|
+
ispecies = _get_species_indices(species, state, messages=messages)
|
|
125
|
+
|
|
126
|
+
# Handle adding to existing basis
|
|
127
|
+
if add and old_basis is not None:
|
|
128
|
+
# Check for duplicates
|
|
129
|
+
existing_indices = old_basis['ispecies'].tolist()
|
|
130
|
+
for i, idx in enumerate(ispecies):
|
|
131
|
+
if idx in existing_indices:
|
|
132
|
+
sp_name = species[i] if isinstance(species[i], str) else str(species[i])
|
|
133
|
+
raise BasisError(f"Species {sp_name} is already in the basis definition")
|
|
134
|
+
|
|
135
|
+
# Append to existing basis
|
|
136
|
+
ispecies = existing_indices + ispecies
|
|
137
|
+
logact = old_basis['logact'].tolist() + logact
|
|
138
|
+
|
|
139
|
+
# Create new basis
|
|
140
|
+
new_basis = put_basis(ispecies, logact, global_state=global_state)
|
|
141
|
+
|
|
142
|
+
# Only update global species list if using global state
|
|
143
|
+
if global_state:
|
|
144
|
+
# Handle species list when adding
|
|
145
|
+
if add and thermo_obj.species is not None:
|
|
146
|
+
_update_species_for_added_basis(old_basis, new_basis)
|
|
147
|
+
else:
|
|
148
|
+
# Clear species since basis changed
|
|
149
|
+
from .species import species as species_func
|
|
150
|
+
species_func(delete=True)
|
|
151
|
+
|
|
152
|
+
return new_basis
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _process_basis_arguments(species, state, logact):
|
|
156
|
+
"""Process and validate basis function arguments."""
|
|
157
|
+
# Convert single values to lists
|
|
158
|
+
if not isinstance(species, list):
|
|
159
|
+
species = [species]
|
|
160
|
+
|
|
161
|
+
# Handle argument swapping for compatibility with R version
|
|
162
|
+
# If logact looks like states (strings), swap them
|
|
163
|
+
if logact is not None:
|
|
164
|
+
if isinstance(logact, list) and len(logact) > 0 and isinstance(logact[0], str):
|
|
165
|
+
state, logact = logact, state
|
|
166
|
+
elif isinstance(logact, str):
|
|
167
|
+
state, logact = logact, state
|
|
168
|
+
# If state is numeric, treat it as logact (like R CHNOSZ)
|
|
169
|
+
elif state is not None:
|
|
170
|
+
if isinstance(state, (int, float)):
|
|
171
|
+
state, logact = None, state
|
|
172
|
+
elif isinstance(state, list) and len(state) > 0 and isinstance(state[0], (int, float)):
|
|
173
|
+
state, logact = None, state
|
|
174
|
+
|
|
175
|
+
# Ensure consistent lengths
|
|
176
|
+
n_species = len(species)
|
|
177
|
+
if state is not None:
|
|
178
|
+
if isinstance(state, str):
|
|
179
|
+
state = [state] * n_species
|
|
180
|
+
else:
|
|
181
|
+
state = list(state)[:n_species] # Truncate if too long
|
|
182
|
+
state.extend([state[-1]] * (n_species - len(state))) # Extend if too short
|
|
183
|
+
|
|
184
|
+
if logact is not None:
|
|
185
|
+
if isinstance(logact, (int, float)):
|
|
186
|
+
logact = [float(logact)] * n_species
|
|
187
|
+
else:
|
|
188
|
+
logact = list(logact)[:n_species]
|
|
189
|
+
logact.extend([0.0] * (n_species - len(logact)))
|
|
190
|
+
|
|
191
|
+
return species, state, logact
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _handle_special_species(species, logact):
|
|
195
|
+
"""Handle special species transformations (pH, pe, Eh)."""
|
|
196
|
+
new_species = []
|
|
197
|
+
new_logact = logact.copy() if logact else [0.0] * len(species)
|
|
198
|
+
|
|
199
|
+
for i, sp in enumerate(species):
|
|
200
|
+
if sp == "pH":
|
|
201
|
+
new_logact[i] = -new_logact[i]
|
|
202
|
+
new_species.append("H+")
|
|
203
|
+
elif sp == "pe":
|
|
204
|
+
new_logact[i] = -new_logact[i]
|
|
205
|
+
new_species.append("e-")
|
|
206
|
+
elif sp == "Eh":
|
|
207
|
+
# Convert Eh to pe (simplified - assumes 25°C)
|
|
208
|
+
new_logact[i] = -_convert_eh_to_pe(new_logact[i])
|
|
209
|
+
new_species.append("e-")
|
|
210
|
+
else:
|
|
211
|
+
new_species.append(sp)
|
|
212
|
+
|
|
213
|
+
return new_species, new_logact
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _convert_eh_to_pe(eh_value):
|
|
217
|
+
"""Convert Eh to pe (simplified for 25°C)."""
|
|
218
|
+
# This is a simplified conversion - full implementation would
|
|
219
|
+
# use proper temperature-dependent conversion
|
|
220
|
+
return eh_value / 0.05916 # Approximate conversion at 25°C
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _all_species_in_basis(species, basis_df):
|
|
224
|
+
"""Check if all species are already in the basis definition."""
|
|
225
|
+
if basis_df is None:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
basis_formulas = basis_df.index.tolist()
|
|
229
|
+
basis_indices = basis_df['ispecies'].tolist()
|
|
230
|
+
|
|
231
|
+
for sp in species:
|
|
232
|
+
if isinstance(sp, str):
|
|
233
|
+
if sp not in basis_formulas:
|
|
234
|
+
return False
|
|
235
|
+
elif isinstance(sp, int):
|
|
236
|
+
if sp not in basis_indices:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _get_species_indices(species, state, messages=True):
|
|
243
|
+
"""Get species indices for basis species."""
|
|
244
|
+
ispecies = []
|
|
245
|
+
|
|
246
|
+
for i, sp in enumerate(species):
|
|
247
|
+
if isinstance(sp, int):
|
|
248
|
+
# Already an index
|
|
249
|
+
ispecies.append(sp)
|
|
250
|
+
else:
|
|
251
|
+
# Look up by name/formula
|
|
252
|
+
sp_state = state[i] if state and i < len(state) else None
|
|
253
|
+
try:
|
|
254
|
+
idx = find_species(sp, sp_state, messages=messages)
|
|
255
|
+
ispecies.append(idx)
|
|
256
|
+
except ValueError:
|
|
257
|
+
available = f"({sp_state})" if sp_state else ""
|
|
258
|
+
raise BasisError(f"Species not available: {sp}{available}")
|
|
259
|
+
|
|
260
|
+
return ispecies
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def put_basis(ispecies: List[int], logact: List[float], global_state: bool = True) -> pd.DataFrame:
|
|
264
|
+
"""
|
|
265
|
+
Create and validate a basis species definition.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
ispecies : list of int
|
|
270
|
+
Species indices in thermo().obigt
|
|
271
|
+
logact : list of float
|
|
272
|
+
Log activities for the basis species
|
|
273
|
+
global_state : bool, default True
|
|
274
|
+
If True, store in global thermo().basis (default)
|
|
275
|
+
If False, return without storing globally
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
pd.DataFrame
|
|
280
|
+
Validated basis definition
|
|
281
|
+
|
|
282
|
+
Raises
|
|
283
|
+
------
|
|
284
|
+
BasisError
|
|
285
|
+
If the basis is invalid (non-square or singular matrix)
|
|
286
|
+
"""
|
|
287
|
+
thermo_obj = thermo()
|
|
288
|
+
obigt = thermo_obj.obigt
|
|
289
|
+
|
|
290
|
+
if obigt is None:
|
|
291
|
+
raise RuntimeError("Thermodynamic database not initialized")
|
|
292
|
+
|
|
293
|
+
# Get species information
|
|
294
|
+
states = [obigt.iloc[i-1]['state'] for i in ispecies]
|
|
295
|
+
formulas = [obigt.iloc[i-1]['formula'] for i in ispecies]
|
|
296
|
+
|
|
297
|
+
# Create stoichiometric matrix
|
|
298
|
+
comp_matrix = _make_composition_matrix(ispecies, formulas)
|
|
299
|
+
|
|
300
|
+
# Validate matrix
|
|
301
|
+
n_species, n_elements = comp_matrix.shape
|
|
302
|
+
if n_species > n_elements:
|
|
303
|
+
if 'Z' in comp_matrix.columns:
|
|
304
|
+
raise BasisError("the number of basis species is greater than the number of elements and charge")
|
|
305
|
+
else:
|
|
306
|
+
raise BasisError("the number of basis species is greater than the number of elements")
|
|
307
|
+
elif n_species < n_elements:
|
|
308
|
+
if 'Z' in comp_matrix.columns:
|
|
309
|
+
raise BasisError("the number of basis species is less than the number of elements and charge")
|
|
310
|
+
else:
|
|
311
|
+
raise BasisError("the number of basis species is less than the number of elements")
|
|
312
|
+
|
|
313
|
+
# Check if matrix is invertible
|
|
314
|
+
try:
|
|
315
|
+
np.linalg.inv(comp_matrix.values)
|
|
316
|
+
except np.linalg.LinAlgError:
|
|
317
|
+
raise BasisError("singular stoichiometric matrix")
|
|
318
|
+
|
|
319
|
+
# Create basis DataFrame
|
|
320
|
+
basis_data = comp_matrix.copy()
|
|
321
|
+
basis_data['ispecies'] = ispecies
|
|
322
|
+
basis_data['logact'] = logact
|
|
323
|
+
basis_data['state'] = states
|
|
324
|
+
|
|
325
|
+
# Set row names to formulas, handling electron specially
|
|
326
|
+
rownames = []
|
|
327
|
+
for formula in formulas:
|
|
328
|
+
if formula == "(Z-1)":
|
|
329
|
+
rownames.append("e-")
|
|
330
|
+
else:
|
|
331
|
+
rownames.append(formula)
|
|
332
|
+
|
|
333
|
+
basis_data.index = rownames
|
|
334
|
+
|
|
335
|
+
# Store in thermo system only if using global state
|
|
336
|
+
if global_state:
|
|
337
|
+
thermo_obj.basis = basis_data
|
|
338
|
+
|
|
339
|
+
return basis_data
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _make_composition_matrix(ispecies: List[int], formulas: List[str]) -> pd.DataFrame:
|
|
343
|
+
"""
|
|
344
|
+
Create elemental composition matrix for basis species.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
ispecies : list of int
|
|
349
|
+
Species indices
|
|
350
|
+
formulas : list of str
|
|
351
|
+
Chemical formulas
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
pd.DataFrame
|
|
356
|
+
Composition matrix with elements as columns
|
|
357
|
+
"""
|
|
358
|
+
# Get elemental makeup for each species
|
|
359
|
+
compositions = []
|
|
360
|
+
all_elements = set()
|
|
361
|
+
|
|
362
|
+
for formula in formulas:
|
|
363
|
+
comp = makeup(formula)
|
|
364
|
+
compositions.append(comp)
|
|
365
|
+
all_elements.update(comp.keys())
|
|
366
|
+
|
|
367
|
+
# Create matrix with all elements
|
|
368
|
+
all_elements = sorted(list(all_elements))
|
|
369
|
+
comp_matrix = pd.DataFrame(index=range(len(formulas)), columns=all_elements)
|
|
370
|
+
|
|
371
|
+
for i, comp in enumerate(compositions):
|
|
372
|
+
for element in all_elements:
|
|
373
|
+
comp_matrix.loc[i, element] = comp.get(element, 0)
|
|
374
|
+
|
|
375
|
+
return comp_matrix.astype(float)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def mod_basis(species: Union[str, int, List[Union[str, int]]],
|
|
379
|
+
state: Optional[Union[str, List[str]]] = None,
|
|
380
|
+
logact: Optional[Union[float, List[float]]] = None,
|
|
381
|
+
messages: bool = True) -> pd.DataFrame:
|
|
382
|
+
"""
|
|
383
|
+
Modify states or log activities of existing basis species.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
species : str, int, or list
|
|
388
|
+
Basis species to modify (by formula or index)
|
|
389
|
+
state : str, list of str, or None
|
|
390
|
+
New state(s) or buffer name(s)
|
|
391
|
+
logact : float, list of float, or None
|
|
392
|
+
New log activity values
|
|
393
|
+
messages : bool, default True
|
|
394
|
+
If True, print informational messages
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
pd.DataFrame
|
|
399
|
+
Updated basis definition
|
|
400
|
+
|
|
401
|
+
Raises
|
|
402
|
+
------
|
|
403
|
+
BasisError
|
|
404
|
+
If basis not defined or species not found
|
|
405
|
+
"""
|
|
406
|
+
thermo_obj = thermo()
|
|
407
|
+
|
|
408
|
+
if thermo_obj.basis is None:
|
|
409
|
+
raise BasisError("basis is not defined")
|
|
410
|
+
|
|
411
|
+
# Ensure arguments are lists
|
|
412
|
+
if not isinstance(species, list):
|
|
413
|
+
species = [species]
|
|
414
|
+
if state is not None and not isinstance(state, list):
|
|
415
|
+
state = [state]
|
|
416
|
+
if logact is not None and not isinstance(logact, list):
|
|
417
|
+
logact = [logact]
|
|
418
|
+
|
|
419
|
+
# Process each species
|
|
420
|
+
for i, sp in enumerate(species):
|
|
421
|
+
# Find basis species index
|
|
422
|
+
if isinstance(sp, int):
|
|
423
|
+
# Match by species index
|
|
424
|
+
try:
|
|
425
|
+
basis_idx = thermo_obj.basis[thermo_obj.basis['ispecies'] == sp].index[0]
|
|
426
|
+
except IndexError:
|
|
427
|
+
raise BasisError(f"{sp} is not a species index of one of the basis species")
|
|
428
|
+
else:
|
|
429
|
+
# Match by formula
|
|
430
|
+
try:
|
|
431
|
+
basis_idx = thermo_obj.basis.loc[sp].name
|
|
432
|
+
basis_idx = sp # Use the formula as index
|
|
433
|
+
except KeyError:
|
|
434
|
+
raise BasisError(f"{sp} is not a formula of one of the basis species")
|
|
435
|
+
|
|
436
|
+
# Modify state
|
|
437
|
+
if state is not None and i < len(state):
|
|
438
|
+
new_state = state[i]
|
|
439
|
+
|
|
440
|
+
# Check if it's a buffer name
|
|
441
|
+
if _is_buffer_name(new_state):
|
|
442
|
+
_validate_buffer_compatibility(new_state, basis_idx, messages=messages)
|
|
443
|
+
thermo_obj.basis.loc[basis_idx, 'logact'] = new_state
|
|
444
|
+
else:
|
|
445
|
+
# Find species in new state
|
|
446
|
+
current_species = thermo_obj.basis.loc[basis_idx, 'ispecies']
|
|
447
|
+
species_name = thermo_obj.obigt.iloc[current_species-1]['name']
|
|
448
|
+
species_formula = thermo_obj.obigt.iloc[current_species-1]['formula']
|
|
449
|
+
|
|
450
|
+
# Try to find by name first, then by formula
|
|
451
|
+
try:
|
|
452
|
+
new_ispecies = find_species(species_name, new_state, messages=messages)
|
|
453
|
+
except ValueError:
|
|
454
|
+
try:
|
|
455
|
+
new_ispecies = find_species(species_formula, new_state, messages=messages)
|
|
456
|
+
except ValueError:
|
|
457
|
+
name_text = species_name if species_name == species_formula else f"{species_name} or {species_formula}"
|
|
458
|
+
raise BasisError(f"state or buffer '{new_state}' not found for {name_text}")
|
|
459
|
+
|
|
460
|
+
# Update basis
|
|
461
|
+
thermo_obj.basis.loc[basis_idx, 'ispecies'] = new_ispecies
|
|
462
|
+
thermo_obj.basis.loc[basis_idx, 'state'] = new_state
|
|
463
|
+
|
|
464
|
+
# Modify log activity
|
|
465
|
+
if logact is not None and i < len(logact):
|
|
466
|
+
thermo_obj.basis.loc[basis_idx, 'logact'] = logact[i]
|
|
467
|
+
|
|
468
|
+
return thermo_obj.basis
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _is_buffer_name(name: str) -> bool:
|
|
472
|
+
"""Check if name corresponds to a buffer system."""
|
|
473
|
+
thermo_obj = thermo()
|
|
474
|
+
if thermo_obj.buffer is None:
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
return name in thermo_obj.buffer['name'].values if 'name' in thermo_obj.buffer.columns else False
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _validate_buffer_compatibility(buffer_name: str, basis_idx: str, messages: bool = True) -> None:
|
|
481
|
+
"""Validate that buffer species are compatible with current basis."""
|
|
482
|
+
thermo_obj = thermo()
|
|
483
|
+
|
|
484
|
+
# Get buffer species
|
|
485
|
+
buffer_data = thermo_obj.buffer[thermo_obj.buffer['name'] == buffer_name]
|
|
486
|
+
|
|
487
|
+
for _, buffer_row in buffer_data.iterrows():
|
|
488
|
+
species_name = buffer_row.get('species', '')
|
|
489
|
+
species_state = buffer_row.get('state', '')
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
ispecies = find_species(species_name, species_state, messages=messages)
|
|
493
|
+
species_makeup = makeup(thermo_obj.obigt.iloc[ispecies-1]['formula'])
|
|
494
|
+
|
|
495
|
+
# Check if all elements are in basis
|
|
496
|
+
basis_elements = set(thermo_obj.basis.columns) - {'ispecies', 'logact', 'state'}
|
|
497
|
+
species_elements = set(species_makeup.keys())
|
|
498
|
+
|
|
499
|
+
missing_elements = species_elements - basis_elements
|
|
500
|
+
if missing_elements:
|
|
501
|
+
raise BasisError(f"the elements '{', '.join(missing_elements)}' of species "
|
|
502
|
+
f"'{species_name}' in buffer '{buffer_name}' are not in the basis")
|
|
503
|
+
except ValueError:
|
|
504
|
+
pass # Skip if species not found
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _update_species_for_added_basis(old_basis: pd.DataFrame, new_basis: pd.DataFrame) -> None:
|
|
508
|
+
"""Update species list when basis species are added."""
|
|
509
|
+
thermo_obj = thermo()
|
|
510
|
+
|
|
511
|
+
if thermo_obj.species is None:
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
n_old = len(old_basis)
|
|
515
|
+
n_new = len(new_basis)
|
|
516
|
+
n_species = len(thermo_obj.species)
|
|
517
|
+
|
|
518
|
+
# Create new stoichiometric matrix with zeros for added basis species
|
|
519
|
+
old_stoich = thermo_obj.species.iloc[:, :n_old].values
|
|
520
|
+
new_cols = np.zeros((n_species, n_new - n_old))
|
|
521
|
+
new_stoich = np.hstack([old_stoich, new_cols])
|
|
522
|
+
|
|
523
|
+
# Create new species DataFrame
|
|
524
|
+
stoich_df = pd.DataFrame(new_stoich, columns=new_basis.index)
|
|
525
|
+
other_cols = thermo_obj.species.iloc[:, n_old:]
|
|
526
|
+
new_species = pd.concat([stoich_df, other_cols], axis=1)
|
|
527
|
+
|
|
528
|
+
thermo_obj.species = new_species
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def preset_basis(key: Optional[str] = None, messages: bool = True, global_state: bool = True) -> Union[List[str], pd.DataFrame]:
|
|
532
|
+
"""
|
|
533
|
+
Load a preset basis definition by keyword.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
key : str or None
|
|
538
|
+
Preset keyword. If None, returns available keywords.
|
|
539
|
+
messages : bool, default True
|
|
540
|
+
If True, print informational messages
|
|
541
|
+
global_state : bool, default True
|
|
542
|
+
If True, store in global thermo().basis (default)
|
|
543
|
+
If False, return without storing globally
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
list of str or pd.DataFrame
|
|
548
|
+
Available keywords or basis definition
|
|
549
|
+
|
|
550
|
+
Examples
|
|
551
|
+
--------
|
|
552
|
+
>>> # List available presets
|
|
553
|
+
>>> preset_basis()
|
|
554
|
+
|
|
555
|
+
>>> # Load CHNOS basis
|
|
556
|
+
>>> preset_basis("CHNOS")
|
|
557
|
+
"""
|
|
558
|
+
keywords = _get_preset_basis_keywords()
|
|
559
|
+
|
|
560
|
+
if key is None:
|
|
561
|
+
return keywords
|
|
562
|
+
|
|
563
|
+
if key not in keywords:
|
|
564
|
+
raise ValueError(f"{key} is not a keyword for preset basis species")
|
|
565
|
+
|
|
566
|
+
# Clear existing basis only if using global state
|
|
567
|
+
if global_state:
|
|
568
|
+
basis(delete=True)
|
|
569
|
+
|
|
570
|
+
# Define preset species
|
|
571
|
+
species_map = {
|
|
572
|
+
"CHNOS": ["CO2", "H2O", "NH3", "H2S", "oxygen"],
|
|
573
|
+
"CHNOS+": ["CO2", "H2O", "NH3", "H2S", "oxygen", "H+"],
|
|
574
|
+
"CHNOSe": ["CO2", "H2O", "NH3", "H2S", "e-", "H+"],
|
|
575
|
+
"CHNOPS+": ["CO2", "H2O", "NH3", "H3PO4", "H2S", "oxygen", "H+"],
|
|
576
|
+
"CHNOPSe": ["CO2", "H2O", "NH3", "H3PO4", "H2S", "e-", "H+"],
|
|
577
|
+
"MgCHNOPS+": ["Mg+2", "CO2", "H2O", "NH3", "H3PO4", "H2S", "oxygen", "H+"],
|
|
578
|
+
"MgCHNOPSe": ["Mg+2", "CO2", "H2O", "NH3", "H3PO4", "H2S", "e-", "H+"],
|
|
579
|
+
"FeCHNOS": ["Fe2O3", "CO2", "H2O", "NH3", "H2S", "oxygen"],
|
|
580
|
+
"FeCHNOS+": ["Fe2O3", "CO2", "H2O", "NH3", "H2S", "oxygen", "H+"],
|
|
581
|
+
"QEC4": ["glutamine", "glutamic acid", "cysteine", "H2O", "oxygen"],
|
|
582
|
+
"QEC": ["glutamine", "glutamic acid", "cysteine", "H2O", "oxygen"],
|
|
583
|
+
"QEC+": ["glutamine", "glutamic acid", "cysteine", "H2O", "oxygen", "H+"],
|
|
584
|
+
"QCa": ["glutamine", "cysteine", "acetic acid", "H2O", "oxygen"],
|
|
585
|
+
"QCa+": ["glutamine", "cysteine", "acetic acid", "H2O", "oxygen", "H+"]
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
species = species_map[key]
|
|
589
|
+
logact = _preset_logact(species)
|
|
590
|
+
|
|
591
|
+
# Special case for QEC4
|
|
592
|
+
if key == "QEC4":
|
|
593
|
+
logact[:3] = [-4.0] * 3
|
|
594
|
+
|
|
595
|
+
return basis(species, logact=logact, messages=messages, global_state=global_state)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _get_preset_basis_keywords() -> List[str]:
|
|
599
|
+
"""Get list of available preset basis keywords."""
|
|
600
|
+
return [
|
|
601
|
+
"CHNOS", "CHNOS+", "CHNOSe", "CHNOPS+", "CHNOPSe",
|
|
602
|
+
"MgCHNOPS+", "MgCHNOPSe", "FeCHNOS", "FeCHNOS+",
|
|
603
|
+
"QEC4", "QEC", "QEC+", "QCa", "QCa+"
|
|
604
|
+
]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _preset_logact(species: List[str]) -> List[float]:
|
|
608
|
+
"""Get preset log activities for basis species."""
|
|
609
|
+
# Standard log activities for common species
|
|
610
|
+
standard_logact = {
|
|
611
|
+
"H2O": 0.0,
|
|
612
|
+
"CO2": -3.0,
|
|
613
|
+
"NH3": -4.0,
|
|
614
|
+
"H2S": -7.0,
|
|
615
|
+
"oxygen": -80.0,
|
|
616
|
+
"H+": -7.0,
|
|
617
|
+
"e-": -7.0,
|
|
618
|
+
"Fe2O3": 0.0,
|
|
619
|
+
# QEC amino acids (from Dick, 2017)
|
|
620
|
+
"glutamine": -3.2,
|
|
621
|
+
"glutamic acid": -4.5,
|
|
622
|
+
"cysteine": -3.6
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
logact = []
|
|
626
|
+
for sp in species:
|
|
627
|
+
if sp in standard_logact:
|
|
628
|
+
logact.append(standard_logact[sp])
|
|
629
|
+
else:
|
|
630
|
+
logact.append(-3.0) # Default for unmatched species
|
|
631
|
+
|
|
632
|
+
return logact
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# Convenience functions
|
|
636
|
+
def get_basis() -> Optional[pd.DataFrame]:
|
|
637
|
+
"""
|
|
638
|
+
Get current basis definition.
|
|
639
|
+
|
|
640
|
+
Returns
|
|
641
|
+
-------
|
|
642
|
+
pd.DataFrame or None
|
|
643
|
+
Current basis definition
|
|
644
|
+
"""
|
|
645
|
+
return thermo().basis
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def is_basis_defined() -> bool:
|
|
649
|
+
"""
|
|
650
|
+
Check if basis is currently defined.
|
|
651
|
+
|
|
652
|
+
Returns
|
|
653
|
+
-------
|
|
654
|
+
bool
|
|
655
|
+
True if basis is defined
|
|
656
|
+
"""
|
|
657
|
+
return thermo().basis is not None
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def basis_elements() -> Optional[np.ndarray]:
|
|
661
|
+
"""
|
|
662
|
+
Get basis elements matrix.
|
|
663
|
+
|
|
664
|
+
Returns
|
|
665
|
+
-------
|
|
666
|
+
np.ndarray or None
|
|
667
|
+
Transposed basis composition matrix
|
|
668
|
+
"""
|
|
669
|
+
basis_df = get_basis()
|
|
670
|
+
if basis_df is None:
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
# Get elemental composition columns
|
|
674
|
+
element_cols = [col for col in basis_df.columns
|
|
675
|
+
if col not in ['ispecies', 'logact', 'state']]
|
|
676
|
+
|
|
677
|
+
return basis_df[element_cols].values.T
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def swap_basis(species1: Union[str, int], species2: Union[str, int]) -> pd.DataFrame:
|
|
681
|
+
"""
|
|
682
|
+
Swap one basis species for another.
|
|
683
|
+
|
|
684
|
+
Parameters
|
|
685
|
+
----------
|
|
686
|
+
species1 : str or int
|
|
687
|
+
Current basis species to replace
|
|
688
|
+
species2 : str or int
|
|
689
|
+
New species to add to basis
|
|
690
|
+
|
|
691
|
+
Returns
|
|
692
|
+
-------
|
|
693
|
+
pd.DataFrame
|
|
694
|
+
Updated basis definition
|
|
695
|
+
|
|
696
|
+
Raises
|
|
697
|
+
------
|
|
698
|
+
BasisError
|
|
699
|
+
If operation is not possible
|
|
700
|
+
"""
|
|
701
|
+
thermo_obj = thermo()
|
|
702
|
+
|
|
703
|
+
if thermo_obj.basis is None:
|
|
704
|
+
raise BasisError("basis is not defined")
|
|
705
|
+
|
|
706
|
+
# This would require solving for the new basis coefficients
|
|
707
|
+
# Full implementation would be more complex
|
|
708
|
+
raise NotImplementedError("swap_basis not yet implemented")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# Export main functions
|
|
712
|
+
__all__ = [
|
|
713
|
+
'basis', 'mod_basis', 'put_basis', 'preset_basis',
|
|
714
|
+
'get_basis', 'is_basis_defined', 'basis_elements',
|
|
715
|
+
'BasisError'
|
|
716
|
+
]
|