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