pychnosz 1.1.1__cp310-cp310-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 +1700 -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 +222 -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.1.dist-info/METADATA +197 -0
  125. pychnosz-1.1.1.dist-info/RECORD +128 -0
  126. pychnosz-1.1.1.dist-info/WHEEL +5 -0
  127. pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
  128. pychnosz-1.1.1.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
+ ]