iapws-if97 2.2.0 → 2.8.0
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.
- package/CHANGELOG.md +25 -2
- package/README.md +69 -246
- package/dist/backward/candidate-resolution.d.ts +8 -0
- package/dist/backward/candidate-resolution.js +40 -0
- package/dist/backward/fixed-temperature-candidates.d.ts +4 -0
- package/dist/backward/fixed-temperature-candidates.js +188 -0
- package/dist/backward/fixed-temperature-solver.d.ts +3 -3
- package/dist/backward/fixed-temperature-solver.js +38 -17
- package/dist/backward/fixed-temperature-stationary.d.ts +4 -0
- package/dist/backward/fixed-temperature-stationary.js +146 -0
- package/dist/backward/hs-region.d.ts +50 -0
- package/dist/backward/hs-region.js +695 -0
- package/dist/backward/hs-state.d.ts +15 -0
- package/dist/backward/hs-state.js +316 -0
- package/dist/backward/hs.js +28 -242
- package/dist/backward/ph.d.ts +2 -0
- package/dist/backward/ph.js +72 -21
- package/dist/backward/pressure-property-region.d.ts +13 -0
- package/dist/backward/pressure-property-region.js +262 -0
- package/dist/backward/ps.d.ts +6 -0
- package/dist/backward/ps.js +81 -28
- package/dist/backward/solution-validation.d.ts +7 -1
- package/dist/backward/solution-validation.js +138 -41
- package/dist/backward/th.d.ts +3 -5
- package/dist/backward/th.js +10 -71
- package/dist/backward/tolerances.d.ts +3 -0
- package/dist/backward/tolerances.js +9 -3
- package/dist/backward/ts.d.ts +3 -5
- package/dist/backward/ts.js +10 -71
- package/dist/constants.d.ts +11 -15
- package/dist/constants.js +14 -15
- package/dist/core/floating-point.d.ts +2 -0
- package/dist/core/floating-point.js +7 -0
- package/dist/core/formulation-seams.d.ts +27 -0
- package/dist/core/formulation-seams.js +289 -0
- package/dist/core/input-validation.d.ts +1 -0
- package/dist/core/input-validation.js +5 -0
- package/dist/core/region-detector.d.ts +3 -40
- package/dist/core/region-detector.js +37 -762
- package/dist/core/region3-pt.d.ts +1 -1
- package/dist/core/region3-pt.js +42 -16
- package/dist/core/solve-input-normalization.js +4 -1
- package/dist/core/solver.d.ts +7 -10
- package/dist/core/solver.js +26 -17
- package/dist/detect.d.ts +16 -2
- package/dist/detect.js +36 -2
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +16 -0
- package/dist/gas-constant.d.ts +5 -0
- package/dist/gas-constant.js +5 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +10 -21
- package/dist/regions/region1.d.ts +2 -1
- package/dist/regions/region1.js +1 -1
- package/dist/regions/region2.d.ts +2 -1
- package/dist/regions/region2.js +1 -1
- package/dist/regions/region3-data.js +3 -3
- package/dist/regions/region3-subregions.js +15 -10
- package/dist/regions/region3.d.ts +4 -1
- package/dist/regions/region3.js +32 -2
- package/dist/regions/region4.d.ts +2 -0
- package/dist/regions/region4.js +39 -12
- package/dist/regions/region5.d.ts +2 -1
- package/dist/regions/region5.js +2 -2
- package/dist/saturation/common.d.ts +2 -8
- package/dist/saturation/common.js +64 -36
- package/dist/saturation/region4-boundaries.js +13 -13
- package/dist/saturation/region4-hs.js +282 -58
- package/dist/saturation/two-phase.d.ts +2 -2
- package/dist/saturation/two-phase.js +7 -7
- package/dist/solvers/bracketed-newton.d.ts +6 -3
- package/dist/solvers/bracketed-newton.js +15 -8
- package/dist/solvers/extended-arithmetic.d.ts +6 -0
- package/dist/solvers/extended-arithmetic.js +38 -0
- package/dist/transport/properties.d.ts +4 -2
- package/dist/transport/properties.js +14 -4
- package/dist/types.d.ts +18 -0
- package/dist/types.js +14 -0
- package/package.json +4 -4
- package/dist/core/public-normalization.d.ts +0 -3
- package/dist/core/public-normalization.js +0 -53
- /package/dist/{backward → core}/thermodynamic-derivatives.d.ts +0 -0
- /package/dist/{backward → core}/thermodynamic-derivatives.js +0 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import * as C from '../constants.js';
|
|
2
|
+
import { createFixedTemperatureStationaryPressureFinder } from './fixed-temperature-stationary.js';
|
|
3
|
+
import { solveHSInRegion, solveHSSeam } from './hs-state.js';
|
|
4
|
+
import { nextPositiveFloat } from '../core/floating-point.js';
|
|
5
|
+
import { boundary23_T_to_P } from '../regions/boundaries.js';
|
|
6
|
+
import { region1 } from '../regions/region1.js';
|
|
7
|
+
import { region2 } from '../regions/region2.js';
|
|
8
|
+
import { region5 } from '../regions/region5.js';
|
|
9
|
+
import { saturationEndpointsAtTemperature } from '../saturation/common.js';
|
|
10
|
+
import { tryRegion4HSState } from '../saturation/region4-hs.js';
|
|
11
|
+
import { IF97Error, OutOfRangeError, Region } from '../types.js';
|
|
12
|
+
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
13
|
+
import { solveRegion3PTBasic } from '../core/region3-pt.js';
|
|
14
|
+
import { findHSSeam } from '../core/formulation-seams.js';
|
|
15
|
+
import { withinClosedLowerBound } from '../core/input-validation.js';
|
|
16
|
+
import { resolveBackwardCandidates } from './candidate-resolution.js';
|
|
17
|
+
/** Check approximate equality with relative tolerance. */
|
|
18
|
+
function isClose(a, b) {
|
|
19
|
+
return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
20
|
+
}
|
|
21
|
+
// ─── H-S Boundary Helper Functions ──────────────────────────────────────────
|
|
22
|
+
// From IAPWS Supplementary Release on backward equations p(h,s)
|
|
23
|
+
/**
|
|
24
|
+
* Saturated liquid enthalpy boundary for Region 1 (Eq. 3).
|
|
25
|
+
* h'_1(s)
|
|
26
|
+
*/
|
|
27
|
+
export function h1Sat(s) {
|
|
28
|
+
const TABLE = [
|
|
29
|
+
[0, 14, 0.332171191705237], [0, 36, 6.11217706323496e-4], [1, 3, -8.82092478906822],
|
|
30
|
+
[1, 16, -0.45562819254325], [2, 0, -2.63483840850452e-5], [2, 5, -22.3949661148062],
|
|
31
|
+
[3, 4, -4.28398660164013], [3, 36, -0.616679338856916], [4, 4, -14.6823031104040],
|
|
32
|
+
[4, 16, 284.523138727299], [4, 24, -113.398503195444], [5, 18, 1156.71380760859],
|
|
33
|
+
[5, 24, 395.551267359325], [7, 1, -1.54891257229285], [8, 4, 19.4486637751291],
|
|
34
|
+
[12, 2, -3.57915139457043], [12, 4, -3.35369414148819], [14, 1, -0.66442679633246],
|
|
35
|
+
[14, 22, 32332.1885383934], [16, 10, 3317.66744667084], [20, 12, -22350.1257931087],
|
|
36
|
+
[20, 28, 5739538.75852936], [22, 8, 173.226193407919], [24, 3, -0.0363968822121321],
|
|
37
|
+
[28, 0, 8.34596332878346e-7], [32, 6, 5.03611916682674], [32, 8, 65.5444787064505],
|
|
38
|
+
];
|
|
39
|
+
const sig = s / 3.8;
|
|
40
|
+
let h = 0;
|
|
41
|
+
for (const [I, J, N] of TABLE) {
|
|
42
|
+
h += N * Math.pow(sig - 1.09, I) * Math.pow(sig + 0.0000366, J);
|
|
43
|
+
}
|
|
44
|
+
return 1700 * h;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Region 1 upper enthalpy bound at 100 MPa for a given entropy.
|
|
48
|
+
* This closes the low-entropy infeasible gap where using the global
|
|
49
|
+
* Region 1 enthalpy maximum would admit impossible h-s pairs.
|
|
50
|
+
*/
|
|
51
|
+
function h1UpperBoundAtPmax(s) {
|
|
52
|
+
let cachedTemperature = Number.NaN;
|
|
53
|
+
let cachedState = null;
|
|
54
|
+
const stateAt = (T) => {
|
|
55
|
+
if (T !== cachedTemperature || cachedState === null) {
|
|
56
|
+
cachedTemperature = T;
|
|
57
|
+
cachedState = region1(C.P_MAX, T);
|
|
58
|
+
}
|
|
59
|
+
return cachedState;
|
|
60
|
+
};
|
|
61
|
+
const temperature = bracketedNewton((T) => stateAt(T).entropy - s, C.T_MIN, C.R2_T_MIN, 350, { derivative: (T) => (stateAt(T).cp ?? Number.NaN) / T, tolerance: 1e-12 });
|
|
62
|
+
return stateAt(temperature).enthalpy;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Region 1 / Region 3 boundary enthalpy (Eq. 1, B13).
|
|
66
|
+
* h_13(s)
|
|
67
|
+
*/
|
|
68
|
+
export function h13Boundary(s) {
|
|
69
|
+
const TABLE = [
|
|
70
|
+
[0, 0, 0.913965547600543], [1, -2, -0.0000430944856041991], [1, 2, 60.3235694765419],
|
|
71
|
+
[3, -12, 1.17518273082168e-18], [5, -4, 0.220000904781292], [6, -3, -69.0815545851641],
|
|
72
|
+
];
|
|
73
|
+
const sig = s / 3.8;
|
|
74
|
+
let h = 0;
|
|
75
|
+
for (const [I, J, N] of TABLE) {
|
|
76
|
+
h += N * Math.pow(sig - 0.884, I) * Math.pow(sig - 0.864, J);
|
|
77
|
+
}
|
|
78
|
+
return 1700 * h;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Region 3a / Region 4 saturation boundary (Eq. 4).
|
|
82
|
+
* h'_3a(s)
|
|
83
|
+
*/
|
|
84
|
+
export function h3aSat(s) {
|
|
85
|
+
const TABLE = [
|
|
86
|
+
[0, 1, 0.822673364673336], [0, 4, 0.181977213534479], [0, 10, -0.0112000260313624],
|
|
87
|
+
[0, 16, -7.46778287048033e-4], [2, 1, -0.179046263257381], [3, 36, 0.0424220110836657],
|
|
88
|
+
[4, 3, -0.341355823438768], [4, 16, -2.09881740853565], [5, 20, -8.22477343323596],
|
|
89
|
+
[5, 36, -4.99684082076008], [6, 4, 0.191413958471069], [7, 2, 0.0581062241093136],
|
|
90
|
+
[7, 28, -1655.05498701029], [7, 32, 1588.70443421201], [10, 14, -85.0623535172818],
|
|
91
|
+
[10, 32, -31771.4386511207], [10, 36, -94589.0406632871], [32, 0, -1.3927384708869e-6],
|
|
92
|
+
[32, 6, 0.63105253224098],
|
|
93
|
+
];
|
|
94
|
+
const sig = s / 3.8;
|
|
95
|
+
let h = 0;
|
|
96
|
+
for (const [I, J, N] of TABLE) {
|
|
97
|
+
h += N * Math.pow(sig - 1.09, I) * Math.pow(sig + 0.0000366, J);
|
|
98
|
+
}
|
|
99
|
+
return 1700 * h;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Region 2ab / Region 4 saturation boundary (Eq. 5).
|
|
103
|
+
* h"_2ab(s)
|
|
104
|
+
*/
|
|
105
|
+
export function h2abSat(s) {
|
|
106
|
+
const TABLE = [
|
|
107
|
+
[1, 8, -524.581170928788], [1, 24, -9269472.18142218], [2, 4, -237.385107491666],
|
|
108
|
+
[2, 32, 2.10770155812776e10], [4, 1, -23.9494562010986], [4, 2, 221.802480294197],
|
|
109
|
+
[7, 7, -5104725.33393438], [8, 5, 1249813.96109147], [8, 12, 2.00008436996201e9],
|
|
110
|
+
[10, 1, -815.158509791035], [12, 0, -157.612685637523], [12, 7, -1.14200422332791e10],
|
|
111
|
+
[18, 10, 6.62364680776872e15], [20, 12, -2.27622818296144e18], [24, 32, -1.71048081348406e31],
|
|
112
|
+
[28, 8, 6.60788766938091e15], [28, 12, 1.66320055886021e22], [28, 20, -2.18003784381501e29],
|
|
113
|
+
[28, 22, -7.87276140295618e29], [28, 24, 1.51062329700346e31], [32, 2, 7.95732170300541e6],
|
|
114
|
+
[32, 7, 1.31957647355347e15], [32, 12, -3.2509706829914e23], [32, 14, -4.18600611419248e25],
|
|
115
|
+
[32, 24, 2.97478906557467e34], [36, 10, -9.53588761745473e19], [36, 12, 1.66957699620939e24],
|
|
116
|
+
[36, 20, -1.75407764869978e32], [36, 22, 3.47581490626396e34], [36, 28, -7.10971318427851e38],
|
|
117
|
+
];
|
|
118
|
+
const sig1 = s / 5.21;
|
|
119
|
+
const sig2 = s / 9.2;
|
|
120
|
+
let h = 0;
|
|
121
|
+
for (const [I, J, N] of TABLE) {
|
|
122
|
+
h += N * Math.pow(1 / sig1 - 0.513, I) * Math.pow(sig2 - 0.524, J);
|
|
123
|
+
}
|
|
124
|
+
return 2800 * Math.exp(h);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Region 2c / Region 3b saturation boundary (Eq. 6).
|
|
128
|
+
* h"_2c3b(s)
|
|
129
|
+
*/
|
|
130
|
+
export function h2c3bSat(s) {
|
|
131
|
+
const TABLE = [
|
|
132
|
+
[0, 0, 1.04351280732769], [0, 3, -2.27807912708513], [0, 4, 1.80535256723202],
|
|
133
|
+
[1, 0, 0.420440834792042], [1, 12, -1.0572124483466e5], [5, 36, 4.36911607493884e24],
|
|
134
|
+
[6, 12, -3.28032702839753e11], [7, 16, -6.7868676080427e15], [8, 2, 7.43957464645363e3],
|
|
135
|
+
[8, 20, -3.56896445355761e19], [12, 32, 1.67590585186801e31], [16, 36, -3.55028625419105e37],
|
|
136
|
+
[22, 2, 3.96611982166538e11], [22, 32, -4.14716268484468e40], [24, 7, 3.59080103867382e18],
|
|
137
|
+
[36, 20, -1.16994334851995e40],
|
|
138
|
+
];
|
|
139
|
+
const sig = s / 5.9;
|
|
140
|
+
let h = 0;
|
|
141
|
+
for (const [I, J, N] of TABLE) {
|
|
142
|
+
h += N * Math.pow(sig - 1.02, I) * Math.pow(sig - 0.726, J);
|
|
143
|
+
}
|
|
144
|
+
return 2800 * h * h * h * h;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Region 2/3 boundary temperature from h,s (Eq. 8).
|
|
148
|
+
* T_B23(h,s)
|
|
149
|
+
*/
|
|
150
|
+
export function tB23hs(h, s) {
|
|
151
|
+
const TABLE = [
|
|
152
|
+
[-12, 10, 6.2909626082981e-4], [-10, 8, -8.23453502583165e-4], [-8, 3, 5.15446951519474e-8],
|
|
153
|
+
[-4, 4, -1.17565945784945], [-3, 3, 3.48519684726192], [-2, -6, -5.07837382408313e-12],
|
|
154
|
+
[-2, 2, -2.84637670005479], [-2, 3, -2.36092263939673], [-2, 4, 6.01492324973779],
|
|
155
|
+
[0, 0, 1.48039650824546], [1, -3, 3.60075182221907e-4], [1, -2, -1.26700045009952e-2],
|
|
156
|
+
[1, 10, -1.22184332521413e6], [3, -2, 1.49276502463272e-1], [3, -1, 6.98733471798484e-1],
|
|
157
|
+
[5, -5, -2.52207040114321e-2], [6, -6, 1.47151930985213e-2], [6, -3, -1.08618917681849],
|
|
158
|
+
[8, -8, -9.36875039816322e-4], [8, -2, 8.19877897570217e1], [8, -1, -1.82041861521835e2],
|
|
159
|
+
[12, -12, 2.61907376402688e-6], [12, -1, -2.91626417025961e4], [14, -12, 1.40660774926165e-5],
|
|
160
|
+
[14, 1, 7.83237062349385e6],
|
|
161
|
+
];
|
|
162
|
+
const mu = h / 3000;
|
|
163
|
+
const sig = s / 5.3;
|
|
164
|
+
let T = 0;
|
|
165
|
+
for (const [I, J, N] of TABLE) {
|
|
166
|
+
T += N * Math.pow(mu - 0.727, I) * Math.pow(sig - 0.864, J);
|
|
167
|
+
}
|
|
168
|
+
return 900 * T;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Backward equation P(h,s) for Region 2c (used for R2/R3 discrimination).
|
|
172
|
+
*/
|
|
173
|
+
export function backward2cPhs(h, s) {
|
|
174
|
+
const TABLE = [
|
|
175
|
+
[0, 0, 0.112225607199012], [0, 1, -3.39005953606712], [0, 2, -32.0503911730094],
|
|
176
|
+
[0, 3, -197.5973051049], [0, 4, -407.693861553446], [0, 8, 13294.3775222331],
|
|
177
|
+
[1, 0, 1.70846839774007], [1, 2, 37.3694198142245], [1, 5, 3581.44365815434],
|
|
178
|
+
[1, 8, 423014.446424664], [1, 14, -751071025.760063], [2, 2, 52.3446127607898],
|
|
179
|
+
[2, 3, -228.351290812417], [2, 7, -960652.417056937], [2, 10, -80705929.2526074],
|
|
180
|
+
[2, 18, 1626980172256.69], [3, 0, 0.772465073604171], [3, 5, 46392.9973837746],
|
|
181
|
+
[3, 8, -13731788.5134128], [3, 16, 1704703926305.12], [3, 18, -25110462818730.8],
|
|
182
|
+
[4, 18, 31774883083552.0], [5, 1, 53.8685623675312], [5, 4, -55308.9094625169],
|
|
183
|
+
[5, 6, -1028615.22421405], [5, 14, 2042494187562.34], [6, 8, 273918446.626977],
|
|
184
|
+
[6, 18, -2.63963146312685e15], [10, 7, -1078908541.08088], [12, 7, -29649262098.0124],
|
|
185
|
+
[16, 10, -1.11754907323424e15],
|
|
186
|
+
];
|
|
187
|
+
const mu = h / 3500;
|
|
188
|
+
const sig = s / 5.9;
|
|
189
|
+
let P = 0;
|
|
190
|
+
for (const [I, J, N] of TABLE) {
|
|
191
|
+
P += N * Math.pow(mu - 0.7, I) * Math.pow(sig - 1.1, J);
|
|
192
|
+
}
|
|
193
|
+
return 100 * P * P * P * P;
|
|
194
|
+
}
|
|
195
|
+
const HS_R1_PMAX_623 = region1(C.P_MAX, C.R2_T_MIN);
|
|
196
|
+
const HS_R1_PMIN_TMIN = region1(C.P_MIN, C.T_MIN);
|
|
197
|
+
const HS_R1_TMIN_SMAX = region1(createFixedTemperatureStationaryPressureFinder(C.T_MIN, 'entropy')(HS_R1_PMIN_TMIN, region1(C.P_MAX, C.T_MIN))[0], C.T_MIN);
|
|
198
|
+
const HS_R3_PMAX_623 = solveRegion3PTBasic(C.P_MAX, C.R2_T_MIN);
|
|
199
|
+
const HS_R1_B23_MIN = region1(C.B23_P_MIN, C.R2_T_MIN);
|
|
200
|
+
const HS_R2_PMAX_TMAX = region2(C.P_MAX, C.R2_T_MAX);
|
|
201
|
+
const HS_R2_PMIN_TMAX = region2(C.P_MIN, C.R2_T_MAX);
|
|
202
|
+
const HS_R2_PMIN_TMIN = region2(C.P_MIN, C.T_MIN);
|
|
203
|
+
const HS_R2_B23_MIN = region2(C.B23_P_MIN, C.R2_T_MIN);
|
|
204
|
+
const HS_R2_B23_MAX = region2(C.P_MAX, C.B23_T_MAX);
|
|
205
|
+
const HS_R2_R5_PMAX = region2(C.R5_P_MAX, C.R2_T_MAX);
|
|
206
|
+
const HS_R5_LOWER = region5(C.R5_P_MAX, C.R5_T_MIN);
|
|
207
|
+
const HS_R5_PMIN_TMIN = region5(C.P_MIN, C.R5_T_MIN);
|
|
208
|
+
const HS_R5_UPPER = region5(C.P_MIN, C.R5_T_MAX);
|
|
209
|
+
const HS_R5_PMAX_TMAX = region5(C.R5_P_MAX, C.R5_T_MAX);
|
|
210
|
+
const HS_TRIPLE_POINT_ENDPOINTS = saturationEndpointsAtTemperature(C.Tt);
|
|
211
|
+
const HS_TRIPLE_LIQUID = HS_TRIPLE_POINT_ENDPOINTS.liquid;
|
|
212
|
+
const HS_TRIPLE_VAPOR = HS_TRIPLE_POINT_ENDPOINTS.vapor;
|
|
213
|
+
const HS_S13 = HS_R1_PMAX_623.entropy;
|
|
214
|
+
const HS_S13S = HS_R1_B23_MIN.entropy;
|
|
215
|
+
const HS_S_TP_MAX = HS_R2_PMAX_TMAX.entropy;
|
|
216
|
+
const HS_S2AB = region2(C.R2_P_CRT, C.R2_T_MAX).entropy;
|
|
217
|
+
const HS_R25_SEAM_RELATIVE_TOLERANCE = 1e-10;
|
|
218
|
+
const HS_BOUNDARY_ENTROPY_TOLERANCE = 1e-12;
|
|
219
|
+
// Covers the 0.0073 kJ/kg maximum boundary-equation error reported by IAPWS.
|
|
220
|
+
const HS_SATURATION_BOUNDARY_CONFIRMATION_BAND = 0.01;
|
|
221
|
+
const HS_SATURATION_BOUNDARY_ENTHALPY_TOLERANCE = 1e-8;
|
|
222
|
+
const HS_SATURATION_INVERSION_UPPER_T = C.Tc - C.REGION4_SUBCRITICAL_TEMPERATURE_MARGIN;
|
|
223
|
+
function h23UpperBoundAtEntropy(s) {
|
|
224
|
+
const isobar = s <= HS_S_TP_MAX;
|
|
225
|
+
// Region 2 supplies the higher reachable branch in the native B23 overlap.
|
|
226
|
+
const region3 = s < HS_R2_B23_MAX.entropy;
|
|
227
|
+
const lower = isobar ? region3 ? C.R2_T_MIN : C.B23_T_MAX : C.P_MIN;
|
|
228
|
+
const upper = isobar ? region3 ? C.B23_T_MAX : C.R2_T_MAX : C.P_MAX;
|
|
229
|
+
let cachedValue = Number.NaN;
|
|
230
|
+
let cachedState = null;
|
|
231
|
+
const stateAt = (value) => {
|
|
232
|
+
if (value !== cachedValue || cachedState === null) {
|
|
233
|
+
cachedValue = value;
|
|
234
|
+
cachedState = region3
|
|
235
|
+
? solveRegion3PTBasic(C.P_MAX, value)
|
|
236
|
+
: region2(isobar ? C.P_MAX : value, isobar ? value : C.R2_T_MAX);
|
|
237
|
+
}
|
|
238
|
+
return cachedState;
|
|
239
|
+
};
|
|
240
|
+
const boundary = hsBoundaryAtEntropy(s, lower, upper, isobar, stateAt);
|
|
241
|
+
return boundary.enthalpy + boundary.tolerance;
|
|
242
|
+
}
|
|
243
|
+
function hsBoundaryAtEntropy(s, lower, upper, isobar, stateAt) {
|
|
244
|
+
const entropyTolerance = HS_BOUNDARY_ENTROPY_TOLERANCE;
|
|
245
|
+
const value = bracketedNewton((candidate) => stateAt(candidate).entropy - s, lower, upper, undefined, {
|
|
246
|
+
tolerance: entropyTolerance,
|
|
247
|
+
derivative: (candidate) => {
|
|
248
|
+
const state = stateAt(candidate);
|
|
249
|
+
return isobar
|
|
250
|
+
? (state.cp ?? Number.NaN) / state.temperature
|
|
251
|
+
: -1000 * state.specificVolume * (state.isobaricExpansion ?? Number.NaN);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const state = stateAt(value);
|
|
255
|
+
const slope = Math.abs(isobar
|
|
256
|
+
? state.temperature
|
|
257
|
+
: state.temperature - 1 / (state.isobaricExpansion ?? Number.NaN));
|
|
258
|
+
// Propagate inversion precision and floating-point residue through dh/ds.
|
|
259
|
+
const tolerance = slope * Math.max(entropyTolerance, Math.abs(state.entropy - s)) + 8 * Number.EPSILON * (Math.max(1, Math.abs(state.enthalpy)) + slope * Math.max(1, Math.abs(s)));
|
|
260
|
+
return { enthalpy: state.enthalpy, tolerance, state };
|
|
261
|
+
}
|
|
262
|
+
function region3LowerHSCandidate(h, s) {
|
|
263
|
+
const entropySpan = Math.abs(HS_S13 - HS_R3_PMAX_623.entropy);
|
|
264
|
+
if (s >= HS_S13 || Math.abs(s - HS_R3_PMAX_623.entropy) > entropySpan)
|
|
265
|
+
return null;
|
|
266
|
+
const lowerTemperature = nextPositiveFloat(C.R2_T_MIN, 1);
|
|
267
|
+
const lowerState = solveRegion3PTBasic(C.P_MAX, lowerTemperature);
|
|
268
|
+
if (s < lowerState.entropy - HS_BOUNDARY_ENTROPY_TOLERANCE)
|
|
269
|
+
return null;
|
|
270
|
+
const boundary = hsBoundaryAtEntropy(s, lowerTemperature, C.B23_T_MAX, true, (T) => T === lowerTemperature ? lowerState : solveRegion3PTBasic(C.P_MAX, T));
|
|
271
|
+
// Region 3 EOS reconstruction adds cancellation beyond boundary inversion roundoff.
|
|
272
|
+
if (h > boundary.enthalpy + boundary.tolerance + 1e-9)
|
|
273
|
+
return null;
|
|
274
|
+
return resolveBackwardCandidates([
|
|
275
|
+
() => solveHSInRegion(h, s, Region.Region3, undefined, false),
|
|
276
|
+
() => solveHSInRegion(h, s, Region.Region3, boundary.state, false),
|
|
277
|
+
], [
|
|
278
|
+
{ label: 'enthalpy', expected: h },
|
|
279
|
+
{ label: 'entropy', expected: s },
|
|
280
|
+
], { solverName: 'solveHS' });
|
|
281
|
+
}
|
|
282
|
+
function hSatLow(s) {
|
|
283
|
+
return HS_TRIPLE_LIQUID.enthalpy +
|
|
284
|
+
(s - HS_TRIPLE_LIQUID.entropy) /
|
|
285
|
+
(HS_TRIPLE_VAPOR.entropy - HS_TRIPLE_LIQUID.entropy) *
|
|
286
|
+
(HS_TRIPLE_VAPOR.enthalpy - HS_TRIPLE_LIQUID.enthalpy);
|
|
287
|
+
}
|
|
288
|
+
function phaseState(endpoints, phase) {
|
|
289
|
+
return phase === 'liquid' ? endpoints.liquid : endpoints.vapor;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Invert one saturation endpoint's monotone entropy curve without scanning.
|
|
293
|
+
* The Clapeyron relation supplies dp_sat/dT, and the Gibbs Maxwell relation
|
|
294
|
+
* supplies the endpoint derivative along that curve.
|
|
295
|
+
*/
|
|
296
|
+
function saturationBoundaryEnthalpyAtEntropy(s, phase) {
|
|
297
|
+
let lower = C.Tt;
|
|
298
|
+
let upper = HS_SATURATION_INVERSION_UPPER_T;
|
|
299
|
+
const tripleEntropy = phaseState(HS_TRIPLE_POINT_ENDPOINTS, phase).entropy;
|
|
300
|
+
const criticalEntropy = C.R3_S_CRT;
|
|
301
|
+
const fraction = phase === 'liquid'
|
|
302
|
+
? (s - tripleEntropy) / (criticalEntropy - tripleEntropy)
|
|
303
|
+
: (tripleEntropy - s) / (tripleEntropy - criticalEntropy);
|
|
304
|
+
const boundedFraction = Math.max(0, Math.min(1, fraction));
|
|
305
|
+
let temperature = lower + boundedFraction * (upper - lower);
|
|
306
|
+
for (let iteration = 0; iteration < 12; iteration += 1) {
|
|
307
|
+
const endpoints = saturationEndpointsAtTemperature(temperature);
|
|
308
|
+
const state = phaseState(endpoints, phase);
|
|
309
|
+
const residual = state.entropy - s;
|
|
310
|
+
if (Math.abs(residual) <= 1e-12 * Math.max(1, Math.abs(s))) {
|
|
311
|
+
return { enthalpy: state.enthalpy, exact: true };
|
|
312
|
+
}
|
|
313
|
+
const residualIncreasesWithTemperature = phase === 'liquid';
|
|
314
|
+
if ((residual < 0) === residualIncreasesWithTemperature)
|
|
315
|
+
lower = temperature;
|
|
316
|
+
else
|
|
317
|
+
upper = temperature;
|
|
318
|
+
if (upper - lower <= 10 * C.REGION4_TEMPERATURE_TOLERANCE) {
|
|
319
|
+
// The 623.15 K formula seam can close the bracket without an entropy root.
|
|
320
|
+
return { enthalpy: state.enthalpy, exact: false };
|
|
321
|
+
}
|
|
322
|
+
const entropySpan = endpoints.vapor.entropy - endpoints.liquid.entropy;
|
|
323
|
+
const volumeSpan = endpoints.vapor.specificVolume - endpoints.liquid.specificVolume;
|
|
324
|
+
const dpSatDT = entropySpan / (1000 * volumeSpan);
|
|
325
|
+
const cp = state.cp;
|
|
326
|
+
const alpha = state.isobaricExpansion;
|
|
327
|
+
const derivative = cp === null || alpha === null
|
|
328
|
+
? Number.NaN
|
|
329
|
+
: cp / temperature - 1000 * state.specificVolume * alpha * dpSatDT;
|
|
330
|
+
const candidate = temperature - residual / derivative;
|
|
331
|
+
temperature = Number.isFinite(candidate) && candidate > lower && candidate < upper
|
|
332
|
+
? candidate
|
|
333
|
+
: (lower + upper) / 2;
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
/** Replace the supplementary polynomial only in its narrow error band. */
|
|
338
|
+
function refinedSaturationBoundaryEnthalpy(h, s, approximateBoundaryH) {
|
|
339
|
+
if (Math.abs(h - approximateBoundaryH) > HS_SATURATION_BOUNDARY_CONFIRMATION_BAND) {
|
|
340
|
+
return { enthalpy: approximateBoundaryH, exact: false };
|
|
341
|
+
}
|
|
342
|
+
const phase = s <= C.R3_S_CRT ? 'liquid' : 'vapor';
|
|
343
|
+
let boundary;
|
|
344
|
+
try {
|
|
345
|
+
boundary = saturationBoundaryEnthalpyAtEntropy(s, phase);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (error instanceof IF97Error)
|
|
349
|
+
return { enthalpy: approximateBoundaryH, exact: false };
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
return boundary === null
|
|
353
|
+
? { enthalpy: approximateBoundaryH, exact: false }
|
|
354
|
+
: { enthalpy: boundary.enthalpy + HS_SATURATION_BOUNDARY_ENTHALPY_TOLERANCE, exact: boundary.exact };
|
|
355
|
+
}
|
|
356
|
+
function confirmedRegion4BoundaryState(h, s, approximateBoundaryH) {
|
|
357
|
+
if (Math.abs(h - approximateBoundaryH) > HS_SATURATION_BOUNDARY_CONFIRMATION_BAND) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
return tryRegion4HSState(h, s) !== null;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (error instanceof IF97Error)
|
|
365
|
+
return false;
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function r2R5BoundaryEnthalpy(s, lowEntropy, highEntropy, stateAtPressure) {
|
|
370
|
+
if (s < lowEntropy || s > highEntropy)
|
|
371
|
+
return null;
|
|
372
|
+
return hsBoundaryAtEntropy(s, C.P_MIN, C.R5_P_MAX, false, stateAtPressure);
|
|
373
|
+
}
|
|
374
|
+
function classifyRegion5HS(h, s, solved) {
|
|
375
|
+
if (!(HS_R5_LOWER.entropy < s && s <= HS_R5_UPPER.entropy)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const region2Boundary = r2R5BoundaryEnthalpy(s, HS_R2_R5_PMAX.entropy, HS_R2_PMIN_TMAX.entropy, (p) => region2(p, C.R2_T_MAX));
|
|
379
|
+
const region5Boundary = r2R5BoundaryEnthalpy(s, HS_R5_LOWER.entropy, HS_R5_PMIN_TMIN.entropy, (p) => region5(p, C.R5_T_MIN));
|
|
380
|
+
const region2BoundaryH = region2Boundary?.enthalpy ?? null;
|
|
381
|
+
const region5BoundaryH = region5Boundary?.enthalpy ?? null;
|
|
382
|
+
const checkedRegion5 = () => {
|
|
383
|
+
const isobar = s <= HS_R5_PMAX_TMAX.entropy;
|
|
384
|
+
const upper = hsBoundaryAtEntropy(s, isobar ? C.R5_T_MIN : C.P_MIN, isobar ? C.R5_T_MAX : C.R5_P_MAX, isobar, (value) => region5(isobar ? C.R5_P_MAX : value, isobar ? value : C.R5_T_MAX));
|
|
385
|
+
const maximum = upper.enthalpy + upper.tolerance;
|
|
386
|
+
if (h > maximum && region2BoundaryH !== null &&
|
|
387
|
+
h <= region2BoundaryH + HS_R25_SEAM_RELATIVE_TOLERANCE * Math.max(1, Math.abs(region2BoundaryH))) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const lower = region5BoundaryH === null
|
|
391
|
+
? hsBoundaryAtEntropy(s, C.R5_T_MIN, C.R5_T_MAX, true, (T) => region5(C.P_MIN, T))
|
|
392
|
+
: null;
|
|
393
|
+
const minimum = lower === null ? region5BoundaryH : lower.enthalpy - lower.tolerance;
|
|
394
|
+
if (h > maximum || (lower !== null && h < minimum)) {
|
|
395
|
+
throw new OutOfRangeError(`Enthalpy in Region 5 at s=${s}`, h, minimum, maximum);
|
|
396
|
+
}
|
|
397
|
+
return Region.Region5;
|
|
398
|
+
};
|
|
399
|
+
if (region5Boundary !== null && Math.abs(h - region5Boundary.enthalpy) <= region5Boundary.tolerance) {
|
|
400
|
+
try {
|
|
401
|
+
const candidate = solveHSInRegion(h, s, Region.Region5);
|
|
402
|
+
if (candidate.temperature > C.R5_T_MIN) {
|
|
403
|
+
const region = checkedRegion5();
|
|
404
|
+
if (region !== null && solved !== undefined)
|
|
405
|
+
solved.state = candidate;
|
|
406
|
+
return region;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (!(error instanceof IF97Error))
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (region2BoundaryH === null) {
|
|
415
|
+
return region5BoundaryH === null || h > region5BoundaryH
|
|
416
|
+
? checkedRegion5()
|
|
417
|
+
: null;
|
|
418
|
+
}
|
|
419
|
+
if (region5BoundaryH === null) {
|
|
420
|
+
return h > region2BoundaryH ? checkedRegion5() : null;
|
|
421
|
+
}
|
|
422
|
+
const tolerance = HS_R25_SEAM_RELATIVE_TOLERANCE * Math.max(1, Math.abs(h), Math.abs(region2BoundaryH), Math.abs(region5BoundaryH));
|
|
423
|
+
if (region5BoundaryH > region2BoundaryH + tolerance) {
|
|
424
|
+
if (h > region5BoundaryH)
|
|
425
|
+
return checkedRegion5();
|
|
426
|
+
if (h > region2BoundaryH + tolerance)
|
|
427
|
+
return -1;
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
if (region2BoundaryH > region5BoundaryH + tolerance) {
|
|
431
|
+
if (h > region2BoundaryH + tolerance)
|
|
432
|
+
return checkedRegion5();
|
|
433
|
+
if (h <= region5BoundaryH + tolerance)
|
|
434
|
+
return null;
|
|
435
|
+
// The independently fitted Region 2 and 5 equations overlap slightly at
|
|
436
|
+
// 1073.15 K. H-S is non-unique there; choose the branch nearest its seam.
|
|
437
|
+
return h <= (region2BoundaryH + region5BoundaryH) / 2
|
|
438
|
+
? checkedRegion5()
|
|
439
|
+
: null;
|
|
440
|
+
}
|
|
441
|
+
return h > region2BoundaryH + tolerance ? checkedRegion5() : null;
|
|
442
|
+
}
|
|
443
|
+
/** Share HS classification, throwing range errors only for proven envelope violations. */
|
|
444
|
+
export function classifyRegionHS(h, s, solved) {
|
|
445
|
+
const accept = (state) => {
|
|
446
|
+
if (solved !== undefined)
|
|
447
|
+
solved.state = state;
|
|
448
|
+
return state.region;
|
|
449
|
+
};
|
|
450
|
+
const seam = findHSSeam(h, s);
|
|
451
|
+
if (seam !== null) {
|
|
452
|
+
const candidate = solveHSSeam(h, s, seam);
|
|
453
|
+
return candidate === null ? -1 : accept(candidate);
|
|
454
|
+
}
|
|
455
|
+
// Move both inverse roots inside the forbidden gap before accounting for near-tangent roundoff.
|
|
456
|
+
const gapEntropy = s + 4 * HS_BOUNDARY_ENTROPY_TOLERANCE;
|
|
457
|
+
if (s >= HS_R1_PMIN_TMIN.entropy && gapEntropy < HS_R1_TMIN_SMAX.entropy) {
|
|
458
|
+
const left = hsBoundaryAtEntropy(gapEntropy, C.P_MIN, HS_R1_TMIN_SMAX.pressure, false, (p) => region1(p, C.T_MIN));
|
|
459
|
+
const leftMaximum = left.enthalpy + left.tolerance;
|
|
460
|
+
if (h > leftMaximum) {
|
|
461
|
+
const right = hsBoundaryAtEntropy(gapEntropy, HS_R1_TMIN_SMAX.pressure, C.P_MAX, false, (p) => region1(p, C.T_MIN));
|
|
462
|
+
const rightMinimum = right.enthalpy - right.tolerance;
|
|
463
|
+
if (h < rightMinimum) {
|
|
464
|
+
const error = h - leftMaximum <= rightMinimum - h
|
|
465
|
+
? new OutOfRangeError(`Enthalpy on the low-pressure branch at s=${s}`, h, C.H_MIN, leftMaximum)
|
|
466
|
+
: new OutOfRangeError(`Enthalpy on the high-pressure branch at s=${s}`, h, rightMinimum, h1UpperBoundAtPmax(s));
|
|
467
|
+
error.message += `; forbidden enthalpy interval (${leftMaximum}, ${rightMinimum}) at T=${C.T_MIN} K`;
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (withinClosedLowerBound(s, C.S_MIN, 'entropy') && s < HS_TRIPLE_LIQUID.entropy) {
|
|
473
|
+
const hmax = h1UpperBoundAtPmax(s);
|
|
474
|
+
if (s < HS_R1_PMIN_TMIN.entropy) {
|
|
475
|
+
const lower = hsBoundaryAtEntropy(s, C.P_MIN, C.P_MAX, false, (p) => region1(p, C.T_MIN));
|
|
476
|
+
if (h < lower.enthalpy - lower.tolerance) {
|
|
477
|
+
throw new OutOfRangeError(`Enthalpy at s=${s}`, h, lower.enthalpy - lower.tolerance, hmax);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (h > hmax && !isClose(h, hmax)) {
|
|
481
|
+
throw new OutOfRangeError(`Enthalpy upper envelope at s=${s}`, h, C.H_MIN, hmax);
|
|
482
|
+
}
|
|
483
|
+
return withinClosedLowerBound(h, C.H_MIN, 'enthalpy') && (h <= hmax || isClose(h, hmax)) ? Region.Region1 : -1;
|
|
484
|
+
}
|
|
485
|
+
if (s >= HS_TRIPLE_LIQUID.entropy && s <= HS_TRIPLE_VAPOR.entropy &&
|
|
486
|
+
isClose(h, hSatLow(s)) && confirmedRegion4BoundaryState(h, s, hSatLow(s)) === true) {
|
|
487
|
+
return Region.Region4;
|
|
488
|
+
}
|
|
489
|
+
if (s >= HS_TRIPLE_LIQUID.entropy && s <= HS_TRIPLE_VAPOR.entropy) {
|
|
490
|
+
const lower = hSatLow(s);
|
|
491
|
+
const hmin = lower - C.Tt * 1e-12 - 8 * Number.EPSILON * Math.max(1, Math.abs(lower));
|
|
492
|
+
if (h < hmin) {
|
|
493
|
+
throw new OutOfRangeError(`Enthalpy lower envelope at s=${s}`, h, hmin, C.H_MAX);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const highTemperatureRegion = classifyRegion5HS(h, s, solved);
|
|
497
|
+
if (highTemperatureRegion !== null)
|
|
498
|
+
return highTemperatureRegion;
|
|
499
|
+
if (s >= HS_R2_PMIN_TMIN.entropy && s <= HS_R2_PMIN_TMAX.entropy) {
|
|
500
|
+
const lower = hsBoundaryAtEntropy(s, C.T_MIN, C.R2_T_MAX, true, (T) => region2(C.P_MIN, T));
|
|
501
|
+
if (h < lower.enthalpy - lower.tolerance) {
|
|
502
|
+
throw new OutOfRangeError(`Enthalpy lower envelope at s=${s}`, h, lower.enthalpy - lower.tolerance, C.H_MAX);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (s > HS_S13 && s <= HS_R2_PMIN_TMAX.entropy) {
|
|
506
|
+
let hmax = h23UpperBoundAtEntropy(s);
|
|
507
|
+
if (s >= Math.max(HS_R2_R5_PMAX.entropy, HS_R5_LOWER.entropy) &&
|
|
508
|
+
s <= Math.min(HS_R2_PMIN_TMAX.entropy, HS_R5_PMIN_TMIN.entropy) &&
|
|
509
|
+
HS_R5_LOWER.enthalpy < h && h <= HS_R5_UPPER.enthalpy) {
|
|
510
|
+
// Preserve the existing Region 2/5 seam comparison window and branch choice.
|
|
511
|
+
hmax += HS_R25_SEAM_RELATIVE_TOLERANCE * Math.max(1, Math.abs(hmax));
|
|
512
|
+
}
|
|
513
|
+
if (h > hmax) {
|
|
514
|
+
throw new OutOfRangeError(`Enthalpy upper envelope at s=${s}`, h, C.H_MIN, hmax);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Zone 1: smin <= s <= s13 (Region 1 / Region 4)
|
|
518
|
+
if (s >= HS_TRIPLE_LIQUID.entropy && s <= HS_S13) {
|
|
519
|
+
const hmin = hSatLow(s);
|
|
520
|
+
const approximateHs = h1Sat(s);
|
|
521
|
+
const boundary = refinedSaturationBoundaryEnthalpy(h, s, approximateHs);
|
|
522
|
+
const hs = boundary.enthalpy;
|
|
523
|
+
const hmax = h1UpperBoundAtPmax(s);
|
|
524
|
+
if (h > hmax && !isClose(h, hmax)) {
|
|
525
|
+
const candidate = region3LowerHSCandidate(h, s);
|
|
526
|
+
if (candidate !== null)
|
|
527
|
+
return accept(candidate);
|
|
528
|
+
throw new OutOfRangeError(`Enthalpy upper envelope at s=${s}`, h, C.H_MIN, hmax);
|
|
529
|
+
}
|
|
530
|
+
const confirmedRegion4 = boundary.exact && h > hs
|
|
531
|
+
? false
|
|
532
|
+
: confirmedRegion4BoundaryState(h, s, approximateHs);
|
|
533
|
+
if (confirmedRegion4 === true)
|
|
534
|
+
return Region.Region4;
|
|
535
|
+
if (confirmedRegion4 === false && h >= hmin && (h <= hmax || isClose(h, hmax)))
|
|
536
|
+
return Region.Region1;
|
|
537
|
+
if (h >= hmin && h <= hs)
|
|
538
|
+
return Region.Region4;
|
|
539
|
+
if (h >= hs && (h <= hmax || isClose(h, hmax)))
|
|
540
|
+
return Region.Region1;
|
|
541
|
+
}
|
|
542
|
+
// Zone 2: s13 < s <= s13s (Region 1 / Region 3 / Region 4)
|
|
543
|
+
else if (s > HS_S13 && s <= HS_S13S) {
|
|
544
|
+
const hmin = hSatLow(s);
|
|
545
|
+
const approximateHs = h1Sat(s);
|
|
546
|
+
const boundary = refinedSaturationBoundaryEnthalpy(h, s, approximateHs);
|
|
547
|
+
const hs = boundary.enthalpy;
|
|
548
|
+
const h13 = h13Boundary(s);
|
|
549
|
+
const confirmedRegion4 = boundary.exact && h > hs
|
|
550
|
+
? false
|
|
551
|
+
: confirmedRegion4BoundaryState(h, s, approximateHs);
|
|
552
|
+
if (confirmedRegion4 === true)
|
|
553
|
+
return Region.Region4;
|
|
554
|
+
if (confirmedRegion4 === false && h >= hmin) {
|
|
555
|
+
return h < h13 ? Region.Region1 : Region.Region3;
|
|
556
|
+
}
|
|
557
|
+
if (h >= hmin && h <= hs)
|
|
558
|
+
return Region.Region4;
|
|
559
|
+
if (h >= hs && h < h13)
|
|
560
|
+
return Region.Region1;
|
|
561
|
+
if (h >= h13)
|
|
562
|
+
return Region.Region3;
|
|
563
|
+
}
|
|
564
|
+
// Zone 3: s13s < s <= R3_CRT_S (Region 3 / Region 4)
|
|
565
|
+
else if (s > HS_S13S && s <= C.R3_S_CRT) {
|
|
566
|
+
const hmin = hSatLow(s);
|
|
567
|
+
const hs = h3aSat(s);
|
|
568
|
+
const confirmedRegion4 = confirmedRegion4BoundaryState(h, s, hs);
|
|
569
|
+
if (confirmedRegion4 === true)
|
|
570
|
+
return Region.Region4;
|
|
571
|
+
if (confirmedRegion4 === false && h >= hmin)
|
|
572
|
+
return Region.Region3;
|
|
573
|
+
if (h >= hmin && h <= hs)
|
|
574
|
+
return Region.Region4;
|
|
575
|
+
if (h >= hs)
|
|
576
|
+
return Region.Region3;
|
|
577
|
+
}
|
|
578
|
+
// Zone 4: R3_CRT_S < s < B23_S_CURVE_MIN (Region 3 / Region 4)
|
|
579
|
+
else if (s > C.R3_S_CRT && s < C.B23_S_CURVE_MIN) {
|
|
580
|
+
const hmin = hSatLow(s);
|
|
581
|
+
const hs = h2c3bSat(s);
|
|
582
|
+
const confirmedRegion4 = confirmedRegion4BoundaryState(h, s, hs);
|
|
583
|
+
if (confirmedRegion4 === true)
|
|
584
|
+
return Region.Region4;
|
|
585
|
+
if (confirmedRegion4 === false && h >= hmin)
|
|
586
|
+
return Region.Region3;
|
|
587
|
+
if (h >= hmin && h <= hs)
|
|
588
|
+
return Region.Region4;
|
|
589
|
+
if (h >= hs)
|
|
590
|
+
return Region.Region3;
|
|
591
|
+
}
|
|
592
|
+
// Zone 5: B23_S_CURVE_MIN <= s < B23_S_CURVE_MAX (Region 2 / Region 3 / Region 4)
|
|
593
|
+
else if (s >= C.B23_S_CURVE_MIN && s < C.B23_S_CURVE_MAX) {
|
|
594
|
+
const hmin = hSatLow(s);
|
|
595
|
+
const approximateHs = h2c3bSat(s);
|
|
596
|
+
const hs = refinedSaturationBoundaryEnthalpy(h, s, approximateHs).enthalpy;
|
|
597
|
+
const h23max = HS_R2_B23_MAX.enthalpy;
|
|
598
|
+
const h23min = HS_R2_B23_MIN.enthalpy;
|
|
599
|
+
const confirmedRegion4 = confirmedRegion4BoundaryState(h, s, approximateHs);
|
|
600
|
+
if (confirmedRegion4 === true)
|
|
601
|
+
return Region.Region4;
|
|
602
|
+
if (confirmedRegion4 === false && h >= hmin) {
|
|
603
|
+
for (const region of [Region.Region2, Region.Region3]) {
|
|
604
|
+
try {
|
|
605
|
+
return accept(solveHSInRegion(h, s, region));
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (!(error instanceof IF97Error))
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (hmin <= h && h <= hs)
|
|
614
|
+
return Region.Region4;
|
|
615
|
+
if (hs <= h && h < h23min)
|
|
616
|
+
return Region.Region3;
|
|
617
|
+
if (h23min <= h && h < h23max) {
|
|
618
|
+
// Discriminate R2 vs R3 using B23 boundary
|
|
619
|
+
const Tb23 = Math.max(C.R2_T_MIN, Math.min(tB23hs(h, s), C.B23_T_MAX));
|
|
620
|
+
const Pb23 = boundary23_T_to_P(Tb23);
|
|
621
|
+
const Phs = backward2cPhs(h, s);
|
|
622
|
+
if (Phs <= Pb23)
|
|
623
|
+
return Region.Region2;
|
|
624
|
+
return Region.Region3;
|
|
625
|
+
}
|
|
626
|
+
if (h23max <= h)
|
|
627
|
+
return Region.Region2;
|
|
628
|
+
}
|
|
629
|
+
// Zone 6: B23_S_CURVE_MAX <= s < R2_S_CRT (Region 2 / Region 4)
|
|
630
|
+
else if (s >= C.B23_S_CURVE_MAX && s < C.R2_S_CRT) {
|
|
631
|
+
const hmin = hSatLow(s);
|
|
632
|
+
const hs = refinedSaturationBoundaryEnthalpy(h, s, h2c3bSat(s)).enthalpy;
|
|
633
|
+
if (hmin <= h && h <= hs)
|
|
634
|
+
return Region.Region4;
|
|
635
|
+
if (hs <= h)
|
|
636
|
+
return Region.Region2;
|
|
637
|
+
}
|
|
638
|
+
// Zone 7: R2_S_CRT <= s < sTPmax (Region 2 / Region 4, use h2abSat)
|
|
639
|
+
else if (s >= C.R2_S_CRT && s < HS_S_TP_MAX) {
|
|
640
|
+
const hmin = hSatLow(s);
|
|
641
|
+
const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s)).enthalpy;
|
|
642
|
+
if (hmin <= h && h <= hs)
|
|
643
|
+
return Region.Region4;
|
|
644
|
+
if (hs <= h)
|
|
645
|
+
return Region.Region2;
|
|
646
|
+
}
|
|
647
|
+
// Zone 8: sTPmax <= s < s2ab (Region 2 / Region 4)
|
|
648
|
+
else if (s >= HS_S_TP_MAX && s < HS_S2AB) {
|
|
649
|
+
const hmin = hSatLow(s);
|
|
650
|
+
const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s)).enthalpy;
|
|
651
|
+
if (hmin <= h && h <= hs)
|
|
652
|
+
return Region.Region4;
|
|
653
|
+
if (hs <= h)
|
|
654
|
+
return Region.Region2;
|
|
655
|
+
}
|
|
656
|
+
// Zone 9: s2ab <= s < s4v (Region 2 / Region 4)
|
|
657
|
+
else if (s >= HS_S2AB && s < HS_TRIPLE_VAPOR.entropy) {
|
|
658
|
+
const hmin = hSatLow(s);
|
|
659
|
+
const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s)).enthalpy;
|
|
660
|
+
if (hmin <= h && h <= hs)
|
|
661
|
+
return Region.Region4;
|
|
662
|
+
if (hs <= h)
|
|
663
|
+
return Region.Region2;
|
|
664
|
+
}
|
|
665
|
+
// Zone 10: s4v <= s <= smax (Region 2 only, superheated low-P)
|
|
666
|
+
else if (s >= HS_TRIPLE_VAPOR.entropy && s <= HS_R2_PMIN_TMAX.entropy) {
|
|
667
|
+
const hmin = HS_R2_PMIN_TMIN.enthalpy;
|
|
668
|
+
if (hmin <= h)
|
|
669
|
+
return Region.Region2;
|
|
670
|
+
if (h < hmin) {
|
|
671
|
+
throw new OutOfRangeError(`Enthalpy lower envelope at s=${s}`, h, hmin, C.H_MAX);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return -1;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Detect the IAPWS-IF97 region for given H and S.
|
|
678
|
+
* Implements the zone-based detection from the IAPWS supplementary release.
|
|
679
|
+
*
|
|
680
|
+
* @param h - Specific enthalpy [kJ/kg]
|
|
681
|
+
* @param s - Specific entropy [kJ/(kg·K)]
|
|
682
|
+
* @returns Region number (1–5) or -1 if out of range
|
|
683
|
+
*/
|
|
684
|
+
export function detectRegionHS(h, s) {
|
|
685
|
+
if (!Number.isFinite(h) || !Number.isFinite(s))
|
|
686
|
+
return -1;
|
|
687
|
+
try {
|
|
688
|
+
return classifyRegionHS(h, s);
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
if (error instanceof OutOfRangeError)
|
|
692
|
+
return -1;
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|