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
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}")