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