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
@@ -0,0 +1,15 @@
1
+ /** Backward H-S state construction within a selected region or formulation seam. */
2
+ import type { BasicProperties } from '../types.js';
3
+ import { Region } from '../types.js';
4
+ import type { FormulationSeamResolution } from '../core/formulation-seams.js';
5
+ export declare function r1Phs(h: number, s: number): number;
6
+ /** Boundary between Region 2a and Region 2b in h-s plane. */
7
+ export declare function b2abBoundary(s: number): number;
8
+ export declare function r2aPhs(h: number, s: number): number;
9
+ export declare function r2bPhs(h: number, s: number): number;
10
+ export declare function r2cPhs(h: number, s: number): number;
11
+ export declare function r3aPhs(h: number, s: number): number;
12
+ export declare function r3bPhs(h: number, s: number): number;
13
+ export declare function solveHSInRegion(h: number, s: number, region: Region | -1, seamInitial?: BasicProperties, validate?: boolean): BasicProperties;
14
+ /** Resolve one proven seam identically for the HS solver and detector. */
15
+ export declare function solveHSSeam(h: number, s: number, seam: FormulationSeamResolution): BasicProperties | null;
@@ -0,0 +1,316 @@
1
+ import { Region, IF97Error } from '../types.js';
2
+ import { NumericalStateError } from '../errors.js';
3
+ import * as C from '../constants.js';
4
+ import { region1 } from '../regions/region1.js';
5
+ import { region2 } from '../regions/region2.js';
6
+ import { region3ByRhoT } from '../regions/region3.js';
7
+ import { region5 } from '../regions/region5.js';
8
+ import { solveRegion3PTBasic } from '../core/region3-pt.js';
9
+ import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
10
+ import { r1BackwardT, r2BackwardT, r3BackwardPHInitial } from './ph.js';
11
+ import { backwardCandidateError, validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
12
+ import { gibbsPropertyDerivatives, helmholtzPropertyDerivatives, } from '../core/thermodynamic-derivatives.js';
13
+ import { tryRegion4HSState } from '../saturation/region4-hs.js';
14
+ import { BACKWARD_HS_NEWTON_TOLERANCE, backwardConstraintTolerance } from './tolerances.js';
15
+ import { resolveBackwardCandidates } from './candidate-resolution.js';
16
+ import { nextPositiveFloat } from '../core/floating-point.js';
17
+ // ─── Backward Polynomial Evaluator ─────────────────────────────────────────
18
+ function evalPoly(table, aShift, bShift, a, b) {
19
+ let sum = 0;
20
+ for (const [I, J, N] of table) {
21
+ sum += N * Math.pow(a - aShift, I) * Math.pow(b - bShift, J);
22
+ }
23
+ return sum;
24
+ }
25
+ // ─── Region 1 P(h,s) — 19 coefficients ─────────────────────────────────────
26
+ const R1_PHS = [
27
+ [0, 0, -0.691997014660582], [0, 1, -18.361254878756], [0, 2, -9.28332409297335],
28
+ [0, 4, 65.9639569909906], [0, 5, -16.2060388912024], [0, 6, 450.620017338667],
29
+ [0, 8, 854.68067822417], [0, 14, 6075.23214001162], [1, 0, 32.6487682621856],
30
+ [1, 1, -26.9408844582931], [1, 4, -319.9478483343], [1, 6, -928.35430704332],
31
+ [2, 0, 30.3634537455249], [2, 1, -65.0540422444146], [2, 10, -4309.91316516130],
32
+ [3, 4, -747.512324096068], [4, 1, 730.000345529245], [4, 4, 1142.84032569021],
33
+ [5, 0, -436.407041874559],
34
+ ];
35
+ export function r1Phs(h, s) {
36
+ const mu = h / 3400;
37
+ const sig = s / 7.6;
38
+ return 100 * evalPoly(R1_PHS, -0.05, -0.05, mu, sig);
39
+ }
40
+ // ─── Region 2 P(h,s) — Subregions 2a, 2b, 2c ──────────────────────────────
41
+ /** Boundary between Region 2a and Region 2b in h-s plane. */
42
+ export function b2abBoundary(s) {
43
+ return -3498.98083432139 + 2575.60716905876 * s - 421.073558227969 * s * s + 27.6349063799944 * s * s * s;
44
+ }
45
+ const R2A_PHS = [
46
+ [0, 1, -0.0182575361923032], [0, 3, -0.125229548799536], [0, 6, 0.592290437320145],
47
+ [0, 16, 6.04769706185122], [0, 20, 238.624965444474], [0, 22, -298.639090222922],
48
+ [1, 0, 0.051225081304075], [1, 1, -0.437266515606486], [1, 2, 0.413336902999504],
49
+ [1, 3, -5.16468254574773], [1, 5, -5.57014838445711], [1, 6, 12.8555037824478],
50
+ [1, 10, 11.414410895329], [1, 16, -119.504225652714], [1, 20, -2847.7798596156],
51
+ [1, 22, 4317.57846408006], [2, 3, 1.1289404080265], [2, 16, 1974.09186206319],
52
+ [2, 20, 1516.12444706087], [3, 0, 0.0141324451421235], [3, 2, 0.585501282219601],
53
+ [3, 3, -2.97258075863012], [3, 6, 5.94567314847319], [3, 16, -6236.56565798905],
54
+ [4, 16, 9659.86235133332], [5, 3, 6.81500934948134], [5, 16, -6332.07286824489],
55
+ [6, 3, -5.5891922446576], [7, 1, 0.0400645798472063],
56
+ ];
57
+ export function r2aPhs(h, s) {
58
+ const mu = h / 4200;
59
+ const sig = s / 12;
60
+ const P = evalPoly(R2A_PHS, 0.5, 1.2, mu, sig);
61
+ return 4 * P * P * P * P;
62
+ }
63
+ const R2B_PHS = [
64
+ [0, 0, 0.0801496989929495], [0, 1, -0.543862807146111], [0, 2, 0.337455597421283],
65
+ [0, 4, 8.9055545115745], [0, 8, 313.840736431485], [1, 0, 0.797367065977789],
66
+ [1, 1, -1.2161697355624], [1, 2, 8.72803386937477], [1, 3, -16.9769781757602],
67
+ [1, 5, -186.552827328416], [1, 12, 95115.9274344237], [2, 1, -18.9168510120494],
68
+ [2, 6, -4334.0703719484], [2, 18, 543212633.012715], [3, 0, 0.144793408386013],
69
+ [3, 1, 128.024559637516], [3, 7, -67230.9534071268], [3, 12, 33697238.0095287],
70
+ [4, 1, -586.63419676272], [4, 16, -22140322476.9889], [5, 1, 1716.06668708389],
71
+ [5, 12, -570817595.806302], [6, 1, -3121.09693178482], [6, 8, -2078413.8463301],
72
+ [6, 18, 3056059461577.86], [7, 1, 3221.57004314333], [7, 16, 326810259797.295],
73
+ [8, 1, -1441.04158934487], [8, 3, 410.694867802691], [8, 14, 109077066873.024],
74
+ [8, 18, -24796465425889.3], [12, 10, 1888019068.65134], [14, 16, -123651009018773],
75
+ ];
76
+ export function r2bPhs(h, s) {
77
+ const mu = h / 4100;
78
+ const sig = s / 7.9;
79
+ const P = evalPoly(R2B_PHS, 0.6, 1.01, mu, sig);
80
+ return 100 * P * P * P * P;
81
+ }
82
+ const R2C_PHS = [
83
+ [0, 0, 0.112225607199012], [0, 1, -3.39005953606712], [0, 2, -32.0503911730094],
84
+ [0, 3, -197.5973051049], [0, 4, -407.693861553446], [0, 8, 13294.3775222331],
85
+ [1, 0, 1.70846839774007], [1, 2, 37.3694198142245], [1, 5, 3581.44365815434],
86
+ [1, 8, 423014.446424664], [1, 14, -751071025.760063], [2, 2, 52.3446127607898],
87
+ [2, 3, -228.351290812417], [2, 7, -960652.417056937], [2, 10, -80705929.2526074],
88
+ [2, 18, 1626980172256.69], [3, 0, 0.772465073604171], [3, 5, 46392.9973837746],
89
+ [3, 8, -13731788.5134128], [3, 16, 1704703926305.12], [3, 18, -25110462818730.8],
90
+ [4, 18, 31774883083552.0], [5, 1, 53.8685623675312], [5, 4, -55308.9094625169],
91
+ [5, 6, -1028615.22421405], [5, 14, 2042494187562.34], [6, 8, 273918446.626977],
92
+ [6, 18, -2.63963146312685e15], [10, 7, -1078908541.08088], [12, 7, -29649262098.0124],
93
+ [16, 10, -1.11754907323424e15],
94
+ ];
95
+ export function r2cPhs(h, s) {
96
+ const mu = h / 3500;
97
+ const sig = s / 5.9;
98
+ const P = evalPoly(R2C_PHS, 0.7, 1.1, mu, sig);
99
+ return 100 * P * P * P * P;
100
+ }
101
+ function r2Phs(h, s) {
102
+ const hBound = b2abBoundary(s);
103
+ // Region 2 h-s subregion selection: 2a below the h(s) boundary,
104
+ // otherwise split 2b/2c at the published entropy crossover.
105
+ if (h <= hBound)
106
+ return r2aPhs(h, s);
107
+ if (s >= C.R2_S_CRT)
108
+ return r2bPhs(h, s);
109
+ return r2cPhs(h, s);
110
+ }
111
+ // ─── Region 3 P(h,s) — Subregions 3a, 3b ───────────────────────────────────
112
+ const R3A_PHS = [
113
+ [0, 0, 7.70889828326934], [0, 1, -26.0835009128688], [0, 5, 267.416218930389],
114
+ [1, 0, 17.2221089496844], [1, 3, -293.54233214597], [1, 4, 614.135601882478],
115
+ [1, 8, -61056.2757725674], [1, 14, -65127225.1118219], [2, 6, 73591.9313521937],
116
+ [2, 16, -11664650591.4191], [3, 0, 35.5267086434461], [3, 2, -596.144543825955],
117
+ [3, 3, -475.842430145708], [4, 0, 69.6781965359503], [4, 1, 335.674250377312],
118
+ [4, 4, 25052.6809130882], [4, 5, 146997.380630766], [5, 28, 5.38069315091534e19],
119
+ [6, 28, 1.43619827291346e21], [7, 24, 3.64985866165994e19], [8, 1, -2547.41561156775],
120
+ [10, 32, 2.40120197096563e27], [10, 36, -3.93847464679496e29], [14, 22, 1.47073407024852e24],
121
+ [18, 28, -4.26391250432059e31], [20, 36, 1.94509340621077e38], [22, 16, 6.66212132114896e23],
122
+ [22, 28, 7.06777016552858e33], [24, 36, 1.75563621975576e41], [28, 16, 1.08408607429124e28],
123
+ [28, 36, 7.30872705175151e43], [32, 10, 1.5914584739887e24], [32, 28, 3.77121605943324e40],
124
+ ];
125
+ export function r3aPhs(h, s) {
126
+ const mu = h / 2300;
127
+ const sig = s / 4.4;
128
+ return 99 * evalPoly(R3A_PHS, 1.01, 0.75, mu, sig);
129
+ }
130
+ const R3B_PHS = [
131
+ [-12, 2, 1.25244360717979e-13], [-12, 10, -0.0126599322553713], [-12, 12, 5.06878030140626],
132
+ [-12, 14, 31.7847171154202], [-12, 20, -391041.161399932], [-10, 2, -9.75733406392044e-11],
133
+ [-10, 10, -18.6312419488279], [-10, 14, 510.973543414101], [-10, 18, 373847.005822362],
134
+ [-8, 2, 2.99804024666572e-8], [-8, 8, 20.0544393820342], [-6, 2, -4.98030487662829e-6],
135
+ [-6, 6, -10.230180636003], [-6, 7, 55.2819126990325], [-6, 8, -206.211367510878],
136
+ [-5, 10, -7940.12232324823], [-4, 4, 7.82248472028153], [-4, 5, -58.6544326902468],
137
+ [-4, 8, 3550.73647696481], [-3, 1, -1.15303107290162e-4], [-3, 3, -1.75092403171802],
138
+ [-3, 5, 257.98168774816], [-3, 6, -727.048374179467], [-2, 0, 1.21644822609198e-4],
139
+ [-2, 1, 0.0393137871762692], [-1, 0, 7.04181005909296e-3], [0, 3, -82.910820069811],
140
+ [2, 0, -0.26517881813125], [2, 1, 13.7531682453991], [5, 0, -52.2394090753046],
141
+ [6, 1, 2405.56298941048], [8, 1, -22736.1631268929], [10, 1, 89074.6343932567],
142
+ [14, 3, -23923456.5822486], [14, 7, 5687958081.29714],
143
+ ];
144
+ export function r3bPhs(h, s) {
145
+ const mu = h / 2800;
146
+ const sig = s / 5.3;
147
+ return 16.6 / evalPoly(R3B_PHS, 0.681, 0.792, mu, sig);
148
+ }
149
+ function r3Phs(h, s) {
150
+ if (s <= C.R3_S_CRT)
151
+ return r3aPhs(h, s);
152
+ return r3bPhs(h, s);
153
+ }
154
+ function solveGibbsHS(h, s, initial, stateAt) {
155
+ const hScale = Math.max(1, Math.abs(h));
156
+ const sScale = Math.max(1, Math.abs(s));
157
+ const [p, T] = dampedNewton2D(([candidateP, candidateT]) => {
158
+ const state = stateAt(candidateP, candidateT);
159
+ const derivatives = gibbsPropertyDerivatives(state);
160
+ return {
161
+ residual: [
162
+ (state.enthalpy - h) / hScale,
163
+ (state.entropy - s) / sScale,
164
+ ],
165
+ jacobian: [
166
+ [derivatives.dhDp / hScale, derivatives.dhDT / hScale],
167
+ [derivatives.dsDp / sScale, derivatives.dsDT / sScale],
168
+ ],
169
+ };
170
+ }, initial, {
171
+ // Entropy convergence must also resolve pressure at the P_MIN boundary.
172
+ tolerance: BACKWARD_HS_NEWTON_TOLERANCE,
173
+ isValid: ([candidateP, candidateT]) => candidateP > 0 && candidateT > 0,
174
+ });
175
+ return stateAt(p, T);
176
+ }
177
+ function solveRegion3HS(h, s, rho0, T0) {
178
+ const hScale = Math.max(1, Math.abs(h));
179
+ const sScale = Math.max(1, Math.abs(s));
180
+ const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
181
+ const state = region3ByRhoT(candidateRho, candidateT);
182
+ const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
183
+ return {
184
+ residual: [
185
+ (state.enthalpy - h) / hScale,
186
+ (state.entropy - s) / sScale,
187
+ ],
188
+ jacobian: [
189
+ [derivatives.dhDrho / hScale, derivatives.dhDT / hScale],
190
+ [derivatives.dsDrho / sScale, derivatives.dsDT / sScale],
191
+ ],
192
+ };
193
+ }, [rho0, T0], {
194
+ // Tighter than the 1e-10 default and load-bearing: near the 100 MPa ceiling
195
+ // Region 3 is nearly incompressible, so a residual the default would accept
196
+ // still lands p far enough outside [P_MIN, P_MAX] to fail envelope
197
+ // validation. Covered by the p=100 MPa cases in newton-regressions.test.ts.
198
+ tolerance: BACKWARD_HS_NEWTON_TOLERANCE,
199
+ isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
200
+ });
201
+ return region3ByRhoT(rho, T);
202
+ }
203
+ // ─── Main HS Solver ─────────────────────────────────────────────────────────
204
+ export function solveHSInRegion(h, s, region, seamInitial, validate = true) {
205
+ // Re-evaluating a converged Region 3 seed can round pressure outside its closed boundary.
206
+ if (!validate && seamInitial?.region === region &&
207
+ Math.abs(seamInitial.enthalpy - h) <= BACKWARD_HS_NEWTON_TOLERANCE * Math.max(1, Math.abs(h)) &&
208
+ Math.abs(seamInitial.entropy - s) <= BACKWARD_HS_NEWTON_TOLERANCE * Math.max(1, Math.abs(s))) {
209
+ return seamInitial;
210
+ }
211
+ switch (region) {
212
+ case Region.Region1: {
213
+ const P0 = seamInitial?.pressure ?? r1Phs(h, s);
214
+ const state = solveGibbsHS(h, s, [P0, seamInitial?.temperature ?? r1BackwardT(P0, h)], region1);
215
+ return !validate ? state : validateBackwardState(state, [
216
+ { label: 'enthalpy', expected: h },
217
+ { label: 'entropy', expected: s },
218
+ ], { solverName: 'solveHS', expectedRegion: Region.Region1 });
219
+ }
220
+ case Region.Region2: {
221
+ const P0 = seamInitial?.pressure ?? Math.max(C.P_MIN, Math.min(C.P_MAX, r2Phs(h, s)));
222
+ const state = solveGibbsHS(h, s, [P0, seamInitial?.temperature ?? r2BackwardT(P0, h)], region2);
223
+ return !validate ? state : validateBackwardState(state, [
224
+ { label: 'enthalpy', expected: h },
225
+ { label: 'entropy', expected: s },
226
+ ], { solverName: 'solveHS', expectedRegion: Region.Region2 });
227
+ }
228
+ case Region.Region3: {
229
+ const P0 = seamInitial?.pressure ?? r3Phs(h, s);
230
+ const initial = seamInitial === undefined
231
+ ? r3BackwardPHInitial(P0, h)
232
+ : { rho: 1 / seamInitial.specificVolume, T: seamInitial.temperature };
233
+ const state = solveRegion3HS(h, s, initial.rho, initial.T);
234
+ if (!validate) {
235
+ if (state.pressure > C.P_MAX &&
236
+ state.pressure - C.P_MAX <= backwardConstraintTolerance('pressure', C.P_MAX)) {
237
+ const constraints = [
238
+ { label: 'enthalpy', expected: h, tolerance: 1e-9 },
239
+ { label: 'entropy', expected: s, tolerance: 1e-12 },
240
+ ];
241
+ const options = { solverName: 'solveHS', expectedRegion: Region.Region3 };
242
+ // Inspect the raw EOS before reconstructing only its closed pressure boundary.
243
+ const error = backwardCandidateError({ ...state, pressure: C.P_MAX }, constraints, options);
244
+ if (error instanceof NumericalStateError)
245
+ throw error;
246
+ if (error === null) {
247
+ const projected = solveRegion3PTBasic(C.P_MAX, state.temperature);
248
+ if (backwardCandidateError(projected, constraints, options) === null)
249
+ return projected;
250
+ }
251
+ }
252
+ if (state.temperature >= C.Tc || backwardCandidateError(state, [
253
+ { label: 'enthalpy', expected: h },
254
+ { label: 'entropy', expected: s },
255
+ ], { solverName: 'solveHS', expectedRegion: Region.Region3 }) !== null)
256
+ return state;
257
+ return solveRegion3PTBasic(state.pressure, state.temperature);
258
+ }
259
+ const validated = validateBackwardState(state, [
260
+ { label: 'enthalpy', expected: h },
261
+ { label: 'entropy', expected: s },
262
+ ], { solverName: 'solveHS', expectedRegion: Region.Region3 });
263
+ if (validated.temperature < C.Tc)
264
+ validateRegion3PTReconstruction(validated, 'solveHS');
265
+ return validated;
266
+ }
267
+ case Region.Region4: {
268
+ const state = tryRegion4HSState(h, s);
269
+ if (state === null) {
270
+ throw new IF97Error('solveHS identified Region 4 but failed to construct a valid saturation state');
271
+ }
272
+ return !validate ? state : validateBackwardState(state, [
273
+ { label: 'enthalpy', expected: h },
274
+ { label: 'entropy', expected: s },
275
+ ], { solverName: 'solveHS', expectedRegion: Region.Region4 });
276
+ }
277
+ case Region.Region5: {
278
+ const state = solveGibbsHS(h, s, [seamInitial?.pressure ?? 1, seamInitial?.temperature ?? 1400], region5);
279
+ return !validate ? state : validateBackwardState(state, [
280
+ { label: 'enthalpy', expected: h },
281
+ { label: 'entropy', expected: s },
282
+ ], { solverName: 'solveHS', expectedRegion: Region.Region5 });
283
+ }
284
+ default:
285
+ throw new IF97Error(`Cannot determine region for h=${h} kJ/kg, s=${s} kJ/(kg·K)`);
286
+ }
287
+ }
288
+ /** Resolve one proven seam identically for the HS solver and detector. */
289
+ export function solveHSSeam(h, s, seam) {
290
+ const constraints = [
291
+ { label: 'enthalpy', expected: h },
292
+ { label: 'entropy', expected: s },
293
+ ];
294
+ const options = { solverName: 'solveHS', seam };
295
+ const adjacent = seam.pair.lower.region === seam.state.region ? seam.pair.upper : seam.pair.lower;
296
+ const solveRegion = (region, initial) => {
297
+ if (initial?.region === Region.Region5 && initial.temperature === C.R5_T_MIN) {
298
+ initial = region5(initial.pressure, nextPositiveFloat(C.R5_T_MIN, 1));
299
+ }
300
+ else if (initial?.region === Region.Region3 && initial.temperature === C.R2_T_MIN) {
301
+ initial = solveRegion3PTBasic(initial.pressure, nextPositiveFloat(C.R2_T_MIN, 1));
302
+ }
303
+ else if (initial?.region === Region.Region2 && seam.pair.seam === '23') {
304
+ initial = region2(initial.pressure, nextPositiveFloat(initial.temperature, 1));
305
+ }
306
+ return solveHSInRegion(h, s, region, initial, false);
307
+ };
308
+ return resolveBackwardCandidates([
309
+ () => tryRegion4HSState(h, s),
310
+ () => seam.state.enthalpy === h && seam.state.entropy === s ? seam.state : null,
311
+ () => solveRegion(seam.state.region),
312
+ () => solveRegion(adjacent.region),
313
+ () => solveRegion(seam.state.region, seam.state),
314
+ () => solveRegion(adjacent.region, adjacent),
315
+ ], constraints, options);
316
+ }
@@ -7,204 +7,12 @@
7
7
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations p(h,s)
8
8
  */
9
9
  import { Region, IF97Error, OutOfRangeError } from '../types.js';
10
+ import { RegionMismatchError } from '../errors.js';
10
11
  import * as C from '../constants.js';
11
- import { region1 } from '../regions/region1.js';
12
- import { region2 } from '../regions/region2.js';
13
- import { region3ByRhoT } from '../regions/region3.js';
14
- import { region5 } from '../regions/region5.js';
15
- import { assertFiniteNumber } from '../core/input-validation.js';
16
- import { detectRegionHS } from '../core/region-detector.js';
17
- import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
18
- import { r1BackwardT, r2BackwardT, r3BackwardPHInitial } from './ph.js';
19
- import { validateBackwardState } from './solution-validation.js';
20
- import { gibbsPropertyDerivatives, helmholtzPropertyDerivatives, } from './thermodynamic-derivatives.js';
21
- import { tryRegion4HSState, } from '../saturation/region4-hs.js';
22
- import { assertCriticalRegion4HSInput, } from '../saturation/region4-boundaries.js';
23
- // ─── Backward Polynomial Evaluator ─────────────────────────────────────────
24
- function evalPoly(table, aShift, bShift, a, b) {
25
- let sum = 0;
26
- for (const [I, J, N] of table) {
27
- sum += N * Math.pow(a - aShift, I) * Math.pow(b - bShift, J);
28
- }
29
- return sum;
30
- }
31
- // ─── Region 1 P(h,s) — 19 coefficients ─────────────────────────────────────
32
- const R1_PHS = [
33
- [0, 0, -0.691997014660582], [0, 1, -18.361254878756], [0, 2, -9.28332409297335],
34
- [0, 4, 65.9639569909906], [0, 5, -16.2060388912024], [0, 6, 450.620017338667],
35
- [0, 8, 854.68067822417], [0, 14, 6075.23214001161], [1, 0, 32.6487682621856],
36
- [1, 1, -26.9408844582931], [1, 4, -319.9478483343], [1, 6, -928.35430704332],
37
- [2, 0, 30.3634537455249], [2, 1, -65.0540422444146], [2, 10, -4309.1316516130],
38
- [3, 4, -747.512324096068], [4, 1, 730.000345529245], [4, 4, 1142.84032569021],
39
- [5, 0, -436.407041874559],
40
- ];
41
- function r1Phs(h, s) {
42
- const mu = h / 3400;
43
- const sig = s / 7.6;
44
- return 100 * evalPoly(R1_PHS, -0.05, -0.05, mu, sig);
45
- }
46
- // ─── Region 2 P(h,s) — Subregions 2a, 2b, 2c ──────────────────────────────
47
- /** Boundary between Region 2a and Region 2b in h-s plane. */
48
- function b2abBoundary(s) {
49
- return -3498.98083432139 + 2575.60716905876 * s - 421.073558227969 * s * s + 27.6349063799944 * s * s * s;
50
- }
51
- const R2A_PHS = [
52
- [0, 1, -0.0182575361923032], [0, 3, -0.125229548799536], [0, 6, 0.592290437320145],
53
- [0, 16, 6.04769706185122], [0, 20, 238.624965444474], [0, 22, -298.639090222922],
54
- [1, 0, 0.051225081304075], [1, 1, -0.437266515606486], [1, 2, 0.413336902999504],
55
- [1, 3, -5.16468254574773], [1, 5, -5.57014838445711], [1, 6, 12.8555037824478],
56
- [1, 10, 11.414410895329], [1, 16, -119.504225652714], [1, 20, -2847.7798596156],
57
- [1, 22, 4317.57846408006], [2, 3, 1.1289404080265], [2, 16, 1974.09186206319],
58
- [2, 20, 1516.12444706087], [3, 0, 0.0141324451421235], [3, 2, 0.585501282219601],
59
- [3, 3, -2.97258075863012], [3, 6, 5.94567314847319], [3, 16, -6236.56565798905],
60
- [4, 16, 9659.86235133332], [5, 3, 6.81500934948134], [5, 16, -6332.07286824489],
61
- [6, 3, -5.5891922446576], [7, 1, 0.0400645798472063],
62
- ];
63
- function r2aPhs(h, s) {
64
- const mu = h / 4200;
65
- const sig = s / 12;
66
- const P = evalPoly(R2A_PHS, 0.5, 1.2, mu, sig);
67
- return 4 * P * P * P * P;
68
- }
69
- const R2B_PHS = [
70
- [0, 0, 0.0801496989929495], [0, 1, -0.543862807146111], [0, 2, 0.337455597421283],
71
- [0, 4, 8.9055545115745], [0, 8, 313.840736431485], [1, 0, 0.797367065977789],
72
- [1, 1, -1.2161697355624], [1, 2, 8.72803386937477], [1, 3, -16.9769781757602],
73
- [1, 5, -186.552827328416], [1, 12, 95115.9274344237], [2, 1, -18.9168510120494],
74
- [2, 6, -4334.0703719484], [2, 18, 543212633.012715], [3, 0, 0.144793408386013],
75
- [3, 1, 128.024559637516], [3, 7, -67230.9534071268], [3, 12, 33697238.0095287],
76
- [4, 1, -586.63419676272], [4, 16, -22140322476.9889], [5, 1, 1716.06668708389],
77
- [5, 12, -570817595.806302], [6, 1, -3121.09693178482], [6, 8, -2078413.8463301],
78
- [6, 18, 3056059461577.86], [7, 1, 3221.57004314333], [7, 16, 326810259797.295],
79
- [8, 1, -1441.04158934487], [8, 3, 410.694867802691], [8, 14, 109077066873.024],
80
- [8, 18, -24796465425889.3], [12, 10, 1888019068.65134], [14, 16, -123651009018773],
81
- ];
82
- function r2bPhs(h, s) {
83
- const mu = h / 4100;
84
- const sig = s / 7.9;
85
- const P = evalPoly(R2B_PHS, 0.6, 1.01, mu, sig);
86
- return 100 * P * P * P * P;
87
- }
88
- const R2C_PHS = [
89
- [0, 0, 0.112225607199012], [0, 1, -3.39005953606712], [0, 2, -32.0503911730094],
90
- [0, 3, -197.5973051049], [0, 4, -407.693861553446], [0, 8, 13294.3775222331],
91
- [1, 0, 1.70846839774007], [1, 2, 37.3694198142245], [1, 5, 3581.44365815434],
92
- [1, 8, 423014.446424664], [1, 14, -751071025.760063], [2, 2, 52.3446127607898],
93
- [2, 3, -228.351290812417], [2, 7, -960652.417056937], [2, 10, -80705929.2526074],
94
- [2, 18, 1626980172256.69], [3, 0, 0.772465073604171], [3, 5, 46392.9973837746],
95
- [3, 8, -13731788.5134128], [3, 16, 1704703926305.12], [3, 18, -25110462818730.8],
96
- [4, 18, 31774883083552.0], [5, 1, 53.8685623675312], [5, 4, -55308.9094625169],
97
- [5, 6, -1028615.22421405], [5, 14, 2042494187562.34], [6, 8, 273918446.626977],
98
- [6, 18, -2.63963146312685e15], [10, 7, -1078908541.08088], [12, 7, -29649262098.0124],
99
- [16, 10, -1.11754907323424e15],
100
- ];
101
- function r2cPhs(h, s) {
102
- const mu = h / 3500;
103
- const sig = s / 5.9;
104
- const P = evalPoly(R2C_PHS, 0.7, 1.1, mu, sig);
105
- return 100 * P * P * P * P;
106
- }
107
- function r2Phs(h, s) {
108
- const hBound = b2abBoundary(s);
109
- // Region 2 h-s subregion selection: 2a below the h(s) boundary,
110
- // otherwise split 2b/2c at the published entropy crossover.
111
- if (h <= hBound)
112
- return r2aPhs(h, s);
113
- if (s >= C.R2_S_CRT)
114
- return r2bPhs(h, s);
115
- return r2cPhs(h, s);
116
- }
117
- // ─── Region 3 P(h,s) — Subregions 3a, 3b ───────────────────────────────────
118
- const R3A_PHS = [
119
- [0, 0, 7.70889828326934], [0, 1, -26.0835009128688], [0, 5, 267.416218930389],
120
- [1, 0, 17.2221089496844], [1, 3, -293.54233214597], [1, 4, 614.135601882478],
121
- [1, 8, -61056.2757725674], [1, 14, -65127225.1118219], [2, 6, 73591.9313521937],
122
- [2, 16, -11664650591.4191], [3, 0, 35.5267086434461], [3, 2, -596.144543825955],
123
- [3, 3, -475.842430145708], [4, 0, 69.6781965359503], [4, 1, 335.674250377312],
124
- [4, 4, 25052.6809130882], [4, 5, 146997.380630766], [5, 28, 5.38069315091534e19],
125
- [6, 28, 1.43619827291346e21], [7, 24, 3.64985866165990e19], [8, 1, -2547.41561156775],
126
- [10, 32, 2.40120197096563e27], [10, 36, -3.93847464679496e29], [14, 22, 1.47073407024852e24],
127
- [18, 28, -4.26391250432059e31], [20, 36, 1.94509340621077e38], [22, 16, 6.66212132114896e23],
128
- [22, 28, 7.06777016552858e33], [24, 36, 1.75563621975576e41], [28, 16, 1.08408607429124e28],
129
- [28, 36, 7.30872705175151e43], [32, 10, 1.5914584739887e24], [32, 28, 3.77121605943324e40],
130
- ];
131
- function r3aPhs(h, s) {
132
- const mu = h / 2300;
133
- const sig = s / 4.4;
134
- return 99 * evalPoly(R3A_PHS, 1.01, 0.75, mu, sig);
135
- }
136
- const R3B_PHS = [
137
- [-12, 2, 1.25244360717979e-13], [-12, 10, -0.0126599322553713], [-12, 12, 5.06878030140626],
138
- [-12, 14, 31.7847171154202], [-12, 20, -391041.161399932], [-10, 2, -9.75733406392044e-11],
139
- [-10, 10, -18.6312419488279], [-10, 14, 510.973543414101], [-10, 18, 373847.005822362],
140
- [-8, 2, 2.99804024666572e-8], [-8, 8, 20.0544393820342], [-6, 2, -4.98030487662829e-6],
141
- [-6, 6, -10.230180636003], [-6, 7, 55.2819126990325], [-6, 8, -206.211367510878],
142
- [-5, 10, -7940.12232324823], [-4, 4, 7.82248472028153], [-4, 5, -58.6544326902468],
143
- [-4, 8, 3550.73647696481], [-3, 1, -1.15303107290162e-4], [-3, 3, -1.75092403171802],
144
- [-3, 5, 257.98168774816], [-3, 6, -727.048374179467], [-2, 0, 1.21644822609198e-4],
145
- [-2, 1, 0.0393137871762692], [-1, 0, 7.04181005909296e-3], [0, 3, -82.910820069811],
146
- [2, 0, -0.26517881813125], [2, 1, 13.7531682453991], [5, 0, -52.3940907530460],
147
- [6, 1, 2405.56298941048], [8, 1, -22736.1631268929], [10, 1, 89074.6343932567],
148
- [14, 3, -23923456.5822486], [14, 7, 5687958081.29714],
149
- ];
150
- function r3bPhs(h, s) {
151
- const mu = h / 2800;
152
- const sig = s / 5.3;
153
- return 16.6 / evalPoly(R3B_PHS, 0.681, 0.792, mu, sig);
154
- }
155
- function r3Phs(h, s) {
156
- if (s <= C.R3_S_CRT)
157
- return r3aPhs(h, s);
158
- return r3bPhs(h, s);
159
- }
160
- function solveGibbsHS(h, s, initial, stateAt) {
161
- const hScale = Math.max(1, Math.abs(h));
162
- const sScale = Math.max(1, Math.abs(s));
163
- const [p, T] = dampedNewton2D(([candidateP, candidateT]) => {
164
- const state = stateAt(candidateP, candidateT);
165
- const derivatives = gibbsPropertyDerivatives(state);
166
- return {
167
- residual: [
168
- (state.enthalpy - h) / hScale,
169
- (state.entropy - s) / sScale,
170
- ],
171
- jacobian: [
172
- [derivatives.dhDp / hScale, derivatives.dhDT / hScale],
173
- [derivatives.dsDp / sScale, derivatives.dsDT / sScale],
174
- ],
175
- };
176
- }, initial, {
177
- isValid: ([candidateP, candidateT]) => candidateP > 0 && candidateT > 0,
178
- });
179
- return stateAt(p, T);
180
- }
181
- function solveRegion3HS(h, s, rho0, T0) {
182
- const hScale = Math.max(1, Math.abs(h));
183
- const sScale = Math.max(1, Math.abs(s));
184
- const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
185
- const state = region3ByRhoT(candidateRho, candidateT);
186
- const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
187
- return {
188
- residual: [
189
- (state.enthalpy - h) / hScale,
190
- (state.entropy - s) / sScale,
191
- ],
192
- jacobian: [
193
- [derivatives.dhDrho / hScale, derivatives.dhDT / hScale],
194
- [derivatives.dsDrho / sScale, derivatives.dsDT / sScale],
195
- ],
196
- };
197
- }, [rho0, T0], {
198
- // Tighter than the 1e-10 default and load-bearing: near the 100 MPa ceiling
199
- // Region 3 is nearly incompressible, so a residual the default would accept
200
- // still lands p far enough outside [P_MIN, P_MAX] to fail envelope
201
- // validation. Covered by the p=100 MPa cases in newton-regressions.test.ts.
202
- tolerance: 1e-12,
203
- isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
204
- });
205
- return region3ByRhoT(rho, T);
206
- }
207
- // ─── Main HS Solver ─────────────────────────────────────────────────────────
12
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
13
+ import { classifyRegionHS } from './hs-region.js';
14
+ import { solveHSInRegion } from './hs-state.js';
15
+ import { assertCriticalRegion4HSInput } from '../saturation/region4-boundaries.js';
208
16
  /**
209
17
  * Solve for thermodynamic state given H and S.
210
18
  * @param h - Specific enthalpy [kJ/kg]
@@ -213,58 +21,36 @@ function solveRegion3HS(h, s, rho0, T0) {
213
21
  export function solveHS(h, s) {
214
22
  assertFiniteNumber('Enthalpy', h);
215
23
  assertFiniteNumber('Entropy', s);
216
- if (h < C.H_MIN || h > C.H_MAX) {
24
+ if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
217
25
  throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
218
26
  }
219
- if (s < C.S_MIN || s > C.S_MAX) {
27
+ if (!withinClosedLowerBound(s, C.S_MIN, 'entropy') || s > C.S_MAX) {
220
28
  throw new OutOfRangeError('Entropy', s, C.S_MIN, C.S_MAX);
221
29
  }
222
30
  assertCriticalRegion4HSInput(h, s, 'solveHS');
223
- const region = detectRegionHS(h, s);
224
- switch (region) {
225
- case Region.Region1: {
226
- const P0 = r1Phs(h, s);
227
- const state = solveGibbsHS(h, s, [P0, r1BackwardT(P0, h)], region1);
228
- return validateBackwardState(state, [
229
- { label: 'enthalpy', expected: h },
230
- { label: 'entropy', expected: s },
231
- ], { solverName: 'solveHS', expectedRegion: Region.Region1 });
232
- }
233
- case Region.Region2: {
234
- const P0 = r2Phs(h, s);
235
- const state = solveGibbsHS(h, s, [P0, r2BackwardT(P0, h)], region2);
236
- return validateBackwardState(state, [
237
- { label: 'enthalpy', expected: h },
238
- { label: 'entropy', expected: s },
239
- ], { solverName: 'solveHS', expectedRegion: Region.Region2 });
240
- }
241
- case Region.Region3: {
242
- const P0 = r3Phs(h, s);
243
- const initial = r3BackwardPHInitial(P0, h);
244
- const state = solveRegion3HS(h, s, initial.rho, initial.T);
245
- return validateBackwardState(state, [
246
- { label: 'enthalpy', expected: h },
247
- { label: 'entropy', expected: s },
248
- ], { solverName: 'solveHS', expectedRegion: Region.Region3 });
31
+ const candidate = {};
32
+ const region = classifyRegionHS(h, s, candidate);
33
+ if (candidate.state !== undefined)
34
+ return candidate.state;
35
+ try {
36
+ return solveHSInRegion(h, s, region);
37
+ }
38
+ catch (error) {
39
+ if (!(error instanceof RegionMismatchError) || error.solverName !== 'solveHS' ||
40
+ error.expectedRegion !== region)
41
+ throw error;
42
+ const adjacent = error.actualRegion;
43
+ if (!(adjacent === Region.Region3 && (region === Region.Region1 || region === Region.Region2)) &&
44
+ !(region === Region.Region3 && (adjacent === Region.Region1 || adjacent === Region.Region2))) {
45
+ throw error;
249
46
  }
250
- case Region.Region4: {
251
- const state = tryRegion4HSState(h, s);
252
- if (state === null) {
253
- throw new IF97Error('solveHS identified Region 4 but failed to construct a valid saturation state');
254
- }
255
- return validateBackwardState(state, [
256
- { label: 'enthalpy', expected: h },
257
- { label: 'entropy', expected: s },
258
- ], { solverName: 'solveHS', expectedRegion: Region.Region4 });
47
+ try {
48
+ return solveHSInRegion(h, s, adjacent);
259
49
  }
260
- case Region.Region5: {
261
- const state = solveGibbsHS(h, s, [1, 1400], region5);
262
- return validateBackwardState(state, [
263
- { label: 'enthalpy', expected: h },
264
- { label: 'entropy', expected: s },
265
- ], { solverName: 'solveHS', expectedRegion: Region.Region5 });
50
+ catch (alternateError) {
51
+ if (!(alternateError instanceof IF97Error))
52
+ throw alternateError;
53
+ throw error;
266
54
  }
267
- default:
268
- throw new IF97Error(`Cannot determine region for h=${h} kJ/kg, s=${s} kJ/(kg·K)`);
269
55
  }
270
56
  }
@@ -12,6 +12,8 @@ export declare function r1BackwardT(p: number, h: number): number;
12
12
  * Ref: IAPWS-IF97, Eq. 20. Check point: h = 3516.004323 → p = 100 MPa. */
13
13
  export declare function b2bc_H_P(h: number): number;
14
14
  export declare function r2BackwardT(p: number, h: number): number;
15
+ /** B3ab boundary: P → H */
16
+ export declare function b3ab_P_to_H(p: number): number;
15
17
  export declare function r3BackwardPHInitial(p: number, h: number): {
16
18
  rho: number;
17
19
  T: number;