pychnosz 1.1.1__cp311-cp311-macosx_10_13_x86_64.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 (131) hide show
  1. pychnosz/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. pychnosz/.dylibs/libgfortran.5.dylib +0 -0
  3. pychnosz/.dylibs/libquadmath.0.dylib +0 -0
  4. pychnosz/__init__.py +129 -0
  5. pychnosz/biomolecules/__init__.py +29 -0
  6. pychnosz/biomolecules/ionize_aa.py +197 -0
  7. pychnosz/biomolecules/proteins.py +595 -0
  8. pychnosz/core/__init__.py +46 -0
  9. pychnosz/core/affinity.py +1256 -0
  10. pychnosz/core/animation.py +593 -0
  11. pychnosz/core/balance.py +334 -0
  12. pychnosz/core/basis.py +716 -0
  13. pychnosz/core/diagram.py +3336 -0
  14. pychnosz/core/equilibrate.py +813 -0
  15. pychnosz/core/equilibrium.py +554 -0
  16. pychnosz/core/info.py +821 -0
  17. pychnosz/core/retrieve.py +364 -0
  18. pychnosz/core/speciation.py +580 -0
  19. pychnosz/core/species.py +599 -0
  20. pychnosz/core/subcrt.py +1700 -0
  21. pychnosz/core/thermo.py +593 -0
  22. pychnosz/core/unicurve.py +1226 -0
  23. pychnosz/data/__init__.py +11 -0
  24. pychnosz/data/add_obigt.py +327 -0
  25. pychnosz/data/extdata/Berman/BDat17_2017.csv +2 -0
  26. pychnosz/data/extdata/Berman/Ber88_1988.csv +68 -0
  27. pychnosz/data/extdata/Berman/Ber90_1990.csv +5 -0
  28. pychnosz/data/extdata/Berman/DS10_2010.csv +6 -0
  29. pychnosz/data/extdata/Berman/FDM+14_2014.csv +2 -0
  30. pychnosz/data/extdata/Berman/Got04_2004.csv +5 -0
  31. pychnosz/data/extdata/Berman/JUN92_1992.csv +3 -0
  32. pychnosz/data/extdata/Berman/SHD91_1991.csv +12 -0
  33. pychnosz/data/extdata/Berman/VGT92_1992.csv +2 -0
  34. pychnosz/data/extdata/Berman/VPT01_2001.csv +3 -0
  35. pychnosz/data/extdata/Berman/VPV05_2005.csv +2 -0
  36. pychnosz/data/extdata/Berman/ZS92_1992.csv +11 -0
  37. pychnosz/data/extdata/Berman/sympy.R +99 -0
  38. pychnosz/data/extdata/Berman/testing/BA96.bib +12 -0
  39. pychnosz/data/extdata/Berman/testing/BA96_Berman.csv +21 -0
  40. pychnosz/data/extdata/Berman/testing/BA96_OBIGT.csv +21 -0
  41. pychnosz/data/extdata/Berman/testing/BA96_refs.csv +6 -0
  42. pychnosz/data/extdata/OBIGT/AD.csv +25 -0
  43. pychnosz/data/extdata/OBIGT/Berman_cr.csv +93 -0
  44. pychnosz/data/extdata/OBIGT/DEW.csv +211 -0
  45. pychnosz/data/extdata/OBIGT/H2O_aq.csv +4 -0
  46. pychnosz/data/extdata/OBIGT/SLOP98.csv +411 -0
  47. pychnosz/data/extdata/OBIGT/SUPCRT92.csv +178 -0
  48. pychnosz/data/extdata/OBIGT/inorganic_aq.csv +729 -0
  49. pychnosz/data/extdata/OBIGT/inorganic_cr.csv +273 -0
  50. pychnosz/data/extdata/OBIGT/inorganic_gas.csv +20 -0
  51. pychnosz/data/extdata/OBIGT/organic_aq.csv +1104 -0
  52. pychnosz/data/extdata/OBIGT/organic_cr.csv +481 -0
  53. pychnosz/data/extdata/OBIGT/organic_gas.csv +268 -0
  54. pychnosz/data/extdata/OBIGT/organic_liq.csv +533 -0
  55. pychnosz/data/extdata/OBIGT/testing/GEMSFIT.csv +43 -0
  56. pychnosz/data/extdata/OBIGT/testing/IGEM.csv +17 -0
  57. pychnosz/data/extdata/OBIGT/testing/Sandia.csv +8 -0
  58. pychnosz/data/extdata/OBIGT/testing/SiO2.csv +4 -0
  59. pychnosz/data/extdata/misc/AD03_Fig1a.csv +69 -0
  60. pychnosz/data/extdata/misc/AD03_Fig1b.csv +43 -0
  61. pychnosz/data/extdata/misc/AD03_Fig1c.csv +89 -0
  62. pychnosz/data/extdata/misc/AD03_Fig1d.csv +30 -0
  63. pychnosz/data/extdata/misc/BZA10.csv +5 -0
  64. pychnosz/data/extdata/misc/HW97_Cp.csv +90 -0
  65. pychnosz/data/extdata/misc/HWM96_V.csv +229 -0
  66. pychnosz/data/extdata/misc/LA19_test.csv +7 -0
  67. pychnosz/data/extdata/misc/Mer75_Table4.csv +42 -0
  68. pychnosz/data/extdata/misc/OBIGT_check.csv +423 -0
  69. pychnosz/data/extdata/misc/PM90.csv +7 -0
  70. pychnosz/data/extdata/misc/RH95.csv +23 -0
  71. pychnosz/data/extdata/misc/RH98_Table15.csv +17 -0
  72. pychnosz/data/extdata/misc/SC10_Rainbow.csv +19 -0
  73. pychnosz/data/extdata/misc/SK95.csv +55 -0
  74. pychnosz/data/extdata/misc/SOJSH.csv +61 -0
  75. pychnosz/data/extdata/misc/SS98_Fig5a.csv +81 -0
  76. pychnosz/data/extdata/misc/SS98_Fig5b.csv +84 -0
  77. pychnosz/data/extdata/misc/TKSS14_Fig2.csv +25 -0
  78. pychnosz/data/extdata/misc/bluered.txt +1000 -0
  79. pychnosz/data/extdata/protein/Cas/Cas_aa.csv +177 -0
  80. pychnosz/data/extdata/protein/Cas/Cas_uniprot.csv +186 -0
  81. pychnosz/data/extdata/protein/Cas/download.R +34 -0
  82. pychnosz/data/extdata/protein/Cas/mkaa.R +34 -0
  83. pychnosz/data/extdata/protein/POLG.csv +12 -0
  84. pychnosz/data/extdata/protein/TBD+05.csv +393 -0
  85. pychnosz/data/extdata/protein/TBD+05_aa.csv +393 -0
  86. pychnosz/data/extdata/protein/rubisco.csv +28 -0
  87. pychnosz/data/extdata/protein/rubisco.fasta +239 -0
  88. pychnosz/data/extdata/protein/rubisco_aa.csv +28 -0
  89. pychnosz/data/extdata/src/H2O92D.f.orig +3457 -0
  90. pychnosz/data/extdata/src/README.txt +5 -0
  91. pychnosz/data/extdata/taxonomy/names.dmp +215 -0
  92. pychnosz/data/extdata/taxonomy/nodes.dmp +63 -0
  93. pychnosz/data/extdata/thermo/Bdot_acirc.csv +60 -0
  94. pychnosz/data/extdata/thermo/buffer.csv +40 -0
  95. pychnosz/data/extdata/thermo/element.csv +135 -0
  96. pychnosz/data/extdata/thermo/groups.csv +6 -0
  97. pychnosz/data/extdata/thermo/opt.csv +2 -0
  98. pychnosz/data/extdata/thermo/protein.csv +506 -0
  99. pychnosz/data/extdata/thermo/refs.csv +343 -0
  100. pychnosz/data/extdata/thermo/stoich.csv.xz +0 -0
  101. pychnosz/data/loader.py +431 -0
  102. pychnosz/data/mod_obigt.py +322 -0
  103. pychnosz/data/obigt.py +471 -0
  104. pychnosz/data/worm.py +228 -0
  105. pychnosz/fortran/__init__.py +16 -0
  106. pychnosz/fortran/h2o92.dylib +0 -0
  107. pychnosz/fortran/h2o92_interface.py +527 -0
  108. pychnosz/geochemistry/__init__.py +21 -0
  109. pychnosz/geochemistry/minerals.py +514 -0
  110. pychnosz/geochemistry/redox.py +500 -0
  111. pychnosz/models/__init__.py +47 -0
  112. pychnosz/models/archer_wang.py +165 -0
  113. pychnosz/models/berman.py +309 -0
  114. pychnosz/models/cgl.py +381 -0
  115. pychnosz/models/dew.py +997 -0
  116. pychnosz/models/hkf.py +523 -0
  117. pychnosz/models/hkf_helpers.py +222 -0
  118. pychnosz/models/iapws95.py +1113 -0
  119. pychnosz/models/supcrt92_fortran.py +238 -0
  120. pychnosz/models/water.py +480 -0
  121. pychnosz/utils/__init__.py +27 -0
  122. pychnosz/utils/expression.py +1074 -0
  123. pychnosz/utils/formula.py +830 -0
  124. pychnosz/utils/formula_ox.py +227 -0
  125. pychnosz/utils/reset.py +33 -0
  126. pychnosz/utils/units.py +259 -0
  127. pychnosz-1.1.1.dist-info/METADATA +197 -0
  128. pychnosz-1.1.1.dist-info/RECORD +131 -0
  129. pychnosz-1.1.1.dist-info/WHEEL +5 -0
  130. pychnosz-1.1.1.dist-info/licenses/LICENSE.txt +19 -0
  131. pychnosz-1.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1113 @@
1
+ """
2
+ IAPWS-95 water model implementation.
3
+
4
+ This module implements the IAPWS-95 formulation for the thermodynamic properties
5
+ of ordinary water substance for general and scientific use. This is the international
6
+ standard for water properties.
7
+
8
+ This implementation exactly matches the R CHNOSZ package, with identical coefficients
9
+ and derivative calculations. No shortcuts or approximations - full fidelity to Wagner & Pruss (2002).
10
+
11
+ References:
12
+ - Wagner, W., & Pruß, A. (2002). The IAPWS formulation 1995 for the thermodynamic
13
+ properties of ordinary water substance for general and scientific use.
14
+ Journal of Physical and Chemical Reference Data, 31(2), 387-535.
15
+ - Fernández, D. P., et al. (1997). A formulation for the static permittivity of
16
+ water and steam at temperatures from 238 K to 873 K at pressures up to 1200 MPa.
17
+ Journal of Physical and Chemical Reference Data, 26(4), 1125-1166.
18
+ - R CHNOSZ package IAPWS95.R implementation
19
+ """
20
+
21
+ import numpy as np
22
+ from typing import Union, List, Optional, Dict, Any
23
+ import warnings
24
+ from scipy.optimize import brentq
25
+
26
+
27
+ class AccurateIAPWS95Water:
28
+ """
29
+ Accurate IAPWS-95 water model implementation matching R CHNOSZ exactly.
30
+
31
+ This class provides thermodynamic properties of water using the IAPWS-95
32
+ formulation with exact coefficients and derivative calculations from the
33
+ Wagner & Pruss (2002) specification as implemented in R CHNOSZ.
34
+ """
35
+
36
+ def __init__(self):
37
+ """Initialize IAPWS95 water model with exact constants."""
38
+ # Physical constants (exactly matching R CHNOSZ)
39
+ self.R = 0.46151805 # kJ/(kg·K) - Specific gas constant for water
40
+ self.MW = 18.015268 # g/mol - Molecular weight
41
+
42
+ # Critical constants (exactly matching R CHNOSZ)
43
+ self.Tc = 647.096 # K - Critical temperature
44
+ self.rhoc = 322.0 # kg/m³ - Critical density
45
+
46
+ # Triple point constants
47
+ self.Tt = 273.16 # K - Triple point temperature
48
+
49
+ # Initialize coefficients exactly as in R CHNOSZ
50
+ self._init_coefficients()
51
+
52
+ def _init_coefficients(self):
53
+ """Initialize coefficients for IAPWS-95 fundamental equation (exact R match)."""
54
+ # Ideal gas coefficients (Table 6.1 Wagner & Pruss 2002, R CHNOSZ lines 114-117)
55
+ self.n_ideal = np.array([
56
+ -8.32044648201, 6.6832105268, 3.00632, 0.012436,
57
+ 0.97315, 1.27950, 0.96956, 0.24873
58
+ ])
59
+
60
+ self.gamma_ideal = np.array([
61
+ np.nan, np.nan, np.nan, 1.28728967,
62
+ 3.53734222, 7.74073708, 9.24437796, 27.5075105
63
+ ])
64
+
65
+ # Residual part coefficients (Table 6.2 Wagner & Pruss 2002, R CHNOSZ lines 134-171)
66
+ # c coefficients
67
+ c_list = [np.nan]*7 + [1]*15 + [2]*20 + [3]*4 + [4] + [6]*4 + [np.nan]*5
68
+ self.c_res = np.array(c_list)
69
+
70
+ # d coefficients
71
+ self.d_res = np.array([
72
+ 1,1,1,2,2,3,4,1,1,1,2,2,3,4,
73
+ 4,5,7,9,10,11,13,15,1,2,2,2,3,4,
74
+ 4,4,5,6,6,7,9,9,9,9,9,10,10,12,
75
+ 3,4,4,5,14,3,6,6,6,3,3,3,np.nan,np.nan
76
+ ])
77
+
78
+ # t coefficients
79
+ self.t_res = np.array([
80
+ -0.5,0.875,1,0.5,0.75,0.375,1,4,6,12,1,5,4,2,
81
+ 13,9,3,4,11,4,13,1,7,1,9,10,10,3,
82
+ 7,10,10,6,10,10,1,2,3,4,8,6,9,8,
83
+ 16,22,23,23,10,50,44,46,50,0,1,4,np.nan,np.nan
84
+ ])
85
+
86
+ # n coefficients (exact values from R CHNOSZ)
87
+ self.n_res = np.array([
88
+ 0.12533547935523E-1, 0.78957634722828E1 ,-0.87803203303561E1 ,
89
+ 0.31802509345418 ,-0.26145533859358 ,-0.78199751687981E-2,
90
+ 0.88089493102134E-2,-0.66856572307965 , 0.20433810950965 ,
91
+ -0.66212605039687E-4,-0.19232721156002 ,-0.25709043003438 ,
92
+ 0.16074868486251 ,-0.40092828925807E-1, 0.39343422603254E-6,
93
+ -0.75941377088144E-5, 0.56250979351888E-3,-0.15608652257135E-4,
94
+ 0.11537996422951E-8, 0.36582165144204E-6,-0.13251180074668E-11,
95
+ -0.62639586912454E-9,-0.10793600908932 , 0.17611491008752E-1,
96
+ 0.22132295167546 ,-0.40247669763528 , 0.58083399985759 ,
97
+ 0.49969146990806E-2,-0.31358700712549E-1,-0.74315929710341 ,
98
+ 0.47807329915480 , 0.20527940895948E-1,-0.13636435110343 ,
99
+ 0.14180634400617E-1, 0.83326504880713E-2,-0.29052336009585E-1,
100
+ 0.38615085574206E-1,-0.20393486513704E-1,-0.16554050063734E-2,
101
+ 0.19955571979541E-2, 0.15870308324157E-3,-0.16388568342530E-4,
102
+ 0.43613615723811E-1, 0.34994005463765E-1,-0.76788197844621E-1,
103
+ 0.22446277332006E-1,-0.62689710414685E-4,-0.55711118565645E-9,
104
+ -0.19905718354408 , 0.31777497330738 ,-0.11841182425981 ,
105
+ -0.31306260323435E2 , 0.31546140237781E2 ,-0.25213154341695E4 ,
106
+ -0.14874640856724 , 0.31806110878444
107
+ ])
108
+
109
+ # Additional coefficients for complex terms (R CHNOSZ lines 162-171)
110
+ alpha_list = [np.nan]*51 + [20,20,20,np.nan,np.nan]
111
+ self.alpha_res = np.array(alpha_list)
112
+
113
+ beta_list = [np.nan]*51 + [150,150,250,0.3,0.3]
114
+ self.beta_res = np.array(beta_list)
115
+
116
+ gamma_list = [np.nan]*51 + [1.21,1.21,1.25,np.nan,np.nan]
117
+ self.gamma_res = np.array(gamma_list)
118
+
119
+ epsilon_list = [np.nan]*51 + [1,1,1,np.nan,np.nan]
120
+ self.epsilon_res = np.array(epsilon_list)
121
+
122
+ a_list = [np.nan]*54 + [3.5,3.5]
123
+ self.a_res = np.array(a_list)
124
+
125
+ b_list = [np.nan]*54 + [0.85,0.95]
126
+ self.b_res = np.array(b_list)
127
+
128
+ B_list = [np.nan]*54 + [0.2,0.2]
129
+ self.B_res = np.array(B_list)
130
+
131
+ C_list = [np.nan]*54 + [28,32]
132
+ self.C_res = np.array(C_list)
133
+
134
+ D_list = [np.nan]*54 + [700,800]
135
+ self.D_res = np.array(D_list)
136
+
137
+ A_list = [np.nan]*54 + [0.32,0.32]
138
+ self.A_res = np.array(A_list)
139
+
140
+ # Index ranges (from R CHNOSZ Table 6.5)
141
+ self.i1 = np.arange(0, 7) # 1:7 in R (0-based in Python)
142
+ self.i2 = np.arange(7, 51) # 8:51 in R
143
+ self.i3 = np.arange(51, 54) # 52:54 in R
144
+ self.i4 = np.arange(54, 56) # 55:56 in R
145
+
146
+ def _phi_ideal(self, delta: float, tau: float, derivative: str = 'phi') -> float:
147
+ """
148
+ Calculate ideal gas part of dimensionless Helmholtz energy and derivatives.
149
+
150
+ Exact implementation matching R CHNOSZ IAPWS95.idealgas function.
151
+ """
152
+ if derivative == 'phi':
153
+ # Equation 6.5 from Wagner & Pruss 2002
154
+ result = (np.log(delta) + self.n_ideal[0] + self.n_ideal[1]*tau +
155
+ self.n_ideal[2]*np.log(tau))
156
+
157
+ # Sum term with exponentials
158
+ for i in range(3, 8): # n[4:8] in R (indices 3:7 in Python)
159
+ gamma_val = self.gamma_ideal[i]
160
+ if not np.isnan(gamma_val):
161
+ result += self.n_ideal[i] * np.log(1 - np.exp(-gamma_val*tau))
162
+
163
+ return result
164
+
165
+ elif derivative == 'phi.delta':
166
+ return 1.0/delta
167
+
168
+ elif derivative == 'phi.delta.delta':
169
+ return -1.0/(delta**2)
170
+
171
+ elif derivative == 'phi.tau':
172
+ result = self.n_ideal[1] + self.n_ideal[2]/tau
173
+
174
+ # Sum term with exponentials and gamma
175
+ for i in range(3, 8):
176
+ gamma_val = self.gamma_ideal[i]
177
+ if not np.isnan(gamma_val):
178
+ exp_term = np.exp(-gamma_val*tau)
179
+ result += self.n_ideal[i] * gamma_val * ((1-exp_term)**(-1) - 1)
180
+
181
+ return result
182
+
183
+ elif derivative == 'phi.tau.tau':
184
+ result = -self.n_ideal[2]/(tau**2)
185
+
186
+ # Sum term with exponentials
187
+ for i in range(3, 8):
188
+ gamma_val = self.gamma_ideal[i]
189
+ if not np.isnan(gamma_val):
190
+ exp_term = np.exp(-gamma_val*tau)
191
+ result -= (self.n_ideal[i] * gamma_val**2 * exp_term *
192
+ (1-exp_term)**(-2))
193
+
194
+ return result
195
+
196
+ elif derivative == 'phi.delta.tau':
197
+ return 0.0
198
+
199
+ elif derivative == 'phi.tau.tau.tau':
200
+ # Third derivative with respect to tau
201
+ result = 2*self.n_ideal[2]/(tau**3)
202
+
203
+ # Sum term with exponentials
204
+ for i in range(3, 8):
205
+ gamma_val = self.gamma_ideal[i]
206
+ if not np.isnan(gamma_val):
207
+ exp_term = np.exp(-gamma_val*tau)
208
+ result += (self.n_ideal[i] * gamma_val**3 * exp_term *
209
+ (1-exp_term)**(-3) * (2*exp_term - 1))
210
+
211
+ return result
212
+
213
+ elif derivative == 'phi.delta.delta.delta':
214
+ # Third derivative with respect to delta
215
+ return 2.0/(delta**3)
216
+
217
+ elif derivative == 'phi.delta.tau.tau':
218
+ # Mixed derivative: d³φ⁰/dδdτ²
219
+ return 0.0
220
+
221
+ elif derivative == 'phi.delta.delta.tau':
222
+ # Mixed derivative: d³φ⁰/dδ²dτ
223
+ return 0.0
224
+
225
+ else:
226
+ raise ValueError(f"Unknown derivative: {derivative}")
227
+
228
+ def _delta_function(self, i: int, delta: float, tau: float) -> float:
229
+ """Delta function for complex terms (R CHNOSZ Delta function)."""
230
+ theta = self._theta_function(i, delta, tau)
231
+ B_val = self.B_res[i]
232
+ a_val = self.a_res[i]
233
+ return theta**2 + B_val * ((delta-1)**2)**a_val
234
+
235
+ def _theta_function(self, i: int, delta: float, tau: float) -> float:
236
+ """Theta function for complex terms (R CHNOSZ Theta function)."""
237
+ A_val = self.A_res[i]
238
+ beta_val = self.beta_res[i]
239
+ return (1-tau) + A_val * ((delta-1)**2)**(1/(2*beta_val))
240
+
241
+ def _psi_function(self, i: int, delta: float, tau: float) -> float:
242
+ """Psi function for complex terms (R CHNOSZ Psi function)."""
243
+ C_val = self.C_res[i]
244
+ D_val = self.D_res[i]
245
+ return np.exp(-C_val*(delta-1)**2 - D_val*(tau-1)**2)
246
+
247
+ def _delta_derivatives(self, i: int, delta: float, tau: float) -> Dict[str, float]:
248
+ """Calculate Delta function derivatives (matching R CHNOSZ exactly)."""
249
+ theta = self._theta_function(i, delta, tau)
250
+ A_val = self.A_res[i]
251
+ B_val = self.B_res[i]
252
+ a_val = self.a_res[i]
253
+ b_val = self.b_res[i]
254
+ beta_val = self.beta_res[i]
255
+
256
+ # dDelta/ddelta
257
+ dDelta_ddelta = ((delta-1) *
258
+ (A_val*theta*2/beta_val*((delta-1)**2)**(1/(2*beta_val)-1) +
259
+ 2*B_val*a_val*((delta-1)**2)**(a_val-1)))
260
+
261
+ # d²Delta/ddelta² (handle division by zero when delta ≈ 1)
262
+ if abs(delta - 1) < 1e-15:
263
+ # Use L'Hôpital's rule or limit behavior
264
+ d2Delta_ddelta2 = (4*B_val*a_val*(a_val-1) +
265
+ 2*A_val**2*(1/beta_val)**2 +
266
+ A_val*theta*4/beta_val*(1/(2*beta_val)-1))
267
+ else:
268
+ d2Delta_ddelta2 = (1/(delta-1)*dDelta_ddelta + (delta-1)**2 * (
269
+ 4*B_val*a_val*(a_val-1)*((delta-1)**2)**(a_val-2) +
270
+ 2*A_val**2*(1/beta_val)**2 * (((delta-1)**2)**(1/(2*beta_val)-1))**2 +
271
+ A_val*theta*4/beta_val*(1/(2*beta_val)-1) *
272
+ ((delta-1)**2)**(1/(2*beta_val)-2)))
273
+
274
+ # Delta^b derivatives
275
+ delta_func = self._delta_function(i, delta, tau)
276
+ dDelta_bi_ddelta = b_val * delta_func**(b_val-1) * dDelta_ddelta
277
+ d2Delta_bi_ddelta2 = (b_val * (delta_func**(b_val-1) * d2Delta_ddelta2 +
278
+ (b_val-1) * delta_func**(b_val-2) * dDelta_ddelta**2))
279
+
280
+ # Tau derivatives
281
+ dDelta_bi_dtau = -2*theta*b_val*delta_func**(b_val-1)
282
+ d2Delta_bi_dtau2 = (2*b_val*delta_func**(b_val-1) +
283
+ 4*theta**2*b_val*(b_val-1)*delta_func**(b_val-2))
284
+
285
+ # Mixed derivative
286
+ d2Delta_bi_ddelta_dtau = (-A_val*b_val*2/beta_val*delta_func**(b_val-1)*(delta-1) *
287
+ ((delta-1)**2)**(1/(2*beta_val)-1) -
288
+ 2*theta*b_val*(b_val-1)*delta_func**(b_val-2)*dDelta_ddelta)
289
+
290
+ return {
291
+ 'dDelta_ddelta': dDelta_ddelta,
292
+ 'd2Delta_ddelta2': d2Delta_ddelta2,
293
+ 'dDelta_bi_ddelta': dDelta_bi_ddelta,
294
+ 'd2Delta_bi_ddelta2': d2Delta_bi_ddelta2,
295
+ 'dDelta_bi_dtau': dDelta_bi_dtau,
296
+ 'd2Delta_bi_dtau2': d2Delta_bi_dtau2,
297
+ 'd2Delta_bi_ddelta_dtau': d2Delta_bi_ddelta_dtau
298
+ }
299
+
300
+ def _psi_derivatives(self, i: int, delta: float, tau: float) -> Dict[str, float]:
301
+ """Calculate Psi function derivatives (matching R CHNOSZ exactly)."""
302
+ C_val = self.C_res[i]
303
+ D_val = self.D_res[i]
304
+ psi = self._psi_function(i, delta, tau)
305
+
306
+ return {
307
+ 'dPsi_ddelta': -2*C_val*(delta-1)*psi,
308
+ 'd2Psi_ddelta2': (2*C_val*(delta-1)**2 - 1) * 2*C_val*psi,
309
+ 'dPsi_dtau': -2*D_val*(tau-1)*psi,
310
+ 'd2Psi_dtau2': (2*D_val*(tau-1)**2 - 1) * 2*D_val*psi,
311
+ 'd2Psi_ddelta_dtau': 4*C_val*D_val*(delta-1)*(tau-1)*psi
312
+ }
313
+
314
+ def _phi_residual(self, delta: float, tau: float, derivative: str = 'phi') -> float:
315
+ """
316
+ Calculate residual part of dimensionless Helmholtz energy and derivatives.
317
+
318
+ Exact implementation matching R CHNOSZ IAPWS95.residual function.
319
+ """
320
+ if derivative == 'phi':
321
+ # Four terms as in R CHNOSZ phi function (lines 201-206)
322
+ term1 = np.sum(self.n_res[self.i1] * delta**self.d_res[self.i1] * tau**self.t_res[self.i1])
323
+
324
+ term2 = np.sum(self.n_res[self.i2] * delta**self.d_res[self.i2] * tau**self.t_res[self.i2] *
325
+ np.exp(-delta**self.c_res[self.i2]))
326
+
327
+ term3 = 0.0
328
+ for i in self.i3:
329
+ alpha_val = self.alpha_res[i]
330
+ beta_val = self.beta_res[i]
331
+ epsilon_val = self.epsilon_res[i]
332
+ gamma_val = self.gamma_res[i]
333
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
334
+ term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] *
335
+ np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2))
336
+
337
+ term4 = 0.0
338
+ for i in self.i4:
339
+ if not np.isnan(self.b_res[i]):
340
+ delta_func = self._delta_function(i, delta, tau)
341
+ psi_val = self._psi_function(i, delta, tau)
342
+ term4 += self.n_res[i] * delta_func**self.b_res[i] * delta * psi_val
343
+
344
+ return term1 + term2 + term3 + term4
345
+
346
+ elif derivative == 'phi.delta':
347
+ # phi.delta implementation (R CHNOSZ lines 208-214)
348
+ term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] *
349
+ delta**(self.d_res[self.i1]-1) * tau**self.t_res[self.i1])
350
+
351
+ term2 = np.sum(self.n_res[self.i2] * np.exp(-delta**self.c_res[self.i2]) *
352
+ (delta**(self.d_res[self.i2]-1) * tau**self.t_res[self.i2] *
353
+ (self.d_res[self.i2] - self.c_res[self.i2] * delta**self.c_res[self.i2])))
354
+
355
+ term3 = 0.0
356
+ for i in self.i3:
357
+ alpha_val = self.alpha_res[i]
358
+ beta_val = self.beta_res[i]
359
+ epsilon_val = self.epsilon_res[i]
360
+ gamma_val = self.gamma_res[i]
361
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
362
+ exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
363
+ term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
364
+ (self.d_res[i]/delta - 2*alpha_val*(delta-epsilon_val)))
365
+
366
+ term4 = 0.0
367
+ for i in self.i4:
368
+ if not np.isnan(self.b_res[i]):
369
+ delta_func = self._delta_function(i, delta, tau)
370
+ psi_val = self._psi_function(i, delta, tau)
371
+ psi_derivs = self._psi_derivatives(i, delta, tau)
372
+ delta_derivs = self._delta_derivatives(i, delta, tau)
373
+
374
+ term4 += (self.n_res[i] *
375
+ (delta_func**self.b_res[i] * (psi_val + delta*psi_derivs['dPsi_ddelta']) +
376
+ delta_derivs['dDelta_bi_ddelta'] * delta * psi_val))
377
+
378
+ return term1 + term2 + term3 + term4
379
+
380
+ elif derivative == 'phi.delta.delta':
381
+ # phi.delta.delta implementation (R CHNOSZ lines 216-224)
382
+ term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] * (self.d_res[self.i1]-1) *
383
+ delta**(self.d_res[self.i1]-2) * tau**self.t_res[self.i1])
384
+
385
+ term2 = 0.0
386
+ for i in self.i2:
387
+ d_val = self.d_res[i]
388
+ c_val = self.c_res[i]
389
+ exp_term = np.exp(-delta**c_val)
390
+ factor = ((d_val - c_val*delta**c_val) * (d_val - 1 - c_val*delta**c_val) -
391
+ c_val**2 * delta**c_val)
392
+ term2 += (self.n_res[i] * exp_term * delta**(d_val-2) * tau**self.t_res[i] * factor)
393
+
394
+ term3 = 0.0
395
+ for i in self.i3:
396
+ alpha_val = self.alpha_res[i]
397
+ beta_val = self.beta_res[i]
398
+ epsilon_val = self.epsilon_res[i]
399
+ gamma_val = self.gamma_res[i]
400
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
401
+ d_val = self.d_res[i]
402
+ exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
403
+ factor = (-2*alpha_val*delta**d_val + 4*alpha_val**2*delta**d_val*(delta-epsilon_val)**2 -
404
+ 4*d_val*alpha_val*delta**(d_val-1)*(delta-epsilon_val) +
405
+ d_val*(d_val-1)*delta**(d_val-2))
406
+ term3 += self.n_res[i] * tau**self.t_res[i] * exp_term * factor
407
+
408
+ term4 = 0.0
409
+ for i in self.i4:
410
+ if not np.isnan(self.b_res[i]):
411
+ delta_func = self._delta_function(i, delta, tau)
412
+ psi_val = self._psi_function(i, delta, tau)
413
+ psi_derivs = self._psi_derivatives(i, delta, tau)
414
+ delta_derivs = self._delta_derivatives(i, delta, tau)
415
+
416
+ term4 += (self.n_res[i] *
417
+ (delta_func**self.b_res[i] *
418
+ (2*psi_derivs['dPsi_ddelta'] + delta*psi_derivs['d2Psi_ddelta2']) +
419
+ 2*delta_derivs['dDelta_bi_ddelta'] *
420
+ (psi_val + delta*psi_derivs['dPsi_ddelta']) +
421
+ delta_derivs['d2Delta_bi_ddelta2'] * delta * psi_val))
422
+
423
+ return term1 + term2 + term3 + term4
424
+
425
+ elif derivative == 'phi.tau':
426
+ # phi.tau implementation (R CHNOSZ lines 226-231)
427
+ term1 = np.sum(self.n_res[self.i1] * self.t_res[self.i1] *
428
+ delta**self.d_res[self.i1] * tau**(self.t_res[self.i1]-1))
429
+
430
+ term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] *
431
+ delta**self.d_res[self.i2] * tau**(self.t_res[self.i2]-1) *
432
+ np.exp(-delta**self.c_res[self.i2]))
433
+
434
+ term3 = 0.0
435
+ for i in self.i3:
436
+ alpha_val = self.alpha_res[i]
437
+ beta_val = self.beta_res[i]
438
+ epsilon_val = self.epsilon_res[i]
439
+ gamma_val = self.gamma_res[i]
440
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
441
+ exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
442
+ term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
443
+ (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val)))
444
+
445
+ term4 = 0.0
446
+ for i in self.i4:
447
+ if not np.isnan(self.b_res[i]):
448
+ psi_val = self._psi_function(i, delta, tau)
449
+ psi_derivs = self._psi_derivatives(i, delta, tau)
450
+ delta_derivs = self._delta_derivatives(i, delta, tau)
451
+
452
+ term4 += (self.n_res[i] * delta *
453
+ (delta_derivs['dDelta_bi_dtau'] * psi_val +
454
+ self._delta_function(i, delta, tau)**self.b_res[i] * psi_derivs['dPsi_dtau']))
455
+
456
+ return term1 + term2 + term3 + term4
457
+
458
+ elif derivative == 'phi.tau.tau':
459
+ # phi.tau.tau implementation (R CHNOSZ lines 233-239)
460
+ term1 = np.sum(self.n_res[self.i1] * self.t_res[self.i1] * (self.t_res[self.i1]-1) *
461
+ delta**self.d_res[self.i1] * tau**(self.t_res[self.i1]-2))
462
+
463
+ term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] * (self.t_res[self.i2]-1) *
464
+ delta**self.d_res[self.i2] * tau**(self.t_res[self.i2]-2) *
465
+ np.exp(-delta**self.c_res[self.i2]))
466
+
467
+ term3 = 0.0
468
+ for i in self.i3:
469
+ alpha_val = self.alpha_res[i]
470
+ beta_val = self.beta_res[i]
471
+ epsilon_val = self.epsilon_res[i]
472
+ gamma_val = self.gamma_res[i]
473
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
474
+ exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
475
+ tau_factor = (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val))
476
+ term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
477
+ (tau_factor**2 - self.t_res[i]/tau**2 - 2*beta_val))
478
+
479
+ term4 = 0.0
480
+ for i in self.i4:
481
+ if not np.isnan(self.b_res[i]):
482
+ delta_func = self._delta_function(i, delta, tau)
483
+ psi_val = self._psi_function(i, delta, tau)
484
+ psi_derivs = self._psi_derivatives(i, delta, tau)
485
+ delta_derivs = self._delta_derivatives(i, delta, tau)
486
+
487
+ term4 += (self.n_res[i] * delta *
488
+ (delta_derivs['d2Delta_bi_dtau2'] * psi_val +
489
+ 2*delta_derivs['dDelta_bi_dtau'] * psi_derivs['dPsi_dtau'] +
490
+ delta_func**self.b_res[i] * psi_derivs['d2Psi_dtau2']))
491
+
492
+ return term1 + term2 + term3 + term4
493
+
494
+ elif derivative == 'phi.delta.tau':
495
+ # phi.delta.tau implementation (R CHNOSZ lines 241-248)
496
+ term1 = np.sum(self.n_res[self.i1] * self.d_res[self.i1] * self.t_res[self.i1] *
497
+ delta**(self.d_res[self.i1]-1) * tau**(self.t_res[self.i1]-1))
498
+
499
+ term2 = np.sum(self.n_res[self.i2] * self.t_res[self.i2] *
500
+ delta**(self.d_res[self.i2]-1) * tau**(self.t_res[self.i2]-1) *
501
+ (self.d_res[self.i2] - self.c_res[self.i2]*delta**self.c_res[self.i2]) *
502
+ np.exp(-delta**self.c_res[self.i2]))
503
+
504
+ term3 = 0.0
505
+ for i in self.i3:
506
+ alpha_val = self.alpha_res[i]
507
+ beta_val = self.beta_res[i]
508
+ epsilon_val = self.epsilon_res[i]
509
+ gamma_val = self.gamma_res[i]
510
+ if not (np.isnan(alpha_val) or np.isnan(beta_val)):
511
+ exp_term = np.exp(-alpha_val*(delta-epsilon_val)**2 - beta_val*(tau-gamma_val)**2)
512
+ delta_factor = (self.d_res[i]/delta - 2*alpha_val*(delta-epsilon_val))
513
+ tau_factor = (self.t_res[i]/tau - 2*beta_val*(tau-gamma_val))
514
+ term3 += (self.n_res[i] * delta**self.d_res[i] * tau**self.t_res[i] * exp_term *
515
+ delta_factor * tau_factor)
516
+
517
+ term4 = 0.0
518
+ for i in self.i4:
519
+ if not np.isnan(self.b_res[i]):
520
+ delta_func = self._delta_function(i, delta, tau)
521
+ psi_val = self._psi_function(i, delta, tau)
522
+ psi_derivs = self._psi_derivatives(i, delta, tau)
523
+ delta_derivs = self._delta_derivatives(i, delta, tau)
524
+
525
+ term4 += (self.n_res[i] *
526
+ (delta_func**self.b_res[i] *
527
+ (psi_derivs['dPsi_dtau'] + delta*psi_derivs['d2Psi_ddelta_dtau']) +
528
+ delta*delta_derivs['dDelta_bi_ddelta']*psi_derivs['dPsi_dtau'] +
529
+ delta_derivs['dDelta_bi_dtau'] *
530
+ (psi_val + delta*psi_derivs['dPsi_ddelta']) +
531
+ delta_derivs['d2Delta_bi_ddelta_dtau']*delta*psi_val))
532
+
533
+ return term1 + term2 + term3 + term4
534
+
535
+
536
+
537
+
538
+ else:
539
+ raise ValueError(f"Unknown derivative: {derivative}")
540
+
541
+ def calculate_IAPWS95_property(self, property_name: str, T: float, rho: float) -> float:
542
+ """
543
+ Calculate individual IAPWS95 property (matching R CHNOSZ IAPWS95 function).
544
+
545
+ Parameters
546
+ ----------
547
+ property_name : str
548
+ Property name (matching R CHNOSZ property names)
549
+ T : float
550
+ Temperature in K
551
+ rho : float
552
+ Density in kg/m³
553
+
554
+ Returns
555
+ -------
556
+ float
557
+ Property value
558
+ """
559
+ # Calculate dimensionless variables (Equation 6.4)
560
+ delta = rho / self.rhoc
561
+ tau = self.Tc / T
562
+
563
+ # Property calculations matching R CHNOSZ (lines 32-81)
564
+ if property_name.lower() == 'p':
565
+ # Pressure in MPa (R line 34: x*rho*R*T/1000)
566
+ x = 1 + delta * self._phi_residual(delta, tau, 'phi.delta')
567
+ return x * rho * self.R * T / 1000.0
568
+
569
+ elif property_name.lower() == 's':
570
+ # Entropy in kJ/(kg·K) (R lines 36-38)
571
+ phi_ideal = self._phi_ideal(delta, tau, 'phi')
572
+ phi_residual = self._phi_residual(delta, tau, 'phi')
573
+ phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
574
+ phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
575
+ x = tau * (phi_tau_ideal + phi_tau_residual) - phi_ideal - phi_residual
576
+ return x * self.R
577
+
578
+ elif property_name.lower() == 'u':
579
+ # Internal energy in kJ/kg (R lines 40-42)
580
+ phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
581
+ phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
582
+ x = tau * (phi_tau_ideal + phi_tau_residual)
583
+ return x * self.R * T
584
+
585
+ elif property_name.lower() == 'h':
586
+ # Enthalpy in kJ/kg (R lines 44-46)
587
+ phi_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau')
588
+ phi_tau_residual = self._phi_residual(delta, tau, 'phi.tau')
589
+ phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
590
+ x = 1 + tau * (phi_tau_ideal + phi_tau_residual) + delta * phi_delta_residual
591
+ return x * self.R * T
592
+
593
+ elif property_name.lower() == 'g':
594
+ # Gibbs energy in kJ/kg (R lines 48-50)
595
+ phi_ideal = self._phi_ideal(delta, tau, 'phi')
596
+ phi_residual = self._phi_residual(delta, tau, 'phi')
597
+ phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
598
+ x = 1 + phi_ideal + phi_residual + delta * phi_delta_residual
599
+ return x * self.R * T
600
+
601
+ elif property_name.lower() == 'cv':
602
+ # Isochoric heat capacity in kJ/(kg·K) (R lines 52-54)
603
+ phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
604
+ phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
605
+ x = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
606
+ return x * self.R
607
+
608
+ elif property_name.lower() == 'cp':
609
+ # Isobaric heat capacity in kJ/(kg·K) (R lines 56-60)
610
+ phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
611
+ phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
612
+ phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
613
+ phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
614
+ phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
615
+
616
+ term1 = -tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual)
617
+ term2 = ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
618
+ (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual))
619
+ x = term1 + term2
620
+ return x * self.R
621
+
622
+ elif property_name.lower() == 'w':
623
+ # Speed of sound in m/s (R lines 71-75)
624
+ phi_tau_tau_ideal = self._phi_ideal(delta, tau, 'phi.tau.tau')
625
+ phi_tau_tau_residual = self._phi_residual(delta, tau, 'phi.tau.tau')
626
+ phi_delta_residual = self._phi_residual(delta, tau, 'phi.delta')
627
+ phi_delta_tau_residual = self._phi_residual(delta, tau, 'phi.delta.tau')
628
+ phi_delta_delta_residual = self._phi_residual(delta, tau, 'phi.delta.delta')
629
+
630
+ x = (1 + 2*delta*phi_delta_residual + delta**2*phi_delta_delta_residual -
631
+ ((1 + delta*phi_delta_residual - delta*tau*phi_delta_tau_residual)**2 /
632
+ (tau**2 * (phi_tau_tau_ideal + phi_tau_tau_residual))))
633
+ return np.sqrt(x * self.R * T)
634
+
635
+
636
+
637
+
638
+
639
+
640
+
641
+ else:
642
+ raise ValueError(f"Unknown property: {property_name}")
643
+
644
+ def calculate(self,
645
+ properties: Union[str, List[str]],
646
+ T: Union[float, np.ndarray] = 298.15,
647
+ rho: Union[float, np.ndarray] = 1000.0) -> Union[float, np.ndarray, Dict[str, Any]]:
648
+ """
649
+ Calculate water properties using accurate IAPWS-95.
650
+
651
+ Parameters
652
+ ----------
653
+ properties : str or list of str
654
+ Property or list of properties to calculate
655
+ T : float or array
656
+ Temperature in Kelvin
657
+ rho : float or array
658
+ Density in kg/m³
659
+
660
+ Returns
661
+ -------
662
+ float, array, or dict
663
+ Calculated properties
664
+ """
665
+ # Handle input types
666
+ if isinstance(properties, str):
667
+ properties = [properties]
668
+ single_prop = True
669
+ else:
670
+ single_prop = False
671
+
672
+ # Convert inputs to arrays
673
+ T = np.atleast_1d(np.asarray(T, dtype=float))
674
+ rho = np.atleast_1d(np.asarray(rho, dtype=float))
675
+
676
+ # Ensure same length
677
+ max_len = max(len(T), len(rho))
678
+ if len(T) < max_len:
679
+ T = np.resize(T, max_len)
680
+ if len(rho) < max_len:
681
+ rho = np.resize(rho, max_len)
682
+
683
+ # Calculate properties
684
+ results = {}
685
+
686
+ for prop in properties:
687
+ prop_values = np.full(max_len, np.nan)
688
+
689
+ for i in range(max_len):
690
+ if not (np.isnan(T[i]) or np.isnan(rho[i]) or T[i] <= 0 or rho[i] <= 0):
691
+ try:
692
+ prop_values[i] = self.calculate_IAPWS95_property(prop, T[i], rho[i])
693
+ except Exception:
694
+ prop_values[i] = np.nan
695
+
696
+ results[prop] = prop_values if len(prop_values) > 1 else prop_values[0]
697
+
698
+ # Return results
699
+ if single_prop:
700
+ return results[properties[0]]
701
+ else:
702
+ return results
703
+
704
+
705
+ # Create module-level instance
706
+ accurate_iapws95 = AccurateIAPWS95Water()
707
+
708
+
709
+ def _WP02_auxiliary_accurate(property_type: str, T: Union[float, np.ndarray]) -> np.ndarray:
710
+ """
711
+ Auxiliary equations for liquid-vapor phase boundary (exact R CHNOSZ match).
712
+
713
+ From Wagner and Pruss, 2002, exactly matching R CHNOSZ util.water.R
714
+ """
715
+ T = np.atleast_1d(np.asarray(T, dtype=float))
716
+ result = np.full_like(T, np.nan)
717
+
718
+ # Critical point constants (exactly matching R CHNOSZ)
719
+ T_critical = 647.096 # K
720
+ P_critical = 22.064 # MPa
721
+ rho_critical = 322.0 # kg/m³
722
+
723
+ # Only calculate for valid temperatures below critical point
724
+ valid = (T > 0) & (T < T_critical)
725
+ T_valid = T[valid]
726
+
727
+ if property_type == "P.sigma":
728
+ # Vapor pressure (R CHNOSZ lines 13-25)
729
+ V = 1 - T_valid / T_critical
730
+ a1, a2, a3, a4, a5, a6 = -7.85951783, 1.84408259, -11.7866497, 22.6807411, -15.9618719, 1.80122502
731
+
732
+ ln_P_sigma_P_critical = (T_critical / T_valid *
733
+ (a1*V + a2*V**1.5 + a3*V**3 + a4*V**3.5 + a5*V**4 + a6*V**7.5))
734
+ P_sigma = P_critical * np.exp(ln_P_sigma_P_critical) # MPa
735
+ result[valid] = P_sigma
736
+
737
+ elif property_type == "rho.liquid":
738
+ # Saturated liquid density (R CHNOSZ lines 27-37)
739
+ V = 1 - T_valid / T_critical
740
+ b1, b2, b3, b4, b5, b6 = 1.99274064, 1.09965342, -0.510839303, -1.75493479, -45.5170352, -6.74694450E5
741
+
742
+ rho_liquid = rho_critical * (1 + b1*V**(1/3) + b2*V**(2/3) + b3*V**(5/3) +
743
+ b4*V**(16/3) + b5*V**(43/3) + b6*V**(110/3))
744
+ result[valid] = rho_liquid
745
+
746
+ elif property_type == "rho.vapor":
747
+ # Saturated vapor density (R CHNOSZ lines 38+)
748
+ V = 1 - T_valid / T_critical
749
+ c1, c2, c3, c4, c5, c6 = -2.03150240, -2.68302940, -5.38626492, -17.2991605, -44.7586581, -63.9201063
750
+
751
+ ln_rho_vapor_rho_critical = (c1*V**(1/3) + c2*V**(2/3) + c3*V**(4/3) +
752
+ c4*V**(3) + c5*V**(37/6) + c6*V**(71/6))
753
+ rho_vapor = rho_critical * np.exp(ln_rho_vapor_rho_critical)
754
+ result[valid] = rho_vapor
755
+
756
+ return result
757
+
758
+
759
+ def rho_IAPWS95_accurate(T: Union[float, np.ndarray], P: Union[float, np.ndarray],
760
+ state: str = "", trace: int = 0) -> np.ndarray:
761
+ """
762
+ Return density in kg/m³ corresponding to given pressure (bar) and temperature (K).
763
+
764
+ Exact implementation matching R CHNOSZ rho.IAPWS95 function with numerical root finding.
765
+ """
766
+ T = np.atleast_1d(np.asarray(T, dtype=float))
767
+ P = np.atleast_1d(np.asarray(P, dtype=float))
768
+
769
+ # Ensure T and P have same length
770
+ if len(P) < len(T):
771
+ P = np.resize(P, len(T))
772
+ elif len(T) < len(P):
773
+ T = np.resize(T, len(P))
774
+
775
+ rho = np.full_like(T, np.nan)
776
+
777
+ # Critical point constants
778
+ T_critical = 647.096 # K
779
+ P_critical = 22.064 # MPa
780
+
781
+ # Convert pressure from bar to MPa (matching R code line 60)
782
+ P_MPa = P / 10.0
783
+
784
+ for i in range(len(T)):
785
+ if np.isnan(T[i]) or np.isnan(P[i]) or T[i] <= 0 or P[i] <= 0:
786
+ continue
787
+
788
+ # Function to find zero: P_calculated - P_target = 0
789
+ def dP(rho_guess):
790
+ if rho_guess <= 0:
791
+ return float('inf')
792
+ try:
793
+ # Use the accurate IAPWS95 pressure calculation
794
+ P_calc_MPa = accurate_iapws95.calculate_IAPWS95_property('P', T[i], rho_guess)
795
+ return P_calc_MPa - P_MPa[i]
796
+ except:
797
+ return float('inf')
798
+
799
+ # Phase identification and initial guess setup (matching R logic)
800
+ try:
801
+ Psat = _WP02_auxiliary_accurate("P.sigma", T[i])[0] # This is in MPa
802
+
803
+ if T[i] > T_critical:
804
+ # Above critical temperature - supercritical
805
+ interval = [0.1, 1000.0]
806
+
807
+ elif P_MPa[i] > P_critical:
808
+ # Above critical pressure - supercritical
809
+ rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
810
+ # For high pressures, we need much higher densities
811
+ # Estimate upper bound based on pressure scaling
812
+ rho_upper = rho_sat + (P_MPa[i] - P_critical) * 4.0 # Rough scaling
813
+ interval = [rho_sat, min(rho_upper, 1500.0)] # Cap at reasonable max density
814
+
815
+ elif P_MPa[i] <= 0.9999 * Psat:
816
+ # Steam region
817
+ rho_sat = _WP02_auxiliary_accurate("rho.vapor", T[i])[0]
818
+ interval = [rho_sat * 0.1, rho_sat * 2.0]
819
+
820
+ elif P_MPa[i] >= 1.00005 * Psat:
821
+ # Liquid water region
822
+ rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
823
+ interval = [rho_sat * 0.9, rho_sat * 1.1]
824
+
825
+ else:
826
+ # Close to saturation - use liquid estimate
827
+ rho_sat = _WP02_auxiliary_accurate("rho.liquid", T[i])[0]
828
+ interval = [rho_sat * 0.95, rho_sat * 1.05]
829
+
830
+ # Numerical root finding using Brent's method
831
+ try:
832
+ rho[i] = brentq(dP, interval[0], interval[1], xtol=1e-10, rtol=1e-10)
833
+ except Exception as e:
834
+ if trace > 0:
835
+ print(f"Warning: rho_IAPWS95_accurate problems finding density at {T[i]} K and {P[i]} bar: {e}")
836
+ rho[i] = np.nan
837
+
838
+ except Exception as e:
839
+ if trace > 0:
840
+ print(f"Warning: rho_IAPWS95_accurate problems with phase identification at {T[i]} K and {P[i]} bar: {e}")
841
+ rho[i] = np.nan
842
+
843
+ return rho
844
+
845
+
846
+ def water_IAPWS95_accurate(properties: Union[str, List[str]],
847
+ T: Union[float, np.ndarray] = 298.15,
848
+ P: Union[float, np.ndarray] = 1.0,
849
+ rho: Optional[Union[float, np.ndarray]] = None) -> Union[float, np.ndarray, Dict[str, Any]]:
850
+ """
851
+ Calculate water properties using accurate IAPWS-95 implementation.
852
+
853
+ This function provides an accurate implementation of IAPWS-95 that matches
854
+ the R CHNOSZ package exactly, with no shortcuts or approximations.
855
+
856
+ Parameters
857
+ ----------
858
+ properties : str or list of str
859
+ Property or properties to calculate ('P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho')
860
+ T : float or array
861
+ Temperature in Kelvin
862
+ P : float or array
863
+ Pressure in bar (used to calculate density if rho not provided)
864
+ rho : float or array, optional
865
+ Density in kg/m³. If provided, used directly; if not, calculated from T,P
866
+
867
+ Returns
868
+ -------
869
+ float, array, or dict
870
+ Calculated water properties
871
+
872
+ Examples
873
+ --------
874
+ >>> # Single property with T,P
875
+ >>> p = water_IAPWS95_accurate('P', T=298.15, P=1.0)
876
+ >>>
877
+ >>> # Single property with T,rho
878
+ >>> p = water_IAPWS95_accurate('P', T=298.15, rho=997.0)
879
+ >>>
880
+ >>> # Multiple properties
881
+ >>> props = water_IAPWS95_accurate(['rho', 'Cp'], T=298.15, P=1.0)
882
+ """
883
+ # Handle input types
884
+ if isinstance(properties, str):
885
+ properties = [properties]
886
+ single_prop = True
887
+ else:
888
+ single_prop = False
889
+
890
+ # Convert inputs
891
+ T = np.atleast_1d(np.asarray(T, dtype=float))
892
+
893
+ if rho is None:
894
+ # Calculate density from T,P
895
+ P = np.atleast_1d(np.asarray(P, dtype=float))
896
+ rho_calc = rho_IAPWS95_accurate(T, P)
897
+ else:
898
+ # Use provided density
899
+ rho_calc = np.atleast_1d(np.asarray(rho, dtype=float))
900
+ P = np.atleast_1d(np.asarray(P, dtype=float))
901
+
902
+ # Ensure same length
903
+ max_len = max(len(T), len(rho_calc))
904
+ if len(T) < max_len:
905
+ T = np.resize(T, max_len)
906
+ if len(rho_calc) < max_len:
907
+ rho_calc = np.resize(rho_calc, max_len)
908
+
909
+ # Calculate properties
910
+ results = {}
911
+
912
+ # Reference state correction constants (from R water.R lines 187-194)
913
+ # Convert to SUPCRT reference state at the triple point
914
+ # difference = SUPCRT - IAPWS ( + entropy in G )
915
+ M = 18.015268 # g/mol, molar mass of water
916
+ Tr = 298.15 # Reference temperature
917
+ cal_to_J = 4.184 # Conversion factor from cal to J
918
+
919
+ # Pre-calculate reference corrections (from R)
920
+ dH = (-68316.76 - 451.75437) * cal_to_J # J/mol
921
+ dS = (16.7123 - 1.581072) * cal_to_J # J/mol/K
922
+ dU = (-67434.5 - 451.3229) * cal_to_J # J/mol
923
+ dA_base = (-55814.06 + 20.07376) * cal_to_J # J/mol
924
+
925
+ for prop in properties:
926
+ if prop.lower() == 'rho':
927
+ # Return density
928
+ results[prop] = rho_calc if len(rho_calc) > 1 else rho_calc[0]
929
+ else:
930
+ # Calculate other properties
931
+ prop_values = np.full(max_len, np.nan)
932
+
933
+ for i in range(max_len):
934
+ if not (np.isnan(T[i]) or np.isnan(rho_calc[i]) or T[i] <= 0 or rho_calc[i] <= 0):
935
+ try:
936
+ # Get raw IAPWS95 value in kJ/kg (specific units)
937
+ raw_value = accurate_iapws95.calculate_IAPWS95_property(prop, T[i], rho_calc[i])
938
+
939
+ # Convert to J/mol (molar units) and apply reference state corrections
940
+ if prop.lower() == 'g':
941
+ # Gibbs energy: IAPWS95("g")*M + dG
942
+ dG = (-56687.71 + 19.64228 - dS * (T[i] - Tr)) * cal_to_J
943
+ prop_values[i] = raw_value * M + dG
944
+ elif prop.lower() == 'h':
945
+ # Enthalpy: IAPWS95("h")*M + dH
946
+ prop_values[i] = raw_value * M + dH
947
+ elif prop.lower() == 'u':
948
+ # Internal energy: IAPWS95("u")*M + dU
949
+ prop_values[i] = raw_value * M + dU
950
+ elif prop.lower() == 'a':
951
+ # Helmholtz energy: IAPWS95("a")*M + dA
952
+ dA = dA_base - dS * (T[i] - Tr)
953
+ prop_values[i] = raw_value * M + dA
954
+ elif prop.lower() == 's':
955
+ # Entropy: IAPWS95("s")*M + dS
956
+ prop_values[i] = raw_value * M + dS
957
+ elif prop.lower() in ['cv', 'cp']:
958
+ # Heat capacities: just convert to molar units (no reference correction)
959
+ prop_values[i] = raw_value * M
960
+ else:
961
+ # Other properties (P, w, etc.): use as-is or convert units as needed
962
+ if prop.lower() == 'w':
963
+ # Speed of sound: convert m/s to cm/s (factor of 100)
964
+ prop_values[i] = raw_value * 100
965
+ elif prop.lower() == 'p':
966
+ # Pressure: convert from MPa to bar (factor of 10)
967
+ prop_values[i] = raw_value * 10
968
+ else:
969
+ # Other properties: return as-is
970
+ prop_values[i] = raw_value
971
+
972
+ except Exception:
973
+ prop_values[i] = np.nan
974
+
975
+ results[prop] = prop_values if len(prop_values) > 1 else prop_values[0]
976
+
977
+ # Return results
978
+ if single_prop:
979
+ return results[properties[0]]
980
+ else:
981
+ return results
982
+
983
+
984
+ # ========================================================================
985
+ # INTERFACE LAYER - Provides standardized API and unit conversion
986
+ # ========================================================================
987
+
988
+ class IAPWS95Water:
989
+ """
990
+ IAPWS-95 water model interface class.
991
+
992
+ This class provides thermodynamic properties of water using the IAPWS-95
993
+ formulation based on a fundamental equation for the Helmholtz free energy.
994
+
995
+ This interface handles unit conversions and provides a standardized API
996
+ that matches other water models in the package.
997
+ """
998
+
999
+ def __init__(self):
1000
+ """Initialize IAPWS95 water model."""
1001
+ pass
1002
+
1003
+ def available_properties(self) -> List[str]:
1004
+ """
1005
+ Get list of available properties.
1006
+
1007
+ Returns
1008
+ -------
1009
+ List[str]
1010
+ List of available property names
1011
+ """
1012
+ return ['P', 'S', 'U', 'H', 'G', 'Cv', 'Cp', 'w', 'rho']
1013
+
1014
+ def calculate(self,
1015
+ properties: Union[str, List[str]],
1016
+ T: Union[float, np.ndarray] = 298.15,
1017
+ P: Union[float, np.ndarray] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
1018
+ """
1019
+ Calculate water properties using IAPWS-95.
1020
+
1021
+ Parameters
1022
+ ----------
1023
+ properties : str or list of str
1024
+ Property or list of properties to calculate
1025
+ T : float or array
1026
+ Temperature in Kelvin
1027
+ P : float or array
1028
+ Pressure in kPa
1029
+
1030
+ Returns
1031
+ -------
1032
+ float, array, or dict
1033
+ Calculated properties
1034
+ """
1035
+ # Convert pressure from kPa to bar for the accurate implementation
1036
+ if isinstance(P, (int, float)):
1037
+ P_bar = P / 100.0
1038
+ else:
1039
+ P_bar = np.asarray(P) / 100.0
1040
+
1041
+ # Use the accurate implementation (defined above in this file)
1042
+ return water_IAPWS95_accurate(properties, T=T, P=P_bar)
1043
+
1044
+
1045
+ # Create module-level instance for backward compatibility
1046
+ iapws95_water = IAPWS95Water()
1047
+
1048
+
1049
+ def water_IAPWS95(properties: Union[str, List[str]],
1050
+ T: Union[float, np.ndarray] = 298.15,
1051
+ P: Union[float, np.ndarray] = 100.0) -> Union[float, np.ndarray, Dict[str, Any]]:
1052
+ """
1053
+ Calculate water properties using IAPWS-95.
1054
+
1055
+ This function provides the main interface to IAPWS-95 water properties,
1056
+ using the accurate implementation that matches R CHNOSZ exactly.
1057
+
1058
+ Parameters
1059
+ ----------
1060
+ properties : str or list of str
1061
+ Property or properties to calculate:
1062
+ - 'P': Pressure in MPa
1063
+ - 'S': Entropy in kJ/(kg·K)
1064
+ - 'U': Internal energy in kJ/kg
1065
+ - 'H': Enthalpy in kJ/kg
1066
+ - 'G': Gibbs free energy in kJ/kg
1067
+ - 'Cv': Isochoric heat capacity in kJ/(kg·K)
1068
+ - 'Cp': Isobaric heat capacity in kJ/(kg·K)
1069
+ - 'w': Speed of sound in m/s
1070
+ - 'rho': Density in kg/m³
1071
+ T : float or array
1072
+ Temperature in Kelvin
1073
+ P : float or array
1074
+ Pressure in kPa (note: different from other modules that use bar)
1075
+
1076
+ Returns
1077
+ -------
1078
+ float, array, or dict
1079
+ Calculated water properties
1080
+
1081
+ Examples
1082
+ --------
1083
+ >>> import numpy as np
1084
+ >>>
1085
+ >>> # Single property at standard conditions
1086
+ >>> rho = water_IAPWS95('rho', T=298.15, P=100.0) # 100 kPa = 1 bar
1087
+ >>> print(f"Density: {rho:.3f} kg/m³")
1088
+ >>>
1089
+ >>> # Multiple properties
1090
+ >>> props = water_IAPWS95(['rho', 'Cp'], T=298.15, P=100.0)
1091
+ >>> print(f"Density: {props['rho']:.3f} kg/m³")
1092
+ >>> print(f"Heat capacity: {props['Cp']:.3f} kJ/(kg·K)")
1093
+ >>>
1094
+ >>> # Array calculations
1095
+ >>> T_array = np.array([273.15, 298.15, 373.15])
1096
+ >>> densities = water_IAPWS95('rho', T=T_array, P=100.0)
1097
+ """
1098
+ # Convert pressure from kPa to bar for the accurate implementation
1099
+ if isinstance(P, (int, float)):
1100
+ P_bar = P / 100.0
1101
+ else:
1102
+ P_bar = np.asarray(P) / 100.0
1103
+
1104
+ # Use the accurate implementation
1105
+ return water_IAPWS95_accurate(properties, T=T, P=P_bar)
1106
+
1107
+
1108
+ # Alias for consistency with naming conventions
1109
+ water_iapws95 = water_IAPWS95
1110
+
1111
+
1112
+ # The main API functions are already defined above as top-level functions
1113
+ # No need to redefine them here