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.
- 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.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 +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 +128 -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/models/dew.py
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DEW (Deep Earth Water) model implementation.
|
|
3
|
+
|
|
4
|
+
This module implements the Deep Earth Water model for calculating thermodynamic
|
|
5
|
+
and electrostatic properties of H2O at high pressures and temperatures relevant
|
|
6
|
+
to deep crustal and mantle conditions.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
- Sverjensky, D. A., Harrison, B., & Azzolini, D. (2014). Water in the deep Earth:
|
|
10
|
+
The dielectric constant and the solubilities of quartz and corundum to 60 kb
|
|
11
|
+
and 1200°C. Geochimica et Cosmochimica Acta, 129, 125-145.
|
|
12
|
+
- Pan, D., Spanu, L., Harrison, B., Sverjensky, D. A., & Car, R. (2013).
|
|
13
|
+
Dielectric properties of water under extreme conditions and validation of the
|
|
14
|
+
corresponding computational approaches. PNAS, 110(17), 6646-6650.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from typing import Union, List, Optional, Dict, Any
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DEWWater:
|
|
23
|
+
"""
|
|
24
|
+
Deep Earth Water (DEW) model implementation.
|
|
25
|
+
|
|
26
|
+
This class provides thermodynamic and electrostatic properties of water
|
|
27
|
+
at high pressures and temperatures using the DEW model correlations.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""Initialize DEW water model."""
|
|
32
|
+
# Physical constants
|
|
33
|
+
self.R = 8.314462618 # J/(mol·K) - Universal gas constant
|
|
34
|
+
self.MW_H2O = 18.0152 # g/mol - Molecular weight of water
|
|
35
|
+
|
|
36
|
+
# Critical constants for water
|
|
37
|
+
self.Tc = 647.067 # K - Critical temperature
|
|
38
|
+
self.Pc = 220.48 # bar - Critical pressure
|
|
39
|
+
self.rhoc = 0.32174 # g/cm³ - Critical density
|
|
40
|
+
|
|
41
|
+
# DEW model parameters
|
|
42
|
+
self.a_epsilon = np.array([
|
|
43
|
+
-1.57637700752506e3, -6.97284414953487e4, -3.14058873029023e6,
|
|
44
|
+
1.11926957750896e7, 5.49375634503012e7, -1.33934314022535e8,
|
|
45
|
+
-2.56395839070779e8, 3.73875501063673e8, 4.35976880906701e8,
|
|
46
|
+
-2.11156427436252e8
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
self.b_epsilon = np.array([
|
|
50
|
+
5.07722476345932e-1, 1.48046755524790e1, 2.42452179259584e2,
|
|
51
|
+
-1.73986255629880e3, -7.18635413197094e3, 1.21415969235037e4,
|
|
52
|
+
1.92102380413670e4, -1.31967093058141e4, -1.35915853762697e4,
|
|
53
|
+
3.17251296019127e3
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
# Pressure and temperature limits for DEW model
|
|
57
|
+
self.T_min = 273.15 # K
|
|
58
|
+
self.T_max = 1473.15 # K (1200°C)
|
|
59
|
+
self.P_min = 1.0 # bar
|
|
60
|
+
self.P_max = 60000.0 # bar (60 kbar)
|
|
61
|
+
|
|
62
|
+
def available_properties(self) -> List[str]:
|
|
63
|
+
"""
|
|
64
|
+
Get list of available water properties.
|
|
65
|
+
|
|
66
|
+
Note: DEW model only calculates a limited set of properties.
|
|
67
|
+
This matches the R CHNOSZ implementation which only provides:
|
|
68
|
+
G, epsilon, QBorn, V, rho, beta, A_DH, B_DH
|
|
69
|
+
|
|
70
|
+
Other properties requested will return NaN.
|
|
71
|
+
"""
|
|
72
|
+
return [
|
|
73
|
+
# Properties actually calculated by DEW
|
|
74
|
+
'G', # Gibbs energy (J/mol)
|
|
75
|
+
'epsilon', # Dielectric constant (DEW specialty)
|
|
76
|
+
'QBorn', # Born Q function (1/bar)
|
|
77
|
+
'V', # Molar volume (cm³/mol)
|
|
78
|
+
'rho', # Density (kg/m³)
|
|
79
|
+
'beta', # Isothermal compressibility (1/bar)
|
|
80
|
+
'A_DH', # Debye-Hückel A parameter
|
|
81
|
+
'B_DH', # Debye-Hückel B parameter
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
def calculate(self,
|
|
85
|
+
properties: Union[str, List[str]],
|
|
86
|
+
T: Union[float, np.ndarray] = 298.15,
|
|
87
|
+
P: Union[float, np.ndarray, str] = 1.0,
|
|
88
|
+
**kwargs) -> Union[float, np.ndarray, Dict[str, Any]]:
|
|
89
|
+
"""
|
|
90
|
+
Calculate water properties using DEW model.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
properties : str or list of str
|
|
95
|
+
Property or properties to calculate
|
|
96
|
+
T : float or array
|
|
97
|
+
Temperature in Kelvin
|
|
98
|
+
P : float, array, or 'Psat'
|
|
99
|
+
Pressure in bar, or 'Psat' for saturation pressure
|
|
100
|
+
**kwargs
|
|
101
|
+
Additional options
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
float, array, or dict
|
|
106
|
+
Calculated properties
|
|
107
|
+
"""
|
|
108
|
+
# DEBUG
|
|
109
|
+
debug_dew = False
|
|
110
|
+
if debug_dew:
|
|
111
|
+
print(f"\nDEBUG DEW.calculate() called:")
|
|
112
|
+
print(f" properties: {properties}")
|
|
113
|
+
print(f" T (input): {T}, type: {type(T)}")
|
|
114
|
+
print(f" P (input): {P}, type: {type(P)}")
|
|
115
|
+
|
|
116
|
+
# Handle input types
|
|
117
|
+
if isinstance(properties, str):
|
|
118
|
+
properties = [properties]
|
|
119
|
+
single_prop = True
|
|
120
|
+
else:
|
|
121
|
+
single_prop = False
|
|
122
|
+
|
|
123
|
+
# Convert inputs to arrays
|
|
124
|
+
T = np.atleast_1d(np.asarray(T, dtype=float))
|
|
125
|
+
|
|
126
|
+
if isinstance(P, str) and P == 'Psat':
|
|
127
|
+
P_is_Psat = True
|
|
128
|
+
P_vals = self._calculate_Psat(T)
|
|
129
|
+
else:
|
|
130
|
+
P_is_Psat = False
|
|
131
|
+
P = np.atleast_1d(np.asarray(P, dtype=float))
|
|
132
|
+
if len(P) < len(T):
|
|
133
|
+
P = np.resize(P, len(T))
|
|
134
|
+
elif len(T) < len(P):
|
|
135
|
+
T = np.resize(T, len(P))
|
|
136
|
+
P_vals = P
|
|
137
|
+
|
|
138
|
+
# Check validity of conditions
|
|
139
|
+
valid = self._check_validity(T, P_vals)
|
|
140
|
+
|
|
141
|
+
# Check for low T or low P conditions (T < 100°C or P < 1000 bar)
|
|
142
|
+
# These should use SUPCRT92 instead of DEW (as in R CHNOSZ water.R line 381)
|
|
143
|
+
ilow = (T < 373.15) | (P_vals < 1000)
|
|
144
|
+
|
|
145
|
+
# Initialize results
|
|
146
|
+
results = {}
|
|
147
|
+
|
|
148
|
+
# Get list of properties that DEW actually calculates
|
|
149
|
+
supported_props = self.available_properties()
|
|
150
|
+
|
|
151
|
+
# For low T or low P conditions, use SUPCRT92 for ALL properties
|
|
152
|
+
if np.any(ilow):
|
|
153
|
+
from .supcrt92_fortran import water_SUPCRT92
|
|
154
|
+
|
|
155
|
+
# Get SUPCRT92 results for low conditions
|
|
156
|
+
T_low = T[ilow]
|
|
157
|
+
P_low = P_vals[ilow]
|
|
158
|
+
|
|
159
|
+
supcrt_results = water_SUPCRT92(properties, T_low, P_low)
|
|
160
|
+
|
|
161
|
+
# Initialize all properties with appropriate array size
|
|
162
|
+
for prop in properties:
|
|
163
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
164
|
+
|
|
165
|
+
# Fill in SUPCRT92 results for low conditions
|
|
166
|
+
if isinstance(supcrt_results, dict):
|
|
167
|
+
for prop in properties:
|
|
168
|
+
if prop in supcrt_results:
|
|
169
|
+
results[prop][ilow] = supcrt_results[prop]
|
|
170
|
+
else:
|
|
171
|
+
# Single property case
|
|
172
|
+
results[properties[0]][ilow] = supcrt_results
|
|
173
|
+
|
|
174
|
+
# Special case for Pr,Tr: epsilon should be 78.47 (DEW spreadsheet value)
|
|
175
|
+
iPrTr = (np.abs(T - 298.15) < 0.1) & (np.abs(P_vals - 1.0) < 0.1)
|
|
176
|
+
if 'epsilon' in properties and np.any(iPrTr):
|
|
177
|
+
results['epsilon'][iPrTr] = 78.47
|
|
178
|
+
|
|
179
|
+
# If all conditions are low, return SUPCRT results
|
|
180
|
+
if np.all(ilow):
|
|
181
|
+
if single_prop:
|
|
182
|
+
result = results[properties[0]]
|
|
183
|
+
return result[0] if len(result) == 1 else result
|
|
184
|
+
else:
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
# Calculate density first (needed for many DEW properties)
|
|
188
|
+
if any(prop in properties for prop in ['rho', 'V', 'epsilon', 'QBorn', 'beta', 'A_DH', 'B_DH']):
|
|
189
|
+
rho_gcm3 = self._calculate_density(T, P_vals, valid) # g/cm³
|
|
190
|
+
rho = rho_gcm3 * 1000.0 # Convert to kg/m³ like SUPCRT
|
|
191
|
+
V = self.MW_H2O / rho_gcm3 # cm³/mol (use g/cm³ for volume calculation)
|
|
192
|
+
else:
|
|
193
|
+
rho = None
|
|
194
|
+
V = None
|
|
195
|
+
|
|
196
|
+
# Calculate each requested property for high T and high P conditions
|
|
197
|
+
for prop in properties:
|
|
198
|
+
# If property is not supported by DEW, return NaN (like R CHNOSZ)
|
|
199
|
+
if prop not in supported_props:
|
|
200
|
+
if prop not in results:
|
|
201
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
202
|
+
elif prop == 'rho':
|
|
203
|
+
if prop not in results:
|
|
204
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
205
|
+
results[prop][~ilow] = rho[~ilow]
|
|
206
|
+
elif prop == 'V':
|
|
207
|
+
if prop not in results:
|
|
208
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
209
|
+
results[prop][~ilow] = V[~ilow]
|
|
210
|
+
elif prop == 'epsilon':
|
|
211
|
+
# Use g/cm³ density for dielectric constant calculation
|
|
212
|
+
if prop not in results:
|
|
213
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
214
|
+
if 'rho_gcm3' not in locals():
|
|
215
|
+
rho_gcm3 = self._calculate_density(T, P_vals, valid)
|
|
216
|
+
epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)
|
|
217
|
+
results[prop][~ilow] = epsilon_vals[~ilow]
|
|
218
|
+
elif prop == 'G':
|
|
219
|
+
# Calculate Gibbs energy using the exact DEW method
|
|
220
|
+
if prop not in results:
|
|
221
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
222
|
+
for i in np.where(~ilow)[0]:
|
|
223
|
+
T_celsius = T[i] - 273.15 # Convert to Celsius
|
|
224
|
+
G_cal_per_mol = self._calculate_gibbs_of_water(P_vals[i], T_celsius) # cal/mol
|
|
225
|
+
if not np.isnan(G_cal_per_mol):
|
|
226
|
+
results[prop][i] = G_cal_per_mol * 4.184 # Convert cal/mol to J/mol
|
|
227
|
+
elif prop == 'QBorn':
|
|
228
|
+
# Calculate QBorn using the exact DEW calculateQ function
|
|
229
|
+
if prop not in results:
|
|
230
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
231
|
+
if rho is None or V is None:
|
|
232
|
+
rho_gcm3 = self._calculate_density(T, P_vals, valid)
|
|
233
|
+
else:
|
|
234
|
+
rho_gcm3 = rho / 1000.0 # Convert kg/m³ to g/cm³
|
|
235
|
+
|
|
236
|
+
for i in np.where(~ilow)[0]:
|
|
237
|
+
if valid[i]:
|
|
238
|
+
T_celsius = T[i] - 273.15 # Convert to Celsius
|
|
239
|
+
results[prop][i] = self._calculate_Q(rho_gcm3[i], T_celsius)
|
|
240
|
+
elif prop == 'beta':
|
|
241
|
+
# Calculate beta (isothermal compressibility)
|
|
242
|
+
if prop not in results:
|
|
243
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
244
|
+
if rho is None or V is None:
|
|
245
|
+
rho_gcm3 = self._calculate_density(T, P_vals, valid)
|
|
246
|
+
else:
|
|
247
|
+
rho_gcm3 = rho / 1000.0 # Convert kg/m³ to g/cm³
|
|
248
|
+
|
|
249
|
+
for i in np.where(~ilow)[0]:
|
|
250
|
+
if valid[i]:
|
|
251
|
+
T_celsius = T[i] - 273.15 # Convert to Celsius
|
|
252
|
+
# Divide drhodP by rho to get units of bar^-1 (like R code)
|
|
253
|
+
drhodP = self._calculate_drhodP(rho_gcm3[i], T_celsius)
|
|
254
|
+
results[prop][i] = drhodP / rho_gcm3[i]
|
|
255
|
+
elif prop in ['A_DH', 'B_DH']:
|
|
256
|
+
# Calculate Debye-Hückel parameters
|
|
257
|
+
if prop not in results:
|
|
258
|
+
results[prop] = np.full_like(T, np.nan, dtype=float)
|
|
259
|
+
if 'rho_gcm3' not in locals():
|
|
260
|
+
rho_gcm3 = self._calculate_density(T, P_vals, valid)
|
|
261
|
+
epsilon_vals = self._calculate_dielectric_constant_with_density(T, P_vals, rho_gcm3, valid)
|
|
262
|
+
|
|
263
|
+
if prop == 'A_DH':
|
|
264
|
+
# A_DH = 1.8246e6 * rho^0.5 / (epsilon * T)^1.5
|
|
265
|
+
results[prop][~ilow] = 1.8246e6 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**1.5
|
|
266
|
+
else: # B_DH
|
|
267
|
+
# B_DH = 50.29e8 * rho^0.5 / (epsilon * T)^0.5
|
|
268
|
+
results[prop][~ilow] = 50.29e8 * rho_gcm3[~ilow]**0.5 / (epsilon_vals[~ilow] * T[~ilow])**0.5
|
|
269
|
+
|
|
270
|
+
# Return results
|
|
271
|
+
if single_prop:
|
|
272
|
+
result = results[properties[0]]
|
|
273
|
+
return result[0] if len(result) == 1 else result
|
|
274
|
+
else:
|
|
275
|
+
# Convert to consistent array lengths
|
|
276
|
+
for key in results:
|
|
277
|
+
if np.isscalar(results[key]):
|
|
278
|
+
results[key] = np.full_like(T, results[key])
|
|
279
|
+
elif len(results[key]) == 1 and len(T) > 1:
|
|
280
|
+
results[key] = np.full_like(T, results[key][0])
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
def _check_validity(self, T: np.ndarray, P: np.ndarray) -> np.ndarray:
|
|
284
|
+
"""Check validity of T-P conditions for DEW model."""
|
|
285
|
+
valid = np.ones_like(T, dtype=bool)
|
|
286
|
+
|
|
287
|
+
# Temperature limits
|
|
288
|
+
valid &= (T >= self.T_min)
|
|
289
|
+
valid &= (T <= self.T_max)
|
|
290
|
+
|
|
291
|
+
# Pressure limits
|
|
292
|
+
valid &= (P >= self.P_min)
|
|
293
|
+
valid &= (P <= self.P_max)
|
|
294
|
+
|
|
295
|
+
# Avoid near-critical conditions where DEW may be less accurate
|
|
296
|
+
valid &= ~((T > 0.95 * self.Tc) & (P < 2 * self.Pc))
|
|
297
|
+
|
|
298
|
+
return valid
|
|
299
|
+
|
|
300
|
+
def _calculate_Psat(self, T: np.ndarray) -> np.ndarray:
|
|
301
|
+
"""
|
|
302
|
+
Calculate saturation pressure using Antoine equation.
|
|
303
|
+
|
|
304
|
+
Valid up to critical point.
|
|
305
|
+
"""
|
|
306
|
+
Psat = np.full_like(T, np.nan)
|
|
307
|
+
valid = (T >= 273.16) & (T <= self.Tc)
|
|
308
|
+
|
|
309
|
+
if np.any(valid):
|
|
310
|
+
T_valid = T[valid]
|
|
311
|
+
|
|
312
|
+
# Antoine equation coefficients for water (bar, K)
|
|
313
|
+
A = 8.07131
|
|
314
|
+
B = 1730.63
|
|
315
|
+
C = -39.724
|
|
316
|
+
|
|
317
|
+
# Antoine equation: log10(Psat) = A - B/(T + C)
|
|
318
|
+
log10_Psat = A - B / (T_valid + C)
|
|
319
|
+
Psat[valid] = 10**log10_Psat
|
|
320
|
+
|
|
321
|
+
return Psat
|
|
322
|
+
|
|
323
|
+
def _calculate_density(self, T: np.ndarray, P: np.ndarray, valid: np.ndarray) -> np.ndarray:
|
|
324
|
+
"""
|
|
325
|
+
Calculate water density using DEW model correlations.
|
|
326
|
+
|
|
327
|
+
This uses the exact bisection method from the R CHNOSZ DEW implementation
|
|
328
|
+
to find the density that produces the target pressure.
|
|
329
|
+
"""
|
|
330
|
+
rho = np.full_like(T, np.nan)
|
|
331
|
+
|
|
332
|
+
if np.any(valid):
|
|
333
|
+
T_valid = T[valid]
|
|
334
|
+
P_valid = P[valid]
|
|
335
|
+
|
|
336
|
+
# Use bisection method for each T, P pair (as in R DEW.R)
|
|
337
|
+
rho_results = np.full(len(T_valid), np.nan)
|
|
338
|
+
for i, (T_val, P_val) in enumerate(zip(T_valid, P_valid)):
|
|
339
|
+
T_celsius = T_val - 273.15 # Convert to Celsius for DEW equations
|
|
340
|
+
rho_results[i] = self._calculate_density_bisection(P_val, T_celsius)
|
|
341
|
+
|
|
342
|
+
rho[valid] = rho_results
|
|
343
|
+
|
|
344
|
+
return rho
|
|
345
|
+
|
|
346
|
+
def _calculate_density_bisection(self, pressure: float, temperature_celsius: float, error: float = 0.01) -> float:
|
|
347
|
+
"""
|
|
348
|
+
Calculate density using bisection method (exact R DEW.R implementation).
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
pressure : float
|
|
353
|
+
Target pressure in bar
|
|
354
|
+
temperature_celsius : float
|
|
355
|
+
Temperature in Celsius
|
|
356
|
+
error : float
|
|
357
|
+
Pressure error tolerance in bar (default 0.01 as in R code)
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
float
|
|
362
|
+
Density in g/cm³
|
|
363
|
+
"""
|
|
364
|
+
min_guess = 1e-5
|
|
365
|
+
guess = 1e-5
|
|
366
|
+
equation = 1 # The maxGuess is dependent on the value of "equation"
|
|
367
|
+
max_guess = 7.5 * equation - 5.0 # Should be 2.5 for equation=1
|
|
368
|
+
|
|
369
|
+
# Loop through and find the density (up to 50 iterations as in R)
|
|
370
|
+
for i in range(50):
|
|
371
|
+
# Calculate the pressure using the specified equation
|
|
372
|
+
calc_p = self._calculate_pressure(guess, temperature_celsius)
|
|
373
|
+
|
|
374
|
+
# If the calculated pressure is not equal to input pressure,
|
|
375
|
+
# determine a new guess based on bisection method
|
|
376
|
+
if abs(calc_p - pressure) > error:
|
|
377
|
+
if calc_p > pressure:
|
|
378
|
+
max_guess = guess
|
|
379
|
+
guess = (guess + min_guess) / 2.0
|
|
380
|
+
elif calc_p < pressure:
|
|
381
|
+
min_guess = guess
|
|
382
|
+
guess = (guess + max_guess) / 2.0
|
|
383
|
+
else:
|
|
384
|
+
return guess
|
|
385
|
+
|
|
386
|
+
# If we didn't converge, return the last guess
|
|
387
|
+
return guess
|
|
388
|
+
|
|
389
|
+
def _calculate_pressure(self, density: float, temperature_celsius: float) -> float:
|
|
390
|
+
"""
|
|
391
|
+
Calculate pressure from density and temperature using Zhang & Duan (2005) EOS.
|
|
392
|
+
|
|
393
|
+
This is the exact implementation from R DEW.R calculatePressure function.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
density : float
|
|
398
|
+
Density in g/cm³
|
|
399
|
+
temperature_celsius : float
|
|
400
|
+
Temperature in Celsius
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
float
|
|
405
|
+
Pressure in bar
|
|
406
|
+
"""
|
|
407
|
+
# Constants from R DEW.R
|
|
408
|
+
m = 18.01528 # Molar mass of water molecule in g/mol
|
|
409
|
+
ZD05_R = 83.144 # Gas Constant in cm³ bar/mol/K
|
|
410
|
+
ZD05_Vc = 55.9480373 # Critical volume in cm³/mol
|
|
411
|
+
ZD05_Tc = 647.25 # Critical temperature in Kelvin
|
|
412
|
+
|
|
413
|
+
TK = temperature_celsius + 273.15 # Temperature must be converted to Kelvin
|
|
414
|
+
Vr = m / density / ZD05_Vc
|
|
415
|
+
Tr = TK / ZD05_Tc
|
|
416
|
+
|
|
417
|
+
B = 0.349824207 - 2.91046273 / (Tr * Tr) + 2.00914688 / (Tr * Tr * Tr)
|
|
418
|
+
C = 0.112819964 + 0.748997714 / (Tr * Tr) - 0.87320704 / (Tr * Tr * Tr)
|
|
419
|
+
D = 0.0170609505 - 0.0146355822 / (Tr * Tr) + 0.0579768283 / (Tr * Tr * Tr)
|
|
420
|
+
E = -0.000841246372 + 0.00495186474 / (Tr * Tr) - 0.00916248538 / (Tr * Tr * Tr)
|
|
421
|
+
f = -0.100358152 / Tr
|
|
422
|
+
g = -0.00182674744 * Tr
|
|
423
|
+
|
|
424
|
+
delta = (1 + B / Vr + C / (Vr * Vr) + D / (Vr**4) + E / (Vr**5) +
|
|
425
|
+
(f / (Vr * Vr) + g / (Vr**4)) * np.exp(-0.0105999998 / (Vr * Vr)))
|
|
426
|
+
|
|
427
|
+
return ZD05_R * TK * density * delta / m
|
|
428
|
+
|
|
429
|
+
def _calculate_gibbs_of_water(self, pressure: float, temperature_celsius: float) -> float:
|
|
430
|
+
"""
|
|
431
|
+
Calculate Gibbs Free Energy of water using exact R DEW.R implementation.
|
|
432
|
+
|
|
433
|
+
This is the exact translation of the calculateGibbsOfWater function from R DEW.R
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
pressure : float
|
|
438
|
+
Pressure in bar
|
|
439
|
+
temperature_celsius : float
|
|
440
|
+
Temperature in Celsius
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
float
|
|
445
|
+
Gibbs Free Energy in cal/mol
|
|
446
|
+
"""
|
|
447
|
+
# Gibbs Free Energy of water at 1 kb. This equation is a polynomial fit to data as a function of temperature.
|
|
448
|
+
# It is valid in the range of 100 to 1000 C.
|
|
449
|
+
GAtOneKb = (2.6880734E-09 * temperature_celsius**4 + 6.3163061E-07 * temperature_celsius**3 -
|
|
450
|
+
0.019372355 * temperature_celsius**2 - 16.945093 * temperature_celsius - 55769.287)
|
|
451
|
+
|
|
452
|
+
if pressure < 1000: # Simply return zero, this method only works at P >= 1000 bars
|
|
453
|
+
integral = np.nan
|
|
454
|
+
elif pressure == 1000: # Return the value calculated above from the polynomial fit
|
|
455
|
+
integral = 0.0
|
|
456
|
+
elif pressure > 1000: # Integrate from 1 kb to P over the volume
|
|
457
|
+
integral = 0.0
|
|
458
|
+
# Integral is sum of rectangles with this width. This function in effect limits the spacing
|
|
459
|
+
# to 20 bars so that very small pressures do not have unreasonably small widths. Otherwise the width
|
|
460
|
+
# is chosen such that there are always 500 steps in the numerical integration. This ensures that for very
|
|
461
|
+
# high pressures, there are not a huge number of steps calculated which is very computationally taxing.
|
|
462
|
+
spacing = max(20.0, (pressure - 1000.0) / 500.0)
|
|
463
|
+
|
|
464
|
+
# Use numpy arange to exactly match R's seq(1000, pressure, by = spacing) behavior
|
|
465
|
+
# R's seq includes the endpoint, so we need to include pressure in our sequence
|
|
466
|
+
P_values = np.arange(1000.0, pressure + spacing/2, spacing) # +spacing/2 ensures we include endpoint
|
|
467
|
+
|
|
468
|
+
for P_current in P_values:
|
|
469
|
+
# This integral determines the density only down to an error of 100 bars
|
|
470
|
+
# rather than the standard of 0.01. This is done to save computational
|
|
471
|
+
# time. Tests indicate this reduces the computation by about a half while
|
|
472
|
+
# introducing little error from the standard of 0.01.
|
|
473
|
+
rho = self._calculate_density_bisection(P_current, temperature_celsius, error=100.0)
|
|
474
|
+
integral += (18.01528 / rho / 41.84) * spacing
|
|
475
|
+
|
|
476
|
+
return GAtOneKb + integral
|
|
477
|
+
|
|
478
|
+
def _calculate_depsdrho(self, density: float, temperature_celsius: float) -> float:
|
|
479
|
+
"""
|
|
480
|
+
Calculate partial derivative of dielectric constant with respect to density (dε/dρ).
|
|
481
|
+
|
|
482
|
+
This is the exact implementation from R DEW.R calculate_depsdrho function.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
density : float
|
|
487
|
+
Density in g/cm³
|
|
488
|
+
temperature_celsius : float
|
|
489
|
+
Temperature in Celsius
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
float
|
|
494
|
+
dε/dρ in cm³/g
|
|
495
|
+
"""
|
|
496
|
+
# Power Function parameters (same as for epsilon calculation)
|
|
497
|
+
a1 = -0.00157637700752506
|
|
498
|
+
a2 = 0.0681028783422197
|
|
499
|
+
a3 = 0.754875480393944
|
|
500
|
+
b1 = -8.01665106535394E-05
|
|
501
|
+
b2 = -0.0687161761831994
|
|
502
|
+
b3 = 4.74797272182151
|
|
503
|
+
|
|
504
|
+
A = a1 * temperature_celsius + a2 * np.sqrt(temperature_celsius) + a3
|
|
505
|
+
B = b1 * temperature_celsius + b2 * np.sqrt(temperature_celsius) + b3
|
|
506
|
+
|
|
507
|
+
# dε/dρ = A * exp(B) * density^(A-1)
|
|
508
|
+
return A * np.exp(B) * (density ** (A - 1))
|
|
509
|
+
|
|
510
|
+
def _calculate_drhodP(self, density: float, temperature_celsius: float) -> float:
|
|
511
|
+
"""
|
|
512
|
+
Calculate partial derivative of density with respect to pressure (dρ/dP).
|
|
513
|
+
|
|
514
|
+
This is the exact implementation from R DEW.R calculate_drhodP function.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
density : float
|
|
519
|
+
Density in g/cm³
|
|
520
|
+
temperature_celsius : float
|
|
521
|
+
Temperature in Celsius
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
float
|
|
526
|
+
dρ/dP in g/cm³/bar
|
|
527
|
+
"""
|
|
528
|
+
# Constants from R DEW.R
|
|
529
|
+
m = 18.01528 # Molar mass of water molecule in g/mol
|
|
530
|
+
ZD05_R = 83.144 # Gas Constant in cm³ bar/mol/K
|
|
531
|
+
ZD05_Vc = 55.9480373 # Critical volume in cm³/mol
|
|
532
|
+
ZD05_Tc = 647.25 # Critical temperature in Kelvin
|
|
533
|
+
|
|
534
|
+
TK = temperature_celsius + 273.15 # temperature must be converted to Kelvin
|
|
535
|
+
Tr = TK / ZD05_Tc
|
|
536
|
+
cc = ZD05_Vc / m # This term appears frequently in the equation
|
|
537
|
+
Vr = m / (density * ZD05_Vc)
|
|
538
|
+
|
|
539
|
+
B = 0.349824207 - 2.91046273 / (Tr * Tr) + 2.00914688 / (Tr * Tr * Tr)
|
|
540
|
+
C = 0.112819964 + 0.748997714 / (Tr * Tr) - 0.87320704 / (Tr * Tr * Tr)
|
|
541
|
+
D = 0.0170609505 - 0.0146355822 / (Tr * Tr) + 0.0579768283 / (Tr * Tr * Tr)
|
|
542
|
+
E = -0.000841246372 + 0.00495186474 / (Tr * Tr) - 0.00916248538 / (Tr * Tr * Tr)
|
|
543
|
+
f = -0.100358152 / Tr
|
|
544
|
+
g = 0.0105999998 * Tr
|
|
545
|
+
|
|
546
|
+
delta = (1 + B / Vr + C / (Vr**2) + D / (Vr**4) + E / (Vr**5) +
|
|
547
|
+
(f / (Vr**2) + g / (Vr**4)) * np.exp(-0.0105999998 / (Vr**2)))
|
|
548
|
+
|
|
549
|
+
kappa = (B * cc + 2 * C * (cc**2) * density + 4 * D * cc**4 * density**3 + 5 * E * cc**5 * density**4 +
|
|
550
|
+
(2 * f * (cc**2) * density + 4 * g * cc**4 * density**3 -
|
|
551
|
+
(f / (Vr**2) + g / (Vr**4)) * (2 * 0.0105999998 * (cc**2) * density)) *
|
|
552
|
+
np.exp(-0.0105999998 / (Vr**2)))
|
|
553
|
+
|
|
554
|
+
return m / (ZD05_R * TK * (delta + density * kappa))
|
|
555
|
+
|
|
556
|
+
def _calculate_Q(self, density: float, temperature_celsius: float) -> float:
|
|
557
|
+
"""
|
|
558
|
+
Calculate Born Q function using exact R DEW.R implementation.
|
|
559
|
+
|
|
560
|
+
This is the exact implementation from R DEW.R calculateQ function.
|
|
561
|
+
|
|
562
|
+
Parameters
|
|
563
|
+
----------
|
|
564
|
+
density : float
|
|
565
|
+
Density in g/cm³
|
|
566
|
+
temperature_celsius : float
|
|
567
|
+
Temperature in Celsius
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
float
|
|
572
|
+
Q in bar⁻¹
|
|
573
|
+
"""
|
|
574
|
+
epsilon = self._calculate_epsilon_single(density, temperature_celsius)
|
|
575
|
+
depsdrho = self._calculate_depsdrho(density, temperature_celsius)
|
|
576
|
+
drhodP = self._calculate_drhodP(density, temperature_celsius)
|
|
577
|
+
|
|
578
|
+
return depsdrho * drhodP / (epsilon**2)
|
|
579
|
+
|
|
580
|
+
def _calculate_epsilon_single(self, density: float, temperature_celsius: float) -> float:
|
|
581
|
+
"""
|
|
582
|
+
Calculate epsilon for single density and temperature values.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
density : float
|
|
587
|
+
Density in g/cm³
|
|
588
|
+
temperature_celsius : float
|
|
589
|
+
Temperature in Celsius
|
|
590
|
+
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
float
|
|
594
|
+
Dielectric constant
|
|
595
|
+
"""
|
|
596
|
+
# DEW power function parameters (same as in array version)
|
|
597
|
+
a1 = -0.00157637700752506
|
|
598
|
+
a2 = 0.0681028783422197
|
|
599
|
+
a3 = 0.754875480393944
|
|
600
|
+
b1 = -8.01665106535394E-05
|
|
601
|
+
b2 = -0.0687161761831994
|
|
602
|
+
b3 = 4.74797272182151
|
|
603
|
+
|
|
604
|
+
A = a1 * temperature_celsius + a2 * np.sqrt(temperature_celsius) + a3
|
|
605
|
+
B = b1 * temperature_celsius + b2 * np.sqrt(temperature_celsius) + b3
|
|
606
|
+
|
|
607
|
+
return np.exp(B) * (density ** A)
|
|
608
|
+
|
|
609
|
+
def _calculate_dielectric_constant_with_density(self, T: np.ndarray, P: np.ndarray,
|
|
610
|
+
rho_gcm3: np.ndarray, valid: np.ndarray) -> np.ndarray:
|
|
611
|
+
"""
|
|
612
|
+
Calculate dielectric constant using pre-computed density in g/cm³.
|
|
613
|
+
"""
|
|
614
|
+
epsilon = np.full_like(T, np.nan)
|
|
615
|
+
|
|
616
|
+
if np.any(valid):
|
|
617
|
+
T_valid = T[valid]
|
|
618
|
+
P_valid = P[valid]
|
|
619
|
+
rho_valid = rho_gcm3[valid] # Already in g/cm³
|
|
620
|
+
|
|
621
|
+
# Convert temperature to Celsius for DEW correlation
|
|
622
|
+
T_celsius = T_valid - 273.15
|
|
623
|
+
|
|
624
|
+
# DEW power function parameters (from R code)
|
|
625
|
+
a1 = -0.00157637700752506
|
|
626
|
+
a2 = 0.0681028783422197
|
|
627
|
+
a3 = 0.754875480393944
|
|
628
|
+
b1 = -8.01665106535394E-5
|
|
629
|
+
b2 = -0.0687161761831994
|
|
630
|
+
b3 = 4.74797272182151
|
|
631
|
+
|
|
632
|
+
# Calculate A and B
|
|
633
|
+
A = a1 * T_celsius + a2 * np.sqrt(T_celsius) + a3
|
|
634
|
+
B = b1 * T_celsius + b2 * np.sqrt(T_celsius) + b3
|
|
635
|
+
|
|
636
|
+
# DEW dielectric constant: epsilon = exp(B) * density^A
|
|
637
|
+
epsilon_calc = np.exp(B) * (rho_valid ** A)
|
|
638
|
+
|
|
639
|
+
# For low T or P conditions, use SUPCRT92 (AW90) as in R version
|
|
640
|
+
low_condition = (T_celsius < 100.0) | (P_valid < 1000.0)
|
|
641
|
+
|
|
642
|
+
if np.any(low_condition):
|
|
643
|
+
# Use Archer & Wang for low conditions
|
|
644
|
+
from .archer_wang import water_AW90
|
|
645
|
+
|
|
646
|
+
# Convert density to kg/m³ and pressure to MPa
|
|
647
|
+
rho_kg_m3 = rho_valid[low_condition] * 1000.0 # g/cm³ to kg/m³
|
|
648
|
+
P_MPa = P_valid[low_condition] / 10.0 # bar to MPa
|
|
649
|
+
T_low = T_valid[low_condition]
|
|
650
|
+
|
|
651
|
+
epsilon_aw90 = water_AW90(T_low, rho_kg_m3, P_MPa)
|
|
652
|
+
epsilon_calc[low_condition] = epsilon_aw90
|
|
653
|
+
|
|
654
|
+
# Special case: at Pr,Tr use 78.47 as in R code
|
|
655
|
+
prtr_condition = (np.abs(T_celsius - 25.0) < 0.1) & (np.abs(P_valid - 1.0) < 0.1)
|
|
656
|
+
if np.any(prtr_condition):
|
|
657
|
+
epsilon_calc[prtr_condition] = 78.47
|
|
658
|
+
|
|
659
|
+
# Apply bounds to ensure physical values
|
|
660
|
+
epsilon_calc = np.clip(epsilon_calc, 1.0, 200.0)
|
|
661
|
+
|
|
662
|
+
epsilon[valid] = epsilon_calc
|
|
663
|
+
|
|
664
|
+
return epsilon
|
|
665
|
+
|
|
666
|
+
def _calculate_reference_density(self, T: np.ndarray) -> np.ndarray:
|
|
667
|
+
"""Calculate reference density at 1 bar pressure."""
|
|
668
|
+
# Simplified fit to water density at 1 bar
|
|
669
|
+
rho0 = 1.0 - 2.5e-4 * (T - 298.15) - 5e-7 * (T - 298.15)**2
|
|
670
|
+
rho0 = np.maximum(rho0, 0.1) # Ensure positive
|
|
671
|
+
return rho0
|
|
672
|
+
|
|
673
|
+
def _calculate_dielectric_constant(self, T: np.ndarray, P: np.ndarray,
|
|
674
|
+
valid: np.ndarray) -> np.ndarray:
|
|
675
|
+
"""
|
|
676
|
+
Calculate dielectric constant using DEW model.
|
|
677
|
+
|
|
678
|
+
This is the key feature of the DEW model - accurate dielectric constants
|
|
679
|
+
at high P-T conditions based on molecular dynamics simulations.
|
|
680
|
+
"""
|
|
681
|
+
epsilon = np.full_like(T, np.nan)
|
|
682
|
+
|
|
683
|
+
if np.any(valid):
|
|
684
|
+
T_valid = T[valid]
|
|
685
|
+
P_valid = P[valid]
|
|
686
|
+
|
|
687
|
+
# DEW dielectric constant correlation
|
|
688
|
+
# Based on the R CHNOSZ implementation which uses the DEW power function
|
|
689
|
+
# for high P-T conditions and falls back to SUPCRT92 (AW90) for low P-T.
|
|
690
|
+
|
|
691
|
+
from .archer_wang import water_AW90
|
|
692
|
+
|
|
693
|
+
# Calculate density first
|
|
694
|
+
rho_full = self._calculate_density(T, P, valid)
|
|
695
|
+
rho_valid = rho_full[valid] # g/cm³
|
|
696
|
+
|
|
697
|
+
# Convert temperature to Celsius for DEW correlation
|
|
698
|
+
T_celsius = T_valid - 273.15
|
|
699
|
+
|
|
700
|
+
# DEW power function parameters (from R code)
|
|
701
|
+
a1 = -0.00157637700752506
|
|
702
|
+
a2 = 0.0681028783422197
|
|
703
|
+
a3 = 0.754875480393944
|
|
704
|
+
b1 = -8.01665106535394E-5
|
|
705
|
+
b2 = -0.0687161761831994
|
|
706
|
+
b3 = 4.74797272182151
|
|
707
|
+
|
|
708
|
+
# Calculate A and B
|
|
709
|
+
A = a1 * T_celsius + a2 * np.sqrt(T_celsius) + a3
|
|
710
|
+
B = b1 * T_celsius + b2 * np.sqrt(T_celsius) + b3
|
|
711
|
+
|
|
712
|
+
# DEW dielectric constant: epsilon = exp(B) * density^A
|
|
713
|
+
epsilon_calc = np.exp(B) * (rho_valid ** A)
|
|
714
|
+
|
|
715
|
+
# For low T or P conditions, use SUPCRT92 (AW90) as in R version
|
|
716
|
+
low_condition = (T_celsius < 100.0) | (P_valid < 1000.0)
|
|
717
|
+
|
|
718
|
+
if np.any(low_condition):
|
|
719
|
+
# Use Archer & Wang for low conditions
|
|
720
|
+
# Convert density to kg/m³ and pressure to MPa
|
|
721
|
+
rho_kg_m3 = rho_valid[low_condition] * 1000.0 # g/cm³ to kg/m³
|
|
722
|
+
P_MPa = P_valid[low_condition] / 10.0 # bar to MPa
|
|
723
|
+
T_low = T_valid[low_condition]
|
|
724
|
+
|
|
725
|
+
epsilon_aw90 = water_AW90(T_low, rho_kg_m3, P_MPa)
|
|
726
|
+
epsilon_calc[low_condition] = epsilon_aw90
|
|
727
|
+
|
|
728
|
+
# Special case: at Pr,Tr use 78.47 as in R code
|
|
729
|
+
prtr_condition = (np.abs(T_celsius - 25.0) < 0.1) & (np.abs(P_valid - 1.0) < 0.1)
|
|
730
|
+
if np.any(prtr_condition):
|
|
731
|
+
epsilon_calc[prtr_condition] = 78.47
|
|
732
|
+
|
|
733
|
+
# Apply bounds to ensure physical values
|
|
734
|
+
epsilon_calc = np.clip(epsilon_calc, 1.0, 200.0)
|
|
735
|
+
|
|
736
|
+
epsilon[valid] = epsilon_calc
|
|
737
|
+
|
|
738
|
+
return epsilon
|
|
739
|
+
|
|
740
|
+
def _calculate_thermodynamic_properties(self, T: np.ndarray, P: np.ndarray,
|
|
741
|
+
rho: np.ndarray, valid: np.ndarray) -> Dict[str, np.ndarray]:
|
|
742
|
+
"""Calculate thermodynamic properties using DEW model."""
|
|
743
|
+
props = {}
|
|
744
|
+
|
|
745
|
+
for prop in ['G', 'H', 'S', 'Cp', 'Cv', 'U', 'A']:
|
|
746
|
+
props[prop] = np.full_like(T, np.nan)
|
|
747
|
+
|
|
748
|
+
if np.any(valid):
|
|
749
|
+
T_valid = T[valid]
|
|
750
|
+
P_valid = P[valid]
|
|
751
|
+
|
|
752
|
+
# Calculate Gibbs energy using the exact DEW method
|
|
753
|
+
G_results = np.full(len(T_valid), np.nan)
|
|
754
|
+
for i, (T_val, P_val) in enumerate(zip(T_valid, P_valid)):
|
|
755
|
+
T_celsius = T_val - 273.15 # Convert to Celsius
|
|
756
|
+
G_cal_per_mol = self._calculate_gibbs_of_water(P_val, T_celsius) # cal/mol
|
|
757
|
+
G_results[i] = G_cal_per_mol * 4.184 # Convert cal/mol to J/mol
|
|
758
|
+
|
|
759
|
+
props['G'][valid] = G_results
|
|
760
|
+
|
|
761
|
+
# For other properties, use simplified approximations (these are not as critical for DEW)
|
|
762
|
+
if any(prop in ['H', 'S', 'Cp', 'Cv', 'U', 'A'] for prop in props.keys()):
|
|
763
|
+
rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
|
|
764
|
+
|
|
765
|
+
# Reference state properties (liquid water at 25°C, 1 bar)
|
|
766
|
+
H_ref = -285830.0 # J/mol
|
|
767
|
+
S_ref = 69.95 # J/(mol·K)
|
|
768
|
+
Cp_ref = 75.31 # J/(mol·K)
|
|
769
|
+
|
|
770
|
+
# Temperature effects
|
|
771
|
+
dT = T_valid - 298.15
|
|
772
|
+
|
|
773
|
+
# Heat capacity (empirical fit for high T-P)
|
|
774
|
+
Cp = Cp_ref + 0.15 * dT - 2e-4 * dT**2 + 1e-7 * dT**3
|
|
775
|
+
|
|
776
|
+
# Pressure effects on heat capacity
|
|
777
|
+
Cp += 1e-5 * P_valid # Small pressure dependence
|
|
778
|
+
|
|
779
|
+
# Entropy (integrate Cp/T)
|
|
780
|
+
S = S_ref + Cp_ref * np.log(T_valid / 298.15) + 0.15 * dT - 1e-4 * dT**2 + (1e-7/2) * dT**3
|
|
781
|
+
|
|
782
|
+
# Enthalpy (integrate Cp)
|
|
783
|
+
H = H_ref + Cp_ref * dT + 0.075 * dT**2 - (2e-4/3) * dT**3 + (1e-7/4) * dT**4
|
|
784
|
+
|
|
785
|
+
# Pressure effects on enthalpy (∫V dP)
|
|
786
|
+
V_molar = self.MW_H2O / (rho_valid / 1000.0) # cm³/mol (convert kg/m³ to g/cm³)
|
|
787
|
+
H += V_molar * (P_valid - 1.0) * 0.01 # Convert bar·cm³/mol to J/mol
|
|
788
|
+
|
|
789
|
+
# Other properties
|
|
790
|
+
Cv = Cp - self.R # Simplified relation
|
|
791
|
+
U = H - P_valid * V_molar * 0.01 # Internal energy
|
|
792
|
+
A = U - T_valid * S # Helmholtz energy
|
|
793
|
+
|
|
794
|
+
# Store results
|
|
795
|
+
props['H'][valid] = H
|
|
796
|
+
props['S'][valid] = S
|
|
797
|
+
props['Cp'][valid] = Cp
|
|
798
|
+
props['Cv'][valid] = Cv
|
|
799
|
+
props['U'][valid] = U
|
|
800
|
+
props['A'][valid] = A
|
|
801
|
+
|
|
802
|
+
return props
|
|
803
|
+
|
|
804
|
+
def _calculate_mechanical_properties(self, T: np.ndarray, P: np.ndarray,
|
|
805
|
+
rho: np.ndarray, valid: np.ndarray) -> Dict[str, np.ndarray]:
|
|
806
|
+
"""Calculate mechanical properties for high P-T conditions."""
|
|
807
|
+
props = {}
|
|
808
|
+
|
|
809
|
+
for prop in ['alpha', 'beta', 'kT', 'E']:
|
|
810
|
+
props[prop] = np.full_like(T, np.nan)
|
|
811
|
+
|
|
812
|
+
if np.any(valid):
|
|
813
|
+
T_valid = T[valid]
|
|
814
|
+
P_valid = P[valid]
|
|
815
|
+
rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
|
|
816
|
+
V_valid = self.MW_H2O / rho_valid # cm³/mol
|
|
817
|
+
|
|
818
|
+
# Thermal expansion coefficient (modified for high P-T)
|
|
819
|
+
alpha = (2.14e-4 + 1e-6 * (T_valid - 298.15) - 2e-8 * P_valid)
|
|
820
|
+
alpha = np.maximum(alpha, 1e-6) # Ensure positive
|
|
821
|
+
|
|
822
|
+
# Isothermal compressibility (decreases with pressure)
|
|
823
|
+
beta = 4.5e-5 * np.exp(-P_valid / 10000.0) * (298.15 / T_valid)**0.5
|
|
824
|
+
beta = np.maximum(beta, 1e-7) # Ensure positive
|
|
825
|
+
|
|
826
|
+
# Derived properties
|
|
827
|
+
kT = V_valid * beta # bar·cm³/mol
|
|
828
|
+
E = V_valid * alpha # cm³/(mol·K)
|
|
829
|
+
|
|
830
|
+
props['alpha'][valid] = alpha
|
|
831
|
+
props['beta'][valid] = beta
|
|
832
|
+
props['kT'][valid] = kT
|
|
833
|
+
props['E'][valid] = E
|
|
834
|
+
|
|
835
|
+
return props
|
|
836
|
+
|
|
837
|
+
def _calculate_born_functions(self, T: np.ndarray, P: np.ndarray, rho: np.ndarray,
|
|
838
|
+
valid: np.ndarray) -> Dict[str, np.ndarray]:
|
|
839
|
+
"""Calculate Born functions using exact DEW model."""
|
|
840
|
+
props = {}
|
|
841
|
+
|
|
842
|
+
for prop in ['QBorn', 'YBorn', 'XBorn', 'ZBorn']:
|
|
843
|
+
props[prop] = np.full_like(T, np.nan)
|
|
844
|
+
|
|
845
|
+
if np.any(valid):
|
|
846
|
+
T_valid = T[valid]
|
|
847
|
+
P_valid = P[valid]
|
|
848
|
+
|
|
849
|
+
# Calculate QBorn using the exact DEW calculateQ function
|
|
850
|
+
# This requires density in g/cm³, not kg/m³
|
|
851
|
+
if rho is not None:
|
|
852
|
+
rho_gcm3_valid = rho[valid] / 1000.0 # Convert kg/m³ to g/cm³
|
|
853
|
+
else:
|
|
854
|
+
rho_gcm3_full = self._calculate_density(T, P, valid)
|
|
855
|
+
rho_gcm3_valid = rho_gcm3_full[valid]
|
|
856
|
+
|
|
857
|
+
QBorn_results = np.full(len(T_valid), np.nan)
|
|
858
|
+
epsilon_results = np.full(len(T_valid), np.nan)
|
|
859
|
+
|
|
860
|
+
for i, (T_val, P_val, rho_val) in enumerate(zip(T_valid, P_valid, rho_gcm3_valid)):
|
|
861
|
+
T_celsius = T_val - 273.15 # Convert to Celsius
|
|
862
|
+
# Use exact DEW Q calculation
|
|
863
|
+
QBorn_results[i] = self._calculate_Q(rho_val, T_celsius)
|
|
864
|
+
epsilon_results[i] = self._calculate_epsilon_single(rho_val, T_celsius)
|
|
865
|
+
|
|
866
|
+
# For other Born functions, use simplified relations (as in R water.R)
|
|
867
|
+
# Get mechanical properties for thermal expansion
|
|
868
|
+
mech_props = self._calculate_mechanical_properties(T, P, rho, valid)
|
|
869
|
+
alpha_valid = mech_props['alpha'][valid]
|
|
870
|
+
|
|
871
|
+
# Born functions
|
|
872
|
+
YBorn = alpha_valid / epsilon_results # 1/K
|
|
873
|
+
XBorn = QBorn_results / epsilon_results # 1/(bar·K) - note: this uses QBorn, not beta
|
|
874
|
+
ZBorn = -1.0 / epsilon_results
|
|
875
|
+
|
|
876
|
+
props['QBorn'][valid] = QBorn_results
|
|
877
|
+
props['YBorn'][valid] = YBorn
|
|
878
|
+
props['XBorn'][valid] = XBorn
|
|
879
|
+
props['ZBorn'][valid] = ZBorn
|
|
880
|
+
|
|
881
|
+
return props
|
|
882
|
+
|
|
883
|
+
def _calculate_debye_huckel(self, T: np.ndarray, P: np.ndarray, rho: np.ndarray,
|
|
884
|
+
valid: np.ndarray) -> Dict[str, np.ndarray]:
|
|
885
|
+
"""Calculate Debye-Hückel parameters using DEW properties."""
|
|
886
|
+
props = {}
|
|
887
|
+
|
|
888
|
+
for prop in ['A_DH', 'B_DH']:
|
|
889
|
+
props[prop] = np.full_like(T, np.nan)
|
|
890
|
+
|
|
891
|
+
if np.any(valid):
|
|
892
|
+
T_valid = T[valid]
|
|
893
|
+
rho_valid = rho[valid] if rho is not None else self._calculate_density(T, P, valid)[valid]
|
|
894
|
+
epsilon = self._calculate_dielectric_constant(T, P, valid)[valid]
|
|
895
|
+
|
|
896
|
+
# Debye-Hückel parameters using DEW dielectric constants
|
|
897
|
+
A_DH = 1.8246e6 * rho_valid**0.5 / (epsilon * T_valid)**1.5
|
|
898
|
+
B_DH = 50.29e8 * rho_valid**0.5 / (epsilon * T_valid)**0.5
|
|
899
|
+
|
|
900
|
+
props['A_DH'][valid] = A_DH
|
|
901
|
+
props['B_DH'][valid] = B_DH
|
|
902
|
+
|
|
903
|
+
return props
|
|
904
|
+
|
|
905
|
+
def _calculate_transport_property(self, prop: str, T: np.ndarray, P: np.ndarray,
|
|
906
|
+
rho: np.ndarray, valid: np.ndarray) -> np.ndarray:
|
|
907
|
+
"""Calculate transport properties (simplified for high P-T)."""
|
|
908
|
+
result = np.full_like(T, np.nan)
|
|
909
|
+
|
|
910
|
+
if np.any(valid):
|
|
911
|
+
T_valid = T[valid]
|
|
912
|
+
P_valid = P[valid]
|
|
913
|
+
|
|
914
|
+
if prop == 'Speed':
|
|
915
|
+
# Speed of sound (increases with pressure)
|
|
916
|
+
result[valid] = (1402.7 + 5.0 * (T_valid - 298.15) +
|
|
917
|
+
0.5 * np.sqrt(P_valid))
|
|
918
|
+
|
|
919
|
+
elif prop == 'visc':
|
|
920
|
+
# Viscosity (empirical fit for high P-T)
|
|
921
|
+
result[valid] = (1e-3 * np.exp(-3.0 + 1000.0 / T_valid) *
|
|
922
|
+
(1 + P_valid / 5000.0)**0.1)
|
|
923
|
+
|
|
924
|
+
elif prop == 'tcond':
|
|
925
|
+
# Thermal conductivity (increases with pressure and temperature)
|
|
926
|
+
result[valid] = (0.6 + 0.002 * (T_valid - 298.15) +
|
|
927
|
+
0.00005 * P_valid)
|
|
928
|
+
|
|
929
|
+
return result
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
# Create global instance
|
|
933
|
+
dew_water = DEWWater()
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def water_DEW(properties: Union[str, List[str]],
|
|
937
|
+
T: Union[float, np.ndarray] = 298.15,
|
|
938
|
+
P: Union[float, np.ndarray, str] = 1.0,
|
|
939
|
+
**kwargs) -> Union[float, np.ndarray, Dict[str, Any]]:
|
|
940
|
+
"""
|
|
941
|
+
Calculate water properties using DEW model.
|
|
942
|
+
|
|
943
|
+
Parameters
|
|
944
|
+
----------
|
|
945
|
+
properties : str or list of str
|
|
946
|
+
Property or properties to calculate
|
|
947
|
+
T : float or array
|
|
948
|
+
Temperature in Kelvin
|
|
949
|
+
P : float, array, or 'Psat'
|
|
950
|
+
Pressure in bar, or 'Psat' for saturation pressure
|
|
951
|
+
**kwargs
|
|
952
|
+
Additional options
|
|
953
|
+
|
|
954
|
+
Returns
|
|
955
|
+
-------
|
|
956
|
+
float, array, or dict
|
|
957
|
+
Calculated water properties
|
|
958
|
+
|
|
959
|
+
Examples
|
|
960
|
+
--------
|
|
961
|
+
>>> # High pressure conditions
|
|
962
|
+
>>> epsilon = water_DEW('epsilon', T=873.15, P=10000) # 600°C, 10 kbar
|
|
963
|
+
>>>
|
|
964
|
+
>>> # Multiple properties at extreme conditions
|
|
965
|
+
>>> props = water_DEW(['rho', 'epsilon'], T=1073.15, P=30000) # 800°C, 30 kbar
|
|
966
|
+
>>>
|
|
967
|
+
>>> # Born functions for electrolyte calculations
|
|
968
|
+
>>> born = water_DEW(['QBorn', 'YBorn'], T=773.15, P=5000)
|
|
969
|
+
"""
|
|
970
|
+
return dew_water.calculate(properties, T, P, **kwargs)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
if __name__ == "__main__":
|
|
974
|
+
# Quick test
|
|
975
|
+
print("DEW Water Model Test")
|
|
976
|
+
print("=" * 25)
|
|
977
|
+
|
|
978
|
+
# Test extreme conditions
|
|
979
|
+
T_test = 873.15 # 600°C
|
|
980
|
+
P_test = 10000.0 # 10 kbar
|
|
981
|
+
|
|
982
|
+
rho = water_DEW('rho', T=T_test, P=P_test)
|
|
983
|
+
epsilon = water_DEW('epsilon', T=T_test, P=P_test)
|
|
984
|
+
|
|
985
|
+
print(f"Water at {T_test} K, {P_test} bar:")
|
|
986
|
+
print(f" Density: {rho:.3f} g/cm³")
|
|
987
|
+
print(f" Dielectric constant: {epsilon:.1f}")
|
|
988
|
+
|
|
989
|
+
# Test Born functions
|
|
990
|
+
born = water_DEW(['QBorn', 'YBorn'], T=T_test, P=P_test)
|
|
991
|
+
print(f"Born functions: Q={born['QBorn']:.2e}, Y={born['YBorn']:.2e}")
|
|
992
|
+
|
|
993
|
+
# Compare with standard conditions
|
|
994
|
+
T_std = 298.15
|
|
995
|
+
P_std = 1.0
|
|
996
|
+
epsilon_std = water_DEW('epsilon', T=T_std, P=P_std)
|
|
997
|
+
print(f"Standard conditions (25°C, 1 bar): ε = {epsilon_std:.1f}")
|