pychnosz 1.1.11__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pychnosz/__init__.py +129 -0
- pychnosz/biomolecules/__init__.py +29 -0
- pychnosz/biomolecules/ionize_aa.py +197 -0
- pychnosz/biomolecules/proteins.py +595 -0
- pychnosz/core/__init__.py +46 -0
- pychnosz/core/affinity.py +1256 -0
- pychnosz/core/animation.py +593 -0
- pychnosz/core/balance.py +334 -0
- pychnosz/core/basis.py +716 -0
- pychnosz/core/diagram.py +3336 -0
- pychnosz/core/equilibrate.py +813 -0
- pychnosz/core/equilibrium.py +554 -0
- pychnosz/core/info.py +821 -0
- pychnosz/core/retrieve.py +364 -0
- pychnosz/core/speciation.py +580 -0
- pychnosz/core/species.py +599 -0
- pychnosz/core/subcrt.py +1696 -0
- pychnosz/core/thermo.py +593 -0
- pychnosz/core/unicurve.py +1226 -0
- pychnosz/data/__init__.py +11 -0
- pychnosz/data/add_obigt.py +327 -0
- pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
- pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
- pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
- pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
- pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
- pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
- pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
- pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
- pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
- pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
- pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
- pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
- pychnosz/data/extdata/Berman/sympy.R +99 -0
- pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
- pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
- pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
- pychnosz/data/extdata/OBIGT/AD.csv +25 -0
- pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
- pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
- pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
- pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
- pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
- pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
- pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
- pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
- pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
- pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
- pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
- pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
- pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
- pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
- pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
- pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
- pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
- pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
- pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
- pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
- pychnosz/data/extdata/misc/BZA10.csv +5 -0
- pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
- pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
- pychnosz/data/extdata/misc/LA19_test.csv +7 -0
- pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
- pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
- pychnosz/data/extdata/misc/PM90.csv +7 -0
- pychnosz/data/extdata/misc/RH95.csv +23 -0
- pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
- pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
- pychnosz/data/extdata/misc/SK95.csv +55 -0
- pychnosz/data/extdata/misc/SOJSH.csv +61 -0
- pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
- pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
- pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
- pychnosz/data/extdata/misc/bluered.txt +1000 -0
- pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
- pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
- pychnosz/data/extdata/protein/Cas/download.R +34 -0
- pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
- pychnosz/data/extdata/protein/POLG.csv +12 -0
- pychnosz/data/extdata/protein/TBD+05.csv +393 -0
- pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
- pychnosz/data/extdata/protein/rubisco.csv +28 -0
- pychnosz/data/extdata/protein/rubisco.fasta +239 -0
- pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
- pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
- pychnosz/data/extdata/src/README.txt +5 -0
- pychnosz/data/extdata/taxonomy/names.dmp +215 -0
- pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
- pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
- pychnosz/data/extdata/thermo/buffer.csv +40 -0
- pychnosz/data/extdata/thermo/element.csv +135 -0
- pychnosz/data/extdata/thermo/groups.csv +6 -0
- pychnosz/data/extdata/thermo/opt.csv +2 -0
- pychnosz/data/extdata/thermo/protein.csv +506 -0
- pychnosz/data/extdata/thermo/refs.csv +343 -0
- pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
- pychnosz/data/loader.py +431 -0
- pychnosz/data/mod_obigt.py +322 -0
- pychnosz/data/obigt.py +471 -0
- pychnosz/data/worm.py +228 -0
- pychnosz/fortran/__init__.py +16 -0
- pychnosz/fortran/h2o92.dll +0 -0
- pychnosz/fortran/h2o92_interface.py +527 -0
- pychnosz/geochemistry/__init__.py +21 -0
- pychnosz/geochemistry/minerals.py +514 -0
- pychnosz/geochemistry/redox.py +500 -0
- pychnosz/models/__init__.py +47 -0
- pychnosz/models/archer_wang.py +165 -0
- pychnosz/models/berman.py +309 -0
- pychnosz/models/cgl.py +381 -0
- pychnosz/models/dew.py +997 -0
- pychnosz/models/hkf.py +523 -0
- pychnosz/models/hkf_helpers.py +231 -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.11.dist-info/METADATA +197 -0
- pychnosz-1.1.11.dist-info/RECORD +128 -0
- pychnosz-1.1.11.dist-info/WHEEL +5 -0
- pychnosz-1.1.11.dist-info/licenses/LICENSE.txt +19 -0
- pychnosz-1.1.11.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
|
+
]
|