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
@@ -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