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.
- 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/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.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 +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.1.dist-info/METADATA +197 -0
- pychnosz-1.1.1.dist-info/RECORD +131 -0
- pychnosz-1.1.1.dist-info/WHEEL +5 -0
- pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.1.dist-info/top_level.txt +1 -0
pychnosz/core/thermo.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core ThermoSystem class for managing global thermodynamic state.
|
|
3
|
+
|
|
4
|
+
This class manages the global thermodynamic system state, similar to the
|
|
5
|
+
'thermo' object in the R version of CHNOSZ.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import numpy as np
|
|
11
|
+
from typing import Optional, Dict, Any, Union
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ..data.loader import DataLoader
|
|
15
|
+
from ..data.obigt import OBIGTDatabase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ThermoSystem:
|
|
19
|
+
"""
|
|
20
|
+
Global thermodynamic system manager for CHNOSZ.
|
|
21
|
+
|
|
22
|
+
This class manages the thermodynamic database, basis species,
|
|
23
|
+
formed species, and calculation options - essentially serving
|
|
24
|
+
as the global state container for all CHNOSZ calculations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the thermodynamic system."""
|
|
29
|
+
self._data_loader = DataLoader()
|
|
30
|
+
self._obigt_db = None
|
|
31
|
+
self._initialized = False
|
|
32
|
+
|
|
33
|
+
# Core data containers (similar to R thermo object)
|
|
34
|
+
self.opt: Dict[str, Any] = {}
|
|
35
|
+
self.element: Optional[pd.DataFrame] = None
|
|
36
|
+
self.obigt: Optional[pd.DataFrame] = None
|
|
37
|
+
self.refs: Optional[pd.DataFrame] = None
|
|
38
|
+
self.Berman: Optional[pd.DataFrame] = None
|
|
39
|
+
self.buffer: Optional[pd.DataFrame] = None
|
|
40
|
+
self.protein: Optional[pd.DataFrame] = None
|
|
41
|
+
self.groups: Optional[pd.DataFrame] = None
|
|
42
|
+
self.stoich: Optional[np.ndarray] = None
|
|
43
|
+
self.stoich_formulas: Optional[np.ndarray] = None
|
|
44
|
+
self.bdot_acirc: Optional[Dict[str, float]] = None
|
|
45
|
+
self.formula_ox: Optional[pd.DataFrame] = None
|
|
46
|
+
|
|
47
|
+
# System state
|
|
48
|
+
self.basis: Optional[pd.DataFrame] = None
|
|
49
|
+
self.species: Optional[pd.DataFrame] = None
|
|
50
|
+
|
|
51
|
+
# Options and parameters
|
|
52
|
+
self.opar: Dict[str, Any] = {}
|
|
53
|
+
|
|
54
|
+
def reset(self, messages: bool = True) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize/reset the thermodynamic system.
|
|
57
|
+
|
|
58
|
+
This is equivalent to reset() in the R version, loading all
|
|
59
|
+
the thermodynamic data and initializing the system.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
messages : bool, default True
|
|
64
|
+
Whether to print informational messages
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
# Load core data files
|
|
68
|
+
self._load_options(messages)
|
|
69
|
+
self._load_element_data(messages)
|
|
70
|
+
self._load_berman_data(messages)
|
|
71
|
+
self._load_buffer_data(messages)
|
|
72
|
+
self._load_protein_data(messages)
|
|
73
|
+
self._load_stoich_data(messages)
|
|
74
|
+
self._load_bdot_data(messages)
|
|
75
|
+
self._load_refs_data(messages)
|
|
76
|
+
|
|
77
|
+
# Initialize OBIGT database
|
|
78
|
+
self._obigt_db = OBIGTDatabase()
|
|
79
|
+
self.obigt = self._obigt_db.get_combined_data()
|
|
80
|
+
|
|
81
|
+
# Reset system state
|
|
82
|
+
self.basis = None
|
|
83
|
+
self.species = None
|
|
84
|
+
self.opar = {}
|
|
85
|
+
|
|
86
|
+
self._initialized = True
|
|
87
|
+
if messages:
|
|
88
|
+
print('reset: thermodynamic system initialized')
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise RuntimeError(f"Failed to initialize thermodynamic system: {e}")
|
|
92
|
+
|
|
93
|
+
def _load_options(self, messages: bool = True) -> None:
|
|
94
|
+
"""Load default thermodynamic options."""
|
|
95
|
+
try:
|
|
96
|
+
opt_file = self._data_loader.get_data_path() / "thermo" / "opt.csv"
|
|
97
|
+
if opt_file.exists():
|
|
98
|
+
df = pd.read_csv(opt_file)
|
|
99
|
+
# Convert to dictionary format (first row contains values)
|
|
100
|
+
self.opt = dict(zip(df.columns, df.iloc[0]))
|
|
101
|
+
else:
|
|
102
|
+
# Default options if file not found
|
|
103
|
+
self.opt = {
|
|
104
|
+
'E.units': 'J',
|
|
105
|
+
'T.units': 'C',
|
|
106
|
+
'P.units': 'bar',
|
|
107
|
+
'state': 'aq',
|
|
108
|
+
'water': 'SUPCRT92',
|
|
109
|
+
'G.tol': 100,
|
|
110
|
+
'Cp.tol': 1,
|
|
111
|
+
'V.tol': 1,
|
|
112
|
+
'varP': False,
|
|
113
|
+
'IAPWS.sat': 'liquid',
|
|
114
|
+
'paramin': 1000,
|
|
115
|
+
'ideal.H': True,
|
|
116
|
+
'ideal.e': True,
|
|
117
|
+
'nonideal': 'Bdot',
|
|
118
|
+
'Setchenow': 'bgamma0',
|
|
119
|
+
'Berman': np.nan,
|
|
120
|
+
'maxcores': 2,
|
|
121
|
+
'ionize.aa': True
|
|
122
|
+
}
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if messages:
|
|
125
|
+
print(f"Warning: Could not load options: {e}")
|
|
126
|
+
# Fallback to hardcoded defaults with critical unit options
|
|
127
|
+
self.opt = {
|
|
128
|
+
'E.units': 'J',
|
|
129
|
+
'T.units': 'C',
|
|
130
|
+
'P.units': 'bar',
|
|
131
|
+
'state': 'aq',
|
|
132
|
+
'water': 'SUPCRT92',
|
|
133
|
+
'G.tol': 100,
|
|
134
|
+
'Cp.tol': 1,
|
|
135
|
+
'V.tol': 1,
|
|
136
|
+
'varP': False,
|
|
137
|
+
'IAPWS.sat': 'liquid',
|
|
138
|
+
'paramin': 1000,
|
|
139
|
+
'ideal.H': True,
|
|
140
|
+
'ideal.e': True,
|
|
141
|
+
'nonideal': 'Bdot',
|
|
142
|
+
'Setchenow': 'bgamma0',
|
|
143
|
+
'Berman': np.nan,
|
|
144
|
+
'maxcores': 2,
|
|
145
|
+
'ionize.aa': True
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def _load_element_data(self, messages: bool = True) -> None:
|
|
149
|
+
"""Load element properties data."""
|
|
150
|
+
try:
|
|
151
|
+
self.element = self._data_loader.load_elements()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
if messages:
|
|
154
|
+
print(f"Warning: Could not load element data: {e}")
|
|
155
|
+
self.element = None
|
|
156
|
+
|
|
157
|
+
def _load_berman_data(self, messages: bool = True) -> None:
|
|
158
|
+
"""Load Berman mineral parameters from CSV files."""
|
|
159
|
+
try:
|
|
160
|
+
# Get path to Berman directory
|
|
161
|
+
berman_path = self._data_loader.data_path / "Berman"
|
|
162
|
+
|
|
163
|
+
if not berman_path.exists():
|
|
164
|
+
if messages:
|
|
165
|
+
print(f"Warning: Berman directory not found: {berman_path}")
|
|
166
|
+
self.Berman = None
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Find all CSV files in the directory
|
|
170
|
+
csv_files = list(berman_path.glob("*.csv"))
|
|
171
|
+
|
|
172
|
+
if not csv_files:
|
|
173
|
+
if messages:
|
|
174
|
+
print(f"Warning: No CSV files found in {berman_path}")
|
|
175
|
+
self.Berman = None
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Extract year from filename and sort in reverse chronological order (youngest first)
|
|
179
|
+
# Following R logic: files <- rev(files[order(sapply(strsplit(files, "_"), "[", 2))])
|
|
180
|
+
def extract_year(filepath):
|
|
181
|
+
filename = filepath.name
|
|
182
|
+
parts = filename.split('_')
|
|
183
|
+
if len(parts) >= 2:
|
|
184
|
+
year_part = parts[1].replace('.csv', '')
|
|
185
|
+
try:
|
|
186
|
+
return int(year_part)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return 0
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
# Sort files by year (youngest first)
|
|
192
|
+
sorted_files = sorted(csv_files, key=extract_year, reverse=True)
|
|
193
|
+
|
|
194
|
+
# Read parameters from each file
|
|
195
|
+
berman_dfs = []
|
|
196
|
+
for file_path in sorted_files:
|
|
197
|
+
try:
|
|
198
|
+
df = pd.read_csv(file_path)
|
|
199
|
+
berman_dfs.append(df)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"Warning: Could not read Berman file {file_path}: {e}")
|
|
202
|
+
|
|
203
|
+
# Combine all data frames (equivalent to do.call(rbind, Berman))
|
|
204
|
+
if berman_dfs:
|
|
205
|
+
self.Berman = pd.concat(berman_dfs, ignore_index=True)
|
|
206
|
+
# Ensure all numeric columns are properly typed
|
|
207
|
+
numeric_cols = ['GfPrTr', 'HfPrTr', 'SPrTr', 'VPrTr', 'k0', 'k1', 'k2', 'k3', 'k4', 'k5', 'k6',
|
|
208
|
+
'v1', 'v2', 'v3', 'v4', 'Tlambda', 'Tref', 'dTdP', 'l1', 'l2', 'DtH', 'Tmax', 'Tmin',
|
|
209
|
+
'd0', 'd1', 'd2', 'd3', 'd4', 'Vad']
|
|
210
|
+
for col in numeric_cols:
|
|
211
|
+
if col in self.Berman.columns:
|
|
212
|
+
self.Berman[col] = pd.to_numeric(self.Berman[col], errors='coerce')
|
|
213
|
+
else:
|
|
214
|
+
self.Berman = None
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
if messages:
|
|
218
|
+
print(f"Warning: Could not load Berman data: {e}")
|
|
219
|
+
self.Berman = None
|
|
220
|
+
|
|
221
|
+
def _load_buffer_data(self, messages: bool = True) -> None:
|
|
222
|
+
"""Load buffer definitions."""
|
|
223
|
+
try:
|
|
224
|
+
self.buffer = self._data_loader.load_buffers()
|
|
225
|
+
except Exception as e:
|
|
226
|
+
if messages:
|
|
227
|
+
print(f"Warning: Could not load buffer data: {e}")
|
|
228
|
+
self.buffer = None
|
|
229
|
+
|
|
230
|
+
def _load_protein_data(self, messages: bool = True) -> None:
|
|
231
|
+
"""Load protein composition data."""
|
|
232
|
+
try:
|
|
233
|
+
self.protein = self._data_loader.load_proteins()
|
|
234
|
+
except Exception as e:
|
|
235
|
+
if messages:
|
|
236
|
+
print(f"Warning: Could not load protein data: {e}")
|
|
237
|
+
self.protein = None
|
|
238
|
+
|
|
239
|
+
def _load_stoich_data(self, messages: bool = True) -> None:
|
|
240
|
+
"""Load stoichiometric matrix data."""
|
|
241
|
+
try:
|
|
242
|
+
stoich_df = self._data_loader.load_stoich()
|
|
243
|
+
if stoich_df is not None:
|
|
244
|
+
# Extract formulas and convert to matrix
|
|
245
|
+
self.stoich_formulas = stoich_df.iloc[:, 0].values
|
|
246
|
+
self.stoich = stoich_df.iloc[:, 1:].values
|
|
247
|
+
else:
|
|
248
|
+
self.stoich_formulas = None
|
|
249
|
+
self.stoich = None
|
|
250
|
+
except Exception as e:
|
|
251
|
+
if messages:
|
|
252
|
+
print(f"Warning: Could not load stoichiometric data: {e}")
|
|
253
|
+
self.stoich_formulas = None
|
|
254
|
+
self.stoich = None
|
|
255
|
+
|
|
256
|
+
def _load_bdot_data(self, messages: bool = True) -> None:
|
|
257
|
+
"""Load B-dot activity coefficient parameters."""
|
|
258
|
+
try:
|
|
259
|
+
bdot_file = self._data_loader.get_data_path() / "thermo" / "Bdot_acirc.csv"
|
|
260
|
+
if bdot_file.exists():
|
|
261
|
+
df = pd.read_csv(bdot_file)
|
|
262
|
+
if len(df.columns) >= 2:
|
|
263
|
+
self.bdot_acirc = dict(zip(df.iloc[:, 0], df.iloc[:, 1]))
|
|
264
|
+
else:
|
|
265
|
+
self.bdot_acirc = {}
|
|
266
|
+
else:
|
|
267
|
+
self.bdot_acirc = {}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
if messages:
|
|
270
|
+
print(f"Warning: Could not load B-dot data: {e}")
|
|
271
|
+
self.bdot_acirc = {}
|
|
272
|
+
|
|
273
|
+
def _load_refs_data(self, messages: bool = True) -> None:
|
|
274
|
+
"""Load references data."""
|
|
275
|
+
try:
|
|
276
|
+
self.refs = self._data_loader.load_refs()
|
|
277
|
+
except Exception as e:
|
|
278
|
+
if messages:
|
|
279
|
+
print(f"Warning: Could not load refs data: {e}")
|
|
280
|
+
self.refs = None
|
|
281
|
+
|
|
282
|
+
def is_initialized(self) -> bool:
|
|
283
|
+
"""Check if the thermodynamic system is initialized."""
|
|
284
|
+
return self._initialized
|
|
285
|
+
|
|
286
|
+
def get_obigt_db(self) -> OBIGTDatabase:
|
|
287
|
+
"""Get the OBIGT database instance."""
|
|
288
|
+
if not self._initialized:
|
|
289
|
+
self.reset()
|
|
290
|
+
return self._obigt_db
|
|
291
|
+
|
|
292
|
+
def get_option(self, key: str, default: Any = None) -> Any:
|
|
293
|
+
"""Get a thermodynamic option value."""
|
|
294
|
+
return self.opt.get(key, default)
|
|
295
|
+
|
|
296
|
+
def set_option(self, key: str, value: Any) -> None:
|
|
297
|
+
"""Set a thermodynamic option value."""
|
|
298
|
+
self.opt[key] = value
|
|
299
|
+
|
|
300
|
+
def info(self) -> Dict[str, Any]:
|
|
301
|
+
"""Get information about the current thermodynamic system."""
|
|
302
|
+
if not self._initialized:
|
|
303
|
+
return {"status": "Not initialized"}
|
|
304
|
+
|
|
305
|
+
info = {
|
|
306
|
+
"status": "Initialized",
|
|
307
|
+
"obigt_species": len(self.obigt) if self.obigt is not None else 0,
|
|
308
|
+
"elements": len(self.element) if self.element is not None else 0,
|
|
309
|
+
"berman_minerals": len(self.Berman) if self.Berman is not None else 0,
|
|
310
|
+
"buffers": len(self.buffer) if self.buffer is not None else 0,
|
|
311
|
+
"proteins": len(self.protein) if self.protein is not None else 0,
|
|
312
|
+
"stoich_species": len(self.stoich_formulas) if self.stoich_formulas is not None else 0,
|
|
313
|
+
"basis_species": len(self.basis) if self.basis is not None else 0,
|
|
314
|
+
"formed_species": len(self.species) if self.species is not None else 0,
|
|
315
|
+
"current_options": dict(self.opt)
|
|
316
|
+
}
|
|
317
|
+
return info
|
|
318
|
+
|
|
319
|
+
def __repr__(self) -> str:
|
|
320
|
+
"""String representation of the thermodynamic system."""
|
|
321
|
+
if not self._initialized:
|
|
322
|
+
return "ThermoSystem(uninitialized)"
|
|
323
|
+
|
|
324
|
+
info = self.info()
|
|
325
|
+
return (f"ThermoSystem("
|
|
326
|
+
f"obigt={info['obigt_species']} species, "
|
|
327
|
+
f"basis={info['basis_species']}, "
|
|
328
|
+
f"formed={info['formed_species']})")
|
|
329
|
+
|
|
330
|
+
# R-style uppercase property aliases for compatibility
|
|
331
|
+
@property
|
|
332
|
+
def OBIGT(self):
|
|
333
|
+
"""Alias for obigt (R compatibility)."""
|
|
334
|
+
# Auto-initialize if needed AND obigt is None (matches R behavior)
|
|
335
|
+
if self.obigt is None and not self._initialized:
|
|
336
|
+
self.reset(messages=True)
|
|
337
|
+
return self.obigt
|
|
338
|
+
|
|
339
|
+
@OBIGT.setter
|
|
340
|
+
def OBIGT(self, value):
|
|
341
|
+
"""Setter for OBIGT (R compatibility)."""
|
|
342
|
+
_set_obigt_data(self, value)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# Global instance (singleton pattern)
|
|
346
|
+
_thermo_system = None
|
|
347
|
+
|
|
348
|
+
def get_thermo_system() -> ThermoSystem:
|
|
349
|
+
"""Get the global thermodynamic system instance."""
|
|
350
|
+
global _thermo_system
|
|
351
|
+
if _thermo_system is None:
|
|
352
|
+
_thermo_system = ThermoSystem()
|
|
353
|
+
return _thermo_system
|
|
354
|
+
|
|
355
|
+
def _set_obigt_data(thermo_sys: ThermoSystem, obigt_df: pd.DataFrame) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Set OBIGT data with proper index normalization.
|
|
358
|
+
|
|
359
|
+
This helper function ensures that when OBIGT is replaced, the DataFrame
|
|
360
|
+
index is properly set to use 1-based indexing (matching R conventions).
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
thermo_sys : ThermoSystem
|
|
365
|
+
The thermodynamic system object
|
|
366
|
+
obigt_df : pd.DataFrame
|
|
367
|
+
The new OBIGT DataFrame to set
|
|
368
|
+
"""
|
|
369
|
+
# Make a copy to avoid modifying the original
|
|
370
|
+
new_obigt = obigt_df.copy()
|
|
371
|
+
|
|
372
|
+
# Ensure the index starts at 1 (R convention)
|
|
373
|
+
# If the DataFrame has a default 0-based index, shift it to 1-based
|
|
374
|
+
if new_obigt.index[0] == 0:
|
|
375
|
+
new_obigt.index = new_obigt.index + 1
|
|
376
|
+
|
|
377
|
+
# Set the OBIGT data
|
|
378
|
+
thermo_sys.obigt = new_obigt
|
|
379
|
+
|
|
380
|
+
# Try to load refs data if available
|
|
381
|
+
# This matches R behavior where OBIGT() loads both OBIGT and refs
|
|
382
|
+
try:
|
|
383
|
+
refs_df = thermo_sys._data_loader.load_refs()
|
|
384
|
+
thermo_sys.refs = refs_df
|
|
385
|
+
except Exception:
|
|
386
|
+
# If refs can't be loaded, just leave it as is
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def thermo(*args, messages=True, **kwargs):
|
|
391
|
+
"""
|
|
392
|
+
Access or modify the thermodynamic system data object.
|
|
393
|
+
|
|
394
|
+
This function provides a convenient interface to get or set parts of the
|
|
395
|
+
thermodynamic system, similar to R's par() function for graphics parameters.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
*args : str or list of str
|
|
400
|
+
Names of attributes to retrieve (e.g., "element", "opt$ideal.H")
|
|
401
|
+
For nested access, use "$" notation (e.g., "opt$E.units")
|
|
402
|
+
Special values:
|
|
403
|
+
- "WORM": Load the WORM thermodynamic database (Python-exclusive feature)
|
|
404
|
+
messages : bool, default True
|
|
405
|
+
Whether to print informational messages during operations
|
|
406
|
+
**kwargs : any
|
|
407
|
+
Named arguments to set attributes (e.g., element=new_df, opt={'E.units': 'cal'})
|
|
408
|
+
For nested attributes, use "$" in the name (e.g., **{"opt$ideal.H": False})
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
various
|
|
413
|
+
- If no arguments: returns the ThermoSystem object
|
|
414
|
+
- If single unnamed argument: returns the requested value
|
|
415
|
+
- If multiple unnamed arguments: returns list of requested values
|
|
416
|
+
- If named arguments: returns original values before modification
|
|
417
|
+
|
|
418
|
+
Examples
|
|
419
|
+
--------
|
|
420
|
+
>>> import pychnosz
|
|
421
|
+
>>> # Get the entire thermo object
|
|
422
|
+
>>> ts = pychnosz.thermo()
|
|
423
|
+
|
|
424
|
+
>>> # Get a specific attribute
|
|
425
|
+
>>> elem = pychnosz.thermo("element")
|
|
426
|
+
|
|
427
|
+
>>> # Get nested attribute
|
|
428
|
+
>>> e_units = pychnosz.thermo("opt$E.units")
|
|
429
|
+
|
|
430
|
+
>>> # Get multiple attributes
|
|
431
|
+
>>> elem, buf = pychnosz.thermo("element", "buffer")
|
|
432
|
+
|
|
433
|
+
>>> # Set an attribute
|
|
434
|
+
>>> old_elem = pychnosz.thermo(element=new_element_df)
|
|
435
|
+
|
|
436
|
+
>>> # Set nested attribute
|
|
437
|
+
>>> old_units = pychnosz.thermo(**{"opt$ideal.H": False})
|
|
438
|
+
|
|
439
|
+
>>> # Load WORM database (Python-exclusive feature)
|
|
440
|
+
>>> pychnosz.thermo("WORM")
|
|
441
|
+
|
|
442
|
+
>>> # Suppress messages
|
|
443
|
+
>>> pychnosz.thermo("WORM", messages=False)
|
|
444
|
+
|
|
445
|
+
Notes
|
|
446
|
+
-----
|
|
447
|
+
This function mimics the behavior of R CHNOSZ thermo() function,
|
|
448
|
+
providing flexible access to the thermodynamic data object.
|
|
449
|
+
|
|
450
|
+
The "WORM" special argument is a Python-exclusive feature that loads
|
|
451
|
+
the Water-Organic-Rock-Microbe thermodynamic database from the
|
|
452
|
+
WORM-db GitHub repository.
|
|
453
|
+
"""
|
|
454
|
+
# Get the global thermo system
|
|
455
|
+
thermo_sys = get_thermo_system()
|
|
456
|
+
|
|
457
|
+
# If no arguments, return the entire object
|
|
458
|
+
if len(args) == 0 and len(kwargs) == 0:
|
|
459
|
+
return thermo_sys
|
|
460
|
+
|
|
461
|
+
# Handle character vectors passed as args (like R's c("basis", "species"))
|
|
462
|
+
# If all args are strings or lists of strings, flatten them
|
|
463
|
+
flat_args = []
|
|
464
|
+
for arg in args:
|
|
465
|
+
if isinstance(arg, (list, tuple)) and all(isinstance(x, str) for x in arg):
|
|
466
|
+
flat_args.extend(arg)
|
|
467
|
+
else:
|
|
468
|
+
flat_args.append(arg)
|
|
469
|
+
args = flat_args
|
|
470
|
+
|
|
471
|
+
# Prepare return values list
|
|
472
|
+
return_values = []
|
|
473
|
+
|
|
474
|
+
# Ensure system is initialized if needed (before accessing any properties)
|
|
475
|
+
# This prevents auto-initialization from using hardcoded messages=True
|
|
476
|
+
if not thermo_sys.is_initialized() and len(args) > 0:
|
|
477
|
+
thermo_sys.reset(messages=messages)
|
|
478
|
+
|
|
479
|
+
# Process unnamed arguments (getters)
|
|
480
|
+
for arg in args:
|
|
481
|
+
if not isinstance(arg, str):
|
|
482
|
+
raise TypeError(f"Unnamed arguments must be strings, got {type(arg)}")
|
|
483
|
+
|
|
484
|
+
# Special handling for "WORM" - load WORM database
|
|
485
|
+
if arg.upper() == "WORM":
|
|
486
|
+
from ..data.worm import load_WORM
|
|
487
|
+
success = load_WORM(keep_default=False, messages=messages)
|
|
488
|
+
return_values.append(success)
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
# Parse the argument to get slots (handle nested access with $)
|
|
492
|
+
slots = arg.split('$')
|
|
493
|
+
|
|
494
|
+
# Get the value from thermo_sys
|
|
495
|
+
value = thermo_sys
|
|
496
|
+
for slot in slots:
|
|
497
|
+
# Handle OBIGT case-insensitively (R uses uppercase, Python uses lowercase)
|
|
498
|
+
slot_lower = slot.lower()
|
|
499
|
+
if hasattr(value, slot_lower):
|
|
500
|
+
value = getattr(value, slot_lower)
|
|
501
|
+
elif hasattr(value, slot):
|
|
502
|
+
value = getattr(value, slot)
|
|
503
|
+
elif isinstance(value, dict) and slot in value:
|
|
504
|
+
value = value[slot]
|
|
505
|
+
else:
|
|
506
|
+
raise AttributeError(f"Attribute '{arg}' not found in thermo object")
|
|
507
|
+
|
|
508
|
+
return_values.append(value)
|
|
509
|
+
|
|
510
|
+
# Process named arguments (setters)
|
|
511
|
+
setter_returns = {}
|
|
512
|
+
|
|
513
|
+
# Ensure system is initialized if needed (before setting any properties)
|
|
514
|
+
if not thermo_sys.is_initialized() and len(kwargs) > 0:
|
|
515
|
+
thermo_sys.reset(messages=messages)
|
|
516
|
+
|
|
517
|
+
for key, new_value in kwargs.items():
|
|
518
|
+
# Parse the key to get slots
|
|
519
|
+
slots = key.split('$')
|
|
520
|
+
|
|
521
|
+
# Get the original value before modification
|
|
522
|
+
orig_value = thermo_sys
|
|
523
|
+
for slot in slots:
|
|
524
|
+
# Handle case-insensitive attribute access (for OBIGT, etc.)
|
|
525
|
+
slot_lower = slot.lower()
|
|
526
|
+
if hasattr(orig_value, slot_lower):
|
|
527
|
+
orig_value = getattr(orig_value, slot_lower)
|
|
528
|
+
elif hasattr(orig_value, slot):
|
|
529
|
+
orig_value = getattr(orig_value, slot)
|
|
530
|
+
elif isinstance(orig_value, dict) and slot in orig_value:
|
|
531
|
+
orig_value = orig_value[slot]
|
|
532
|
+
else:
|
|
533
|
+
raise AttributeError(f"Attribute '{key}' not found in thermo object")
|
|
534
|
+
|
|
535
|
+
setter_returns[key] = orig_value
|
|
536
|
+
|
|
537
|
+
# Set the new value
|
|
538
|
+
if len(slots) == 1:
|
|
539
|
+
# Direct attribute
|
|
540
|
+
# Special handling for OBIGT - normalize index and handle refs
|
|
541
|
+
if slots[0].upper() == 'OBIGT':
|
|
542
|
+
# Handle OBIGT replacement with proper index normalization
|
|
543
|
+
_set_obigt_data(thermo_sys, new_value)
|
|
544
|
+
else:
|
|
545
|
+
# Use lowercase version if it exists (Python convention)
|
|
546
|
+
slot_lower = slots[0].lower()
|
|
547
|
+
if hasattr(thermo_sys, slot_lower):
|
|
548
|
+
setattr(thermo_sys, slot_lower, new_value)
|
|
549
|
+
else:
|
|
550
|
+
setattr(thermo_sys, slots[0], new_value)
|
|
551
|
+
elif len(slots) == 2:
|
|
552
|
+
# Nested attribute (e.g., opt$ideal.H)
|
|
553
|
+
parent = getattr(thermo_sys, slots[0])
|
|
554
|
+
if isinstance(parent, dict):
|
|
555
|
+
parent[slots[1]] = new_value
|
|
556
|
+
else:
|
|
557
|
+
setattr(parent, slots[1], new_value)
|
|
558
|
+
else:
|
|
559
|
+
# Deeper nesting (if needed)
|
|
560
|
+
current = thermo_sys
|
|
561
|
+
for i, slot in enumerate(slots[:-1]):
|
|
562
|
+
if hasattr(current, slot):
|
|
563
|
+
current = getattr(current, slot)
|
|
564
|
+
elif isinstance(current, dict) and slot in current:
|
|
565
|
+
current = current[slot]
|
|
566
|
+
|
|
567
|
+
# Set the final value
|
|
568
|
+
final_slot = slots[-1]
|
|
569
|
+
if isinstance(current, dict):
|
|
570
|
+
current[final_slot] = new_value
|
|
571
|
+
else:
|
|
572
|
+
setattr(current, final_slot, new_value)
|
|
573
|
+
|
|
574
|
+
# Determine return value based on R's behavior
|
|
575
|
+
if len(kwargs) > 0:
|
|
576
|
+
# If we had setters, return the original values as a named dict
|
|
577
|
+
# In R, setters always return a named list
|
|
578
|
+
if len(args) == 0:
|
|
579
|
+
# Only setters - return dict (named list in R)
|
|
580
|
+
return setter_returns
|
|
581
|
+
else:
|
|
582
|
+
# Mix of getters and setters - return all original values
|
|
583
|
+
combined = {}
|
|
584
|
+
for i, arg in enumerate(args):
|
|
585
|
+
combined[arg] = return_values[i]
|
|
586
|
+
combined.update(setter_returns)
|
|
587
|
+
return combined
|
|
588
|
+
else:
|
|
589
|
+
# Only getters
|
|
590
|
+
# Single unnamed argument returns the value directly
|
|
591
|
+
if len(return_values) == 1:
|
|
592
|
+
return return_values[0]
|
|
593
|
+
return return_values
|