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,16 @@
1
+ """
2
+ CHNOSZ Fortran interface package.
3
+
4
+ This package provides Python interfaces to the original CHNOSZ Fortran
5
+ subroutines for high-performance thermodynamic calculations.
6
+ """
7
+
8
+ from .h2o92_interface import H2O92Interface, get_h2o92_interface
9
+
10
+ __all__ = ['H2O92Interface', 'get_h2o92_interface']
11
+
12
+ # Tell pdoc to skip the compiled Fortran library files
13
+ # These are .so/.dll files that are loaded via ctypes, not Python imports
14
+ __pdoc__ = {
15
+ 'h2o92': False,
16
+ }
Binary file
@@ -0,0 +1,527 @@
1
+ """
2
+ Python interface to the H2O92 Fortran subroutine.
3
+
4
+ This module provides a ctypes-based interface to the original SUPCRT92
5
+ Fortran code for high-precision water property calculations.
6
+
7
+ The interface matches exactly what the R CHNOSZ package uses.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import ctypes
13
+ from ctypes import c_int, c_double, c_bool, POINTER
14
+ import numpy as np
15
+ from pathlib import Path
16
+ from typing import Dict, List, Union, Tuple
17
+ import warnings
18
+
19
+
20
+ class H2O92Interface:
21
+ """
22
+ Python interface to the H2O92 Fortran subroutine.
23
+
24
+ This provides the exact same computational kernel as used by
25
+ the R CHNOSZ package, ensuring identical results.
26
+ """
27
+
28
+ def __init__(self):
29
+ """Initialize the Fortran library interface."""
30
+ self._lib = None
31
+ self._load_fortran_library()
32
+ self._setup_function_signatures()
33
+
34
+ # Property names (matches H2O92 Fortran order)
35
+ self.property_names = [
36
+ 'A', 'G', 'S', 'U', 'H', 'Cv', 'Cp', 'Speed', 'alpha', 'beta',
37
+ 'epsilon', 'visc', 'tcond', 'surten', 'tdiff', 'Prndtl', 'visck',
38
+ 'albe', 'ZBorn', 'YBorn', 'QBorn', 'daldT', 'XBorn'
39
+ ]
40
+
41
+ # Cache for water property calculations (significant speedup for repeated T,P)
42
+ self._cache = {}
43
+ self._cache_enabled = True
44
+
45
+ def _load_fortran_library(self):
46
+ """Load the compiled Fortran shared library."""
47
+ lib_dir = Path(__file__).parent
48
+
49
+ # Determine library extension
50
+ if sys.platform == "win32":
51
+ lib_name = "h2o92.dll"
52
+ elif sys.platform == "darwin":
53
+ lib_name = "h2o92.dylib"
54
+ else:
55
+ lib_name = "h2o92.so"
56
+
57
+ lib_path = lib_dir / lib_name
58
+
59
+ if not lib_path.exists():
60
+ raise FileNotFoundError(
61
+ f"Fortran library not found: {lib_path}\n"
62
+ f"Please run compile_fortran.py to build the library first."
63
+ )
64
+
65
+ # On Windows, add MinGW bin to PATH to find runtime dependencies
66
+ original_path = None
67
+ if sys.platform == "win32":
68
+ mingw_bin = r'C:\msys64\mingw64\bin'
69
+ if os.path.exists(mingw_bin):
70
+ original_path = os.environ.get('PATH', '')
71
+ os.environ['PATH'] = mingw_bin + os.pathsep + original_path
72
+
73
+ try:
74
+ self._lib = ctypes.CDLL(str(lib_path))
75
+ except OSError as e:
76
+ # Try loading with WinDLL on Windows
77
+ if sys.platform == "win32":
78
+ try:
79
+ self._lib = ctypes.WinDLL(str(lib_path))
80
+ except OSError:
81
+ raise RuntimeError(
82
+ f"Failed to load Fortran library {lib_path}: {e}\n"
83
+ f"This may be due to missing MinGW runtime dependencies.\n"
84
+ f"Try: \n"
85
+ f"1. Ensure MinGW-w64 is properly installed\n"
86
+ f"2. Add C:\\msys64\\mingw64\\bin to your system PATH\n"
87
+ f"3. Install required MinGW runtime libraries"
88
+ )
89
+ else:
90
+ raise RuntimeError(f"Failed to load Fortran library {lib_path}: {e}")
91
+ finally:
92
+ # Restore original PATH
93
+ if original_path is not None:
94
+ os.environ['PATH'] = original_path
95
+
96
+ def _setup_function_signatures(self):
97
+ """Setup ctypes function signatures for Fortran subroutines."""
98
+
99
+ # H2O92 subroutine signature:
100
+ # SUBROUTINE H2O92(specs, states, props, error)
101
+ # INTEGER specs(10)
102
+ # DOUBLE PRECISION states(4), props(46)
103
+ # LOGICAL error
104
+
105
+ self._h2o92 = self._lib.h2o92_
106
+ self._h2o92.argtypes = [
107
+ POINTER(c_int * 10), # specs(10)
108
+ POINTER(c_double * 4), # states(4)
109
+ POINTER(c_double * 46), # props(46)
110
+ POINTER(c_bool) # error
111
+ ]
112
+ self._h2o92.restype = None
113
+
114
+ # Try to find IDEAL2 subroutine as well
115
+ if hasattr(self._lib, 'ideal2_'):
116
+ self._ideal2 = self._lib.ideal2_
117
+ self._ideal2.argtypes = [
118
+ POINTER(c_double), # T
119
+ POINTER(c_double), # dummy args (8 total)
120
+ POINTER(c_double),
121
+ POINTER(c_double),
122
+ POINTER(c_double),
123
+ POINTER(c_double),
124
+ POINTER(c_double),
125
+ POINTER(c_double)
126
+ ]
127
+ self._ideal2.restype = None
128
+
129
+ def calculate_properties_batch(self, T: np.ndarray, P: Union[np.ndarray, str],
130
+ properties: List[str] = None) -> Dict[str, np.ndarray]:
131
+ """
132
+ Calculate water properties for multiple T,P points (vectorized).
133
+
134
+ This is much faster than calling calculate_properties() in a loop
135
+ because it reduces ctypes overhead.
136
+
137
+ Parameters
138
+ ----------
139
+ T : array
140
+ Temperatures in Kelvin
141
+ P : array or "Psat"
142
+ Pressures in bar, or "Psat" for saturation pressure
143
+ properties : list of str, optional
144
+ Properties to calculate. If None, calculates all available.
145
+
146
+ Returns
147
+ -------
148
+ dict
149
+ Dictionary with calculated property arrays
150
+ """
151
+ T = np.atleast_1d(T)
152
+ n = len(T)
153
+
154
+ # Handle P input
155
+ if isinstance(P, str) and P == "Psat":
156
+ P_is_psat = True
157
+ P_vals = np.full(n, np.nan) # Placeholder
158
+ else:
159
+ P_is_psat = False
160
+ P_vals = np.atleast_1d(P)
161
+ if len(P_vals) < n:
162
+ P_vals = np.resize(P_vals, n)
163
+
164
+ # Initialize output arrays for all properties
165
+ all_props = self.property_names + ['V', 'rho', 'Psat', 'E', 'kT', 'A_DH', 'B_DH']
166
+ results = {prop: np.full(n, np.nan) for prop in all_props}
167
+
168
+ # Optimized calculation loop - minimize Python overhead
169
+ mw_h2o = 18.0152
170
+ cal_to_j = 4.184
171
+
172
+ # Pre-create specs arrays (reuse if possible)
173
+ if P_is_psat:
174
+ specs = (c_int * 10)(2, 2, 2, 5, 1, 1, 1, 1, 4, 0)
175
+ else:
176
+ specs = (c_int * 10)(2, 2, 2, 5, 1, 0, 2, 1, 4, 0)
177
+
178
+ # Reusable arrays
179
+ states = (c_double * 4)()
180
+ props = (c_double * 46)()
181
+ error = c_bool(False)
182
+
183
+ # Property name to index mapping (for fast lookup)
184
+ # R CHNOSZ water.R:159 includes 'tcond' in energy conversion list
185
+ energy_props_idx = {self.property_names.index(p): True for p in ['A', 'G', 'H', 'U', 'S', 'Cv', 'Cp', 'tcond'] if p in self.property_names}
186
+
187
+ for i in range(n):
188
+ if np.isnan(T[i]) or (not P_is_psat and np.isnan(P_vals[i])):
189
+ continue
190
+
191
+ # Check cache first (round to avoid floating point precision issues)
192
+ if self._cache_enabled:
193
+ if P_is_psat:
194
+ cache_key = (round(T[i], 6), 'Psat')
195
+ else:
196
+ cache_key = (round(T[i], 6), round(P_vals[i], 6))
197
+
198
+ if cache_key in self._cache:
199
+ # Use cached values
200
+ cached_props = self._cache[cache_key]
201
+ for prop_name in all_props:
202
+ if prop_name in cached_props:
203
+ results[prop_name][i] = cached_props[prop_name]
204
+ continue
205
+
206
+ # Setup states array
207
+ states[0] = T[i] - 273.15 # K to C
208
+ if P_is_psat:
209
+ states[1] = 0.0
210
+ states[2] = 1.0
211
+ else:
212
+ states[1] = P_vals[i]
213
+ states[2] = 1.0
214
+ states[3] = 0.0
215
+
216
+ # Reset error flag
217
+ error.value = False
218
+
219
+ # Call Fortran
220
+ try:
221
+ self._h2o92(ctypes.byref(specs), ctypes.byref(states),
222
+ ctypes.byref(props), ctypes.byref(error))
223
+ except:
224
+ continue
225
+
226
+ if error.value:
227
+ continue
228
+
229
+ # Extract results - optimized
230
+ rho = states[2]
231
+ rho2 = states[3]
232
+
233
+ if P_is_psat:
234
+ inc = 1 if rho2 > rho else 0
235
+ rho_liquid = rho2 if inc == 1 else rho
236
+ results['Psat'][i] = states[1]
237
+ else:
238
+ rho_liquid = rho
239
+ inc = 0
240
+
241
+ # Store for caching
242
+ if self._cache_enabled:
243
+ cached_result = {}
244
+
245
+ # Extract 23 properties - optimized loop
246
+ for j in range(len(self.property_names)):
247
+ prop_index = 2 * j + inc
248
+ if prop_index < 46:
249
+ val = props[prop_index]
250
+ # Apply unit conversions only to energy properties
251
+ if j in energy_props_idx:
252
+ val *= cal_to_j
253
+ results[self.property_names[j]][i] = val
254
+ if self._cache_enabled:
255
+ cached_result[self.property_names[j]] = val
256
+
257
+ # Derived properties
258
+ if rho_liquid > 0:
259
+ V_i = mw_h2o / rho_liquid
260
+ results['V'][i] = V_i
261
+ results['rho'][i] = rho_liquid * 1000
262
+
263
+ alpha_i = results['alpha'][i]
264
+ if not np.isnan(alpha_i):
265
+ results['E'][i] = V_i * alpha_i
266
+
267
+ beta_i = results['beta'][i]
268
+ if not np.isnan(beta_i):
269
+ results['kT'][i] = V_i * beta_i
270
+
271
+ eps = results['epsilon'][i]
272
+ if eps > 0:
273
+ sqrt_rho = rho_liquid**0.5
274
+ eps_T = eps * T[i]
275
+ results['A_DH'][i] = 1.8246e6 * sqrt_rho / (eps_T**1.5)
276
+ results['B_DH'][i] = 50.29e8 * sqrt_rho / (eps_T**0.5)
277
+
278
+ # Cache derived properties too
279
+ if self._cache_enabled:
280
+ cached_result['V'] = V_i
281
+ cached_result['rho'] = rho_liquid * 1000
282
+ if not np.isnan(alpha_i):
283
+ cached_result['E'] = results['E'][i]
284
+ if not np.isnan(beta_i):
285
+ cached_result['kT'] = results['kT'][i]
286
+ if eps > 0:
287
+ cached_result['A_DH'] = results['A_DH'][i]
288
+ cached_result['B_DH'] = results['B_DH'][i]
289
+ if P_is_psat:
290
+ cached_result['Psat'] = results['Psat'][i]
291
+
292
+ # Store in cache
293
+ if self._cache_enabled and cache_key:
294
+ self._cache[cache_key] = cached_result
295
+
296
+ # Filter requested properties
297
+ if properties is not None:
298
+ filtered_results = {}
299
+ for prop in properties:
300
+ if prop in results:
301
+ filtered_results[prop] = results[prop]
302
+ else:
303
+ raise ValueError(f"Property '{prop}' not available")
304
+ return filtered_results
305
+
306
+ return results
307
+
308
+ def calculate_properties(self, T: float, P: Union[float, str],
309
+ properties: List[str] = None) -> Dict[str, float]:
310
+ """
311
+ Calculate water properties using the H2O92 Fortran subroutine.
312
+
313
+ Parameters
314
+ ----------
315
+ T : float
316
+ Temperature in Kelvin
317
+ P : float or "Psat"
318
+ Pressure in bar, or "Psat" for saturation pressure
319
+ properties : list of str, optional
320
+ Properties to calculate. If None, calculates all available.
321
+
322
+ Returns
323
+ -------
324
+ dict
325
+ Dictionary with calculated properties
326
+
327
+ Examples
328
+ --------
329
+ >>> h2o = H2O92Interface()
330
+ >>> props = h2o.calculate_properties(298.15, 1.0, ['rho', 'epsilon'])
331
+ >>> print(f"Density: {props['rho']:.3f} g/cm³")
332
+ >>> print(f"Dielectric: {props['epsilon']:.1f}")
333
+ """
334
+
335
+ # Setup specs array (H2O92 parameters) - matches R exactly
336
+ # it, id, ip, ih, itripl, isat, iopt, useLVS, epseqn, icrit
337
+ # From R: specs <- c(2, 2, 2, 5, 1, isat, iopt, 1, 4, 0)
338
+ if isinstance(P, str) and P == "Psat":
339
+ isat = 1
340
+ iopt = 1 # T,D input for saturation
341
+ else:
342
+ isat = 0
343
+ iopt = 2 # T,P input for single phase
344
+
345
+ specs = (c_int * 10)(2, 2, 2, 5, 1, isat, iopt, 1, 4, 0)
346
+
347
+ # Setup states array
348
+ states = (c_double * 4)()
349
+ # Temperature must be in Celsius (like R does: Tc <- convert(T, "C"))
350
+ states[0] = T - 273.15 # Convert K to C
351
+ if isinstance(P, str) and P == "Psat":
352
+ states[1] = 0.0 # Pressure not used for saturation
353
+ states[2] = 1.0 # Initial density guess (g/cm³)
354
+ else:
355
+ states[1] = P # Pressure in bar
356
+ states[2] = 1.0 # Initial density guess (g/cm³)
357
+ states[3] = 0.0 # Second density for two-phase
358
+
359
+ # Setup output arrays
360
+ props = (c_double * 46)() # 46 properties (23 vapor + 23 liquid)
361
+ error = c_bool(False)
362
+
363
+ # Call Fortran subroutine
364
+ try:
365
+ self._h2o92(ctypes.byref(specs), ctypes.byref(states),
366
+ ctypes.byref(props), ctypes.byref(error))
367
+ except Exception as e:
368
+ raise RuntimeError(f"Fortran subroutine call failed: {e}")
369
+
370
+ # Check for errors
371
+ if error.value:
372
+ warnings.warn(f"H2O92 calculation error at T={T:.1f}K, P={P}")
373
+ return {prop: np.nan for prop in self.property_names}
374
+
375
+ # Extract results following R's approach exactly
376
+ results = {}
377
+
378
+ # Determine which phase to use (liquid vs vapor) - from R water.R
379
+ # R code: rho <- H2O[[2]][3]; rho2 <- H2O[[2]][4]
380
+ rho = states[2] # First phase density
381
+ rho2 = states[3] # Second phase density
382
+
383
+ if isinstance(P, str) and P == "Psat":
384
+ # For saturation: use liquid phase (denser)
385
+ if rho2 > rho:
386
+ rho_liquid = rho2
387
+ inc = 1 # Second state is liquid (R: inc <- 1)
388
+ else:
389
+ rho_liquid = rho
390
+ inc = 0 # First state is liquid (R: inc <- 0)
391
+ results['Psat'] = states[1] # Saturation pressure
392
+ else:
393
+ # Single phase calculation
394
+ rho_liquid = states[2]
395
+ inc = 0 # Use first state
396
+
397
+ # Extract properties following R's method exactly:
398
+ # R: w <- t(H2O[[3]][seq(1, 45, length.out = 23)+inc])
399
+ # seq(1, 45, length.out = 23) gives: 1, 3, 5, 7, ..., 45 (in R 1-based indexing)
400
+ # In Python 0-based: 0, 2, 4, 6, ..., 44, then add inc
401
+ for i, prop_name in enumerate(self.property_names):
402
+ prop_index = 2 * i + inc # Every other element, offset by inc
403
+ if prop_index < 46:
404
+ results[prop_name] = props[prop_index]
405
+
406
+ # Apply R CHNOSZ-compatible unit conversions and derived property calculations
407
+ mw_h2o = 18.0152 # g/mol (matches R SUP92.f)
408
+
409
+ # Energy unit conversion: Following R CHNOSZ exactly
410
+ # R gets values from FORTRAN (in cal/mol with ih=5), then converts TO Joules
411
+ # Line 159-160 in R water.R:
412
+ # isenergy <- names(w.out) %in% c("A", "G", "S", "U", "H", "Cv", "Cp", "tcond")
413
+ # if(any(isenergy)) w.out[, isenergy] <- convert(w.out[, isenergy], "J")
414
+ cal_to_j = 4.184 # Conversion factor from cal to J (R uses this)
415
+
416
+ # Convert thermodynamic properties from cal/mol to J/mol (like R does)
417
+ energy_props = ['A', 'G', 'H', 'U'] # Extensive thermodynamic properties
418
+ for prop in energy_props:
419
+ if prop in results:
420
+ results[prop] = results[prop] * cal_to_j
421
+
422
+ # Convert heat capacities and entropy from cal/mol/K to J/mol/K (like R does)
423
+ entropy_props = ['S', 'Cv', 'Cp']
424
+ for prop in entropy_props:
425
+ if prop in results:
426
+ results[prop] = results[prop] * cal_to_j
427
+
428
+ # Convert thermal conductivity from cal/(s·cm·K) to W/(m·K) (like R does)
429
+ # R includes 'tcond' in the energy conversion list (water.R:159)
430
+ if 'tcond' in results:
431
+ results['tcond'] = results['tcond'] * cal_to_j
432
+
433
+ # Molar volume: cm³/mol (matches R calculation)
434
+ if rho_liquid > 0:
435
+ results['V'] = mw_h2o / rho_liquid # cm³/mol
436
+ else:
437
+ results['V'] = np.nan
438
+
439
+ # Density conversion: g/cm³ → kg/m³ (matches R line 131: rho <- rho.out*1000)
440
+ results['rho'] = rho_liquid * 1000 # Convert g/cm³ to kg/m³ like R
441
+
442
+ # Derived properties (matches R lines 135-136)
443
+ if 'V' in results and not np.isnan(results['V']):
444
+ # E = V * alpha (thermal expansivity)
445
+ results['E'] = results['V'] * results['alpha'] if 'alpha' in results else np.nan
446
+ # kT = V * beta (isothermal compressibility)
447
+ results['kT'] = results['V'] * results['beta'] if 'beta' in results else np.nan
448
+
449
+ # Debye-Hückel parameters (matches R lines 140-141)
450
+ # A_DH <- 1.8246e6 * rho.out^0.5 / (epsilon * T)^1.5
451
+ # B_DH <- 50.29e8 * rho.out^0.5 / (epsilon * T)^0.5
452
+ # Note: R actually does use 50.29e8 - must match R exactly
453
+ if rho_liquid > 0 and 'epsilon' in results and results['epsilon'] > 0:
454
+ results['A_DH'] = 1.8246e6 * (rho_liquid**0.5) / ((results['epsilon'] * T)**1.5)
455
+ results['B_DH'] = 50.29e8 * (rho_liquid**0.5) / ((results['epsilon'] * T)**0.5) # Match R: 50.29e8
456
+ else:
457
+ results['A_DH'] = np.nan
458
+ results['B_DH'] = np.nan
459
+
460
+ # Filter requested properties
461
+ if properties is not None:
462
+ filtered_results = {}
463
+ for prop in properties:
464
+ if prop in results:
465
+ filtered_results[prop] = results[prop]
466
+ else:
467
+ raise ValueError(f"Property '{prop}' not available")
468
+ return filtered_results
469
+
470
+ return results
471
+
472
+ def calculate_ideal_gas(self, T: float, property: str) -> float:
473
+ """
474
+ Calculate ideal gas properties using IDEAL2 subroutine.
475
+
476
+ Parameters
477
+ ----------
478
+ T : float
479
+ Temperature in Kelvin
480
+ property : str
481
+ 'S' for entropy or 'Cp' for heat capacity
482
+
483
+ Returns
484
+ -------
485
+ float
486
+ Property value
487
+ """
488
+ if not hasattr(self, '_ideal2'):
489
+ raise NotImplementedError("IDEAL2 subroutine not available in library")
490
+
491
+ # Setup parameters (8 dummy args)
492
+ args = [c_double(T)] + [c_double(0.0) for _ in range(7)]
493
+
494
+ # Call Fortran subroutine
495
+ self._ideal2(*[ctypes.byref(arg) for arg in args])
496
+
497
+ if property == 'S':
498
+ return args[3].value # 4th output
499
+ elif property == 'Cp':
500
+ return args[7].value # 8th output
501
+ else:
502
+ raise ValueError(f"Property '{property}' not supported by IDEAL2")
503
+
504
+ def available_properties(self) -> List[str]:
505
+ """Get list of available water properties."""
506
+ return self.property_names + ['V', 'rho', 'Psat', 'E', 'kT', 'A_DH', 'B_DH']
507
+
508
+ def clear_cache(self):
509
+ """Clear the water properties cache."""
510
+ self._cache.clear()
511
+
512
+ def enable_cache(self, enabled: bool = True):
513
+ """Enable or disable caching of water properties."""
514
+ self._cache_enabled = enabled
515
+ if not enabled:
516
+ self.clear_cache()
517
+
518
+
519
+ # Global instance for easy access
520
+ _h2o92_interface = None
521
+
522
+ def get_h2o92_interface() -> H2O92Interface:
523
+ """Get global H2O92Interface instance (singleton pattern)."""
524
+ global _h2o92_interface
525
+ if _h2o92_interface is None:
526
+ _h2o92_interface = H2O92Interface()
527
+ return _h2o92_interface
@@ -0,0 +1,21 @@
1
+ """
2
+ Geochemistry package for CHNOSZ.
3
+
4
+ This package provides specialized geochemical calculations including
5
+ mineral equilibria, redox reactions, and environmental applications.
6
+ """
7
+
8
+ from .minerals import (
9
+ mineral_solubility, stability_field, phase_boundary,
10
+ MineralEquilibria
11
+ )
12
+ from .redox import (
13
+ eh_ph, pe, eh, logfO2,
14
+ RedoxCalculator
15
+ )
16
+
17
+ __all__ = [
18
+ 'mineral_solubility', 'stability_field', 'phase_boundary',
19
+ 'MineralEquilibria', 'eh_ph', 'pe', 'eh', 'logfO2',
20
+ 'RedoxCalculator'
21
+ ]