iapws-if97 2.1.5 → 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 (109) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +78 -215
  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 +6 -6
  8. package/dist/backward/fixed-temperature-solver.js +56 -9
  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.d.ts +1 -2
  16. package/dist/backward/hs.js +28 -232
  17. package/dist/backward/ph.d.ts +8 -3
  18. package/dist/backward/ph.js +102 -54
  19. package/dist/backward/pressure-property-region.d.ts +13 -0
  20. package/dist/backward/pressure-property-region.js +262 -0
  21. package/dist/backward/ps.d.ts +6 -1
  22. package/dist/backward/ps.js +103 -38
  23. package/dist/backward/solution-validation.d.ts +7 -2
  24. package/dist/backward/solution-validation.js +162 -37
  25. package/dist/backward/th.d.ts +3 -6
  26. package/dist/backward/th.js +10 -73
  27. package/dist/backward/tolerances.d.ts +3 -1
  28. package/dist/backward/tolerances.js +9 -4
  29. package/dist/backward/ts.d.ts +3 -6
  30. package/dist/backward/ts.js +10 -73
  31. package/dist/boundaries.d.ts +0 -1
  32. package/dist/boundaries.js +0 -1
  33. package/dist/constants.d.ts +11 -24
  34. package/dist/constants.js +17 -28
  35. package/dist/core/floating-point.d.ts +2 -0
  36. package/dist/core/floating-point.js +7 -0
  37. package/dist/core/formulation-seams.d.ts +27 -0
  38. package/dist/core/formulation-seams.js +289 -0
  39. package/dist/core/input-validation.d.ts +1 -1
  40. package/dist/core/input-validation.js +5 -1
  41. package/dist/core/region-detector.d.ts +3 -41
  42. package/dist/core/region-detector.js +39 -597
  43. package/dist/core/region3-pt.d.ts +12 -1
  44. package/dist/core/region3-pt.js +68 -13
  45. package/dist/core/solve-input-normalization.d.ts +0 -1
  46. package/dist/core/solve-input-normalization.js +4 -2
  47. package/dist/core/solver.d.ts +7 -11
  48. package/dist/core/solver.js +26 -18
  49. package/dist/core/thermodynamic-derivatives.d.ts +17 -0
  50. package/dist/core/thermodynamic-derivatives.js +27 -0
  51. package/dist/detect.d.ts +16 -3
  52. package/dist/detect.js +36 -3
  53. package/dist/errors.d.ts +11 -0
  54. package/dist/errors.js +16 -0
  55. package/dist/gas-constant.d.ts +5 -0
  56. package/dist/gas-constant.js +5 -0
  57. package/dist/index.d.ts +4 -6
  58. package/dist/index.js +10 -22
  59. package/dist/regions/boundaries.d.ts +0 -1
  60. package/dist/regions/boundaries.js +0 -1
  61. package/dist/regions/region1.d.ts +2 -2
  62. package/dist/regions/region1.js +1 -2
  63. package/dist/regions/region2.d.ts +2 -2
  64. package/dist/regions/region2.js +1 -2
  65. package/dist/regions/region3-data.d.ts +0 -1
  66. package/dist/regions/region3-data.js +3 -4
  67. package/dist/regions/region3-eval.d.ts +0 -1
  68. package/dist/regions/region3-eval.js +0 -1
  69. package/dist/regions/region3-subregions.d.ts +0 -1
  70. package/dist/regions/region3-subregions.js +15 -11
  71. package/dist/regions/region3.d.ts +4 -2
  72. package/dist/regions/region3.js +32 -3
  73. package/dist/regions/region4.d.ts +2 -1
  74. package/dist/regions/region4.js +39 -13
  75. package/dist/regions/region5.d.ts +2 -2
  76. package/dist/regions/region5.js +2 -3
  77. package/dist/regions.d.ts +0 -1
  78. package/dist/regions.js +0 -1
  79. package/dist/saturation/common.d.ts +2 -9
  80. package/dist/saturation/common.js +66 -45
  81. package/dist/saturation/region4-boundaries.d.ts +0 -5
  82. package/dist/saturation/region4-boundaries.js +17 -18
  83. package/dist/saturation/region4-hs.d.ts +2 -1
  84. package/dist/saturation/region4-hs.js +499 -106
  85. package/dist/saturation/two-phase.d.ts +2 -3
  86. package/dist/saturation/two-phase.js +7 -8
  87. package/dist/saturation.d.ts +0 -1
  88. package/dist/saturation.js +0 -1
  89. package/dist/solvers/bracketed-newton.d.ts +10 -6
  90. package/dist/solvers/bracketed-newton.js +19 -13
  91. package/dist/solvers/damped-newton-2d.d.ts +12 -0
  92. package/dist/solvers/damped-newton-2d.js +68 -0
  93. package/dist/solvers/extended-arithmetic.d.ts +6 -0
  94. package/dist/solvers/extended-arithmetic.js +38 -0
  95. package/dist/solvers/newton-raphson.d.ts +0 -1
  96. package/dist/solvers/newton-raphson.js +0 -1
  97. package/dist/transport/properties.d.ts +4 -3
  98. package/dist/transport/properties.js +14 -5
  99. package/dist/transport.d.ts +0 -1
  100. package/dist/transport.js +0 -1
  101. package/dist/types.d.ts +18 -1
  102. package/dist/types.js +14 -1
  103. package/package.json +18 -10
  104. package/dist/backward/objective-normalization.d.ts +0 -7
  105. package/dist/backward/objective-normalization.js +0 -8
  106. package/dist/core/public-normalization.d.ts +0 -4
  107. package/dist/core/public-normalization.js +0 -54
  108. package/dist/solvers/nelder-mead.d.ts +0 -17
  109. package/dist/solvers/nelder-mead.js +0 -116
@@ -4,18 +4,19 @@
4
4
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations
5
5
  */
6
6
  import { Region, IF97Error, OutOfRangeError } from '../types.js';
7
+ import { SaturationEndpointError } from '../errors.js';
7
8
  import * as C from '../constants.js';
8
9
  import { region1 } from '../regions/region1.js';
9
10
  import { region2 } from '../regions/region2.js';
10
- import { region3ByRhoT } from '../regions/region3.js';
11
+ import { region3ByRhoT, region3PressureResidual } from '../regions/region3.js';
11
12
  import { region5 } from '../regions/region5.js';
12
- import { assertFiniteNumber } from '../core/input-validation.js';
13
- import { detectRegionPS } from '../core/region-detector.js';
13
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
14
+ import { assertPressurePropertyEnvelope, detectRegionPS, pressurePropertyBoundaryState, pressurePropertySeam } from './pressure-property-region.js';
14
15
  import { newtonRaphson } from '../solvers/newton-raphson.js';
15
- import { nelderMead } from '../solvers/nelder-mead.js';
16
- import { validateBackwardState } from './solution-validation.js';
17
- import { backwardConstraintTolerance } from './tolerances.js';
18
- import { sumNormalizedResiduals } from './objective-normalization.js';
16
+ import { bracketedNewton } from '../solvers/bracketed-newton.js';
17
+ import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
18
+ import { validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
19
+ import { helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
19
20
  import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtPressure, } from '../saturation/common.js';
20
21
  import { assertCriticalRegion4PSInput, assertRegion4StateAllowed, } from '../saturation/region4-boundaries.js';
21
22
  function evalPoly(table, piShift, sigShift, pi, sig) {
@@ -34,7 +35,7 @@ const R1_PS = [
34
35
  [2, 31, -4.2522657042207e-26], [3, 10, 2.6400441360689e-13], [3, 32, 7.8124600459723e-29],
35
36
  [4, 32, -3.0732199903668e-31],
36
37
  ];
37
- function r1BackwardT(p, s) {
38
+ export function r1BackwardT(p, s) {
38
39
  return evalPoly(R1_PS, 0, -2, p, s);
39
40
  }
40
41
  // ─── Region 2 Backward T(P,S) — Subregions 2a/2b/2c ───────────────────────
@@ -42,7 +43,7 @@ const R2A_PS = [
42
43
  [-1.5, -24, -392359.83861984], [-1.5, -23, 515265.7382727], [-1.5, -19, 40482.443161048],
43
44
  [-1.5, -13, -321.93790923902], [-1.5, -11, 96.961424218694], [-1.5, -10, -22.867846371773],
44
45
  [-1.25, -19, -449429.14124357], [-1.25, -15, -5011.8336020166], [-1.25, -6, 0.35684463560015],
45
- [-1, -26, 44235.33584819], [-1, -21, -13673.388811708], [-1, -17, 421632.6027864],
46
+ [-1, -26, 44235.33584819], [-1, -21, -13673.388811708], [-1, -17, 421632.60207864],
46
47
  [-1, -16, 22516.925837475], [-1, -9, 474.42144865646], [-1, -8, -149.31130797647],
47
48
  [-0.75, -15, -197811.26320452], [-0.75, -14, -23554.39947076], [-0.5, -26, -19070.616302076],
48
49
  [-0.5, -13, 55375.669883164], [-0.5, -9, 3829.3691437363], [-0.5, -7, -603.91860580567],
@@ -85,14 +86,14 @@ const R2C_PS = [
85
86
  [6, 1, 1.0136618529763e-8], [7, 0, 5.9925719692351e-12], [7, 1, -2.0677870105164e-11],
86
87
  [7, 3, -2.0874278181886e-11], [7, 4, 1.0162166825089e-10], [7, 5, -1.6429828281347e-10],
87
88
  ];
88
- function r2BackwardT(p, s) {
89
+ export function r2BackwardT(p, s) {
89
90
  if (p <= C.R2_P_CRT) {
90
91
  return evalPoly(R2A_PS, 0, 2, p, s / 2);
91
92
  }
92
93
  if (s >= C.R2_S_CRT) {
93
- return evalPoly(R2B_PS, 0, 10, p, s / 0.7853);
94
+ return evalPoly(R2B_PS, 0, -10, p, -s / 0.7853);
94
95
  }
95
- return evalPoly(R2C_PS, 0, 2, p, s / 2.9251);
96
+ return evalPoly(R2C_PS, 0, -2, p, -s / 2.9251);
96
97
  }
97
98
  // ─── Region 3 Backward T(P,S) and v(P,S) ──────────────────────────────────
98
99
  const R3A_PS_T_TABLE = [
@@ -145,6 +146,40 @@ const R3B_PS_V_TABLE = [
145
146
  [0, 2, -19.1449143716586], [1, 0, -0.0150448002905284], [1, 2, 14.6407900162154],
146
147
  [2, 2, -3.2747778718823],
147
148
  ];
149
+ export function r3BackwardPSInitial(p, s) {
150
+ if (s <= C.R3_S_CRT) {
151
+ const T = 760 * evalPoly(R3A_PS_T_TABLE, -0.240, 0.703, p / 100, s / 4.4);
152
+ const v = 0.0028 * evalPoly(R3A_PS_V_TABLE, -0.187, 0.755, p / 100, s / 4.4);
153
+ return { rho: 1 / v, T };
154
+ }
155
+ const T = 860 * evalPoly(R3B_PS_T_TABLE, -0.760, 0.818, p / 100, s / 5.3);
156
+ const v = 0.0088 * evalPoly(R3B_PS_V_TABLE, -0.298, 0.816, p / 100, s / 5.3);
157
+ return { rho: 1 / v, T };
158
+ }
159
+ function solveRegion3PS(p, s, rho0, T0, refinePressure = false) {
160
+ // Scaling the 1e-13 solver tolerance by 1e-4 resolves pressure below one ULP before PT reconstruction.
161
+ const pScale = Math.max(1, Math.abs(p)) * (refinePressure ? 1e-4 : 1);
162
+ const sScale = Math.max(1, Math.abs(s));
163
+ const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
164
+ const state = region3ByRhoT(candidateRho, candidateT);
165
+ const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
166
+ return {
167
+ residual: [
168
+ (refinePressure ? region3PressureResidual(candidateRho, candidateT, p) : state.pressure - p) / pScale,
169
+ (state.entropy - s) / sScale,
170
+ ],
171
+ jacobian: [
172
+ [derivatives.dpDrho / pScale, derivatives.dpDT / pScale],
173
+ [derivatives.dsDrho / sScale, derivatives.dsDT / sScale],
174
+ ],
175
+ };
176
+ }, [rho0, T0], {
177
+ // Near saturation, the default residual can put the liquid root across Tsat.
178
+ tolerance: 1e-13,
179
+ isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
180
+ });
181
+ return region3ByRhoT(rho, T);
182
+ }
148
183
  // ─── Main PS Solver ────────────────────────────────────────────────────────
149
184
  /**
150
185
  * Solve for thermodynamic state given P and S.
@@ -157,51 +192,81 @@ export function solvePS(p, s) {
157
192
  if (p < C.P_MIN || p > C.P_MAX) {
158
193
  throw new OutOfRangeError('Pressure', p, C.P_MIN, C.P_MAX);
159
194
  }
160
- if (s < C.S_MIN || s > C.S_MAX) {
195
+ if (!withinClosedLowerBound(s, C.S_MIN, 'entropy') || s > C.S_MAX) {
161
196
  throw new OutOfRangeError('Entropy', s, C.S_MIN, C.S_MAX);
162
197
  }
163
198
  assertCriticalRegion4PSInput(p, s, 'solvePS');
164
- const region = detectRegionPS(p, s);
199
+ assertPressurePropertyEnvelope(p, s, 'entropy');
200
+ const seam = pressurePropertySeam(p, s, 'entropy');
201
+ if (seam !== null && seam.kind !== 'overlap') {
202
+ return validateBackwardState(seam.state, [
203
+ { label: 'pressure', expected: p },
204
+ { label: 'entropy', expected: s },
205
+ ], { solverName: 'solvePS', expectedRegion: seam.state.region, seam });
206
+ }
207
+ if (seam === null) {
208
+ const boundary = pressurePropertyBoundaryState(p, s, 'entropy');
209
+ if (boundary !== null)
210
+ return boundary;
211
+ }
212
+ let region;
213
+ let saturationFailure = null;
214
+ try {
215
+ region = seam?.state.region ?? detectRegionPS(p, s);
216
+ }
217
+ catch (error) {
218
+ if (!(error instanceof SaturationEndpointError)) {
219
+ throw error;
220
+ }
221
+ saturationFailure = error;
222
+ region = Region.Region3;
223
+ }
165
224
  switch (region) {
166
225
  case Region.Region1: {
167
226
  const T0 = r1BackwardT(p, s);
168
- const T = newtonRaphson((T_x) => region1(p, T_x).entropy - s, T0);
227
+ const T = newtonRaphson((T_x) => region1(p, T_x).entropy - s, T0, (T_x) => (region1(p, T_x).cp ?? Number.NaN) / T_x);
169
228
  return validateBackwardState(region1(p, T), [
170
229
  { label: 'pressure', expected: p },
171
230
  { label: 'entropy', expected: s },
172
231
  ], { solverName: 'solvePS', expectedRegion: Region.Region1 });
173
232
  }
174
233
  case Region.Region2: {
234
+ const targetS = Math.min(s, region2(p, C.R2_T_MAX).entropy);
175
235
  const T0 = r2BackwardT(p, s);
176
- const T = newtonRaphson((T_x) => region2(p, T_x).entropy - s, T0);
236
+ const T = newtonRaphson((T_x) => region2(p, T_x).entropy - targetS, T0, (T_x) => (region2(p, T_x).cp ?? Number.NaN) / T_x);
177
237
  return validateBackwardState(region2(p, T), [
178
238
  { label: 'pressure', expected: p },
179
239
  { label: 'entropy', expected: s },
180
240
  ], { solverName: 'solvePS', expectedRegion: Region.Region2 });
181
241
  }
182
242
  case Region.Region3: {
183
- const pressureTolerance = 1e-5 * Math.max(1, Math.abs(p));
184
- const entropyTolerance = backwardConstraintTolerance('entropy', s);
185
- let T0, v0;
186
- if (s <= C.R3_S_CRT) {
187
- T0 = 760 * evalPoly(R3A_PS_T_TABLE, -0.240, 0.703, p / 100, s / 4.4);
188
- v0 = 0.0028 * evalPoly(R3A_PS_V_TABLE, -0.187, 0.755, p / 100, s / 4.4);
243
+ try {
244
+ const initial = r3BackwardPSInitial(p, s);
245
+ const validate = (state) => validateBackwardState(state, [
246
+ { label: 'pressure', expected: p },
247
+ { label: 'entropy', expected: s },
248
+ ], { solverName: 'solvePS', expectedRegion: Region.Region3 });
249
+ const validated = validate(solveRegion3PS(p, s, initial.rho, initial.T));
250
+ // Compressibility can amplify an accepted pressure residual beyond PT's density accuracy.
251
+ const relativeDensityError = Math.abs(region3PressureResidual(1 / validated.specificVolume, validated.temperature, p)) * validated.isothermalCompressibility;
252
+ if (saturationFailure === null && relativeDensityError <= 1e-12)
253
+ return validated;
254
+ try {
255
+ const refined = validate(solveRegion3PS(p, s, 1 / validated.specificVolume, validated.temperature, true));
256
+ validateRegion3PTReconstruction(refined, 'solvePS');
257
+ return refined;
258
+ }
259
+ catch (error) {
260
+ if (!(error instanceof IF97Error))
261
+ throw error;
262
+ return saturationFailure === null ? validated : validateRegion3PTReconstruction(validated, 'solvePS');
263
+ }
189
264
  }
190
- else {
191
- T0 = 860 * evalPoly(R3B_PS_T_TABLE, -0.760, 0.818, p / 100, s / 5.3);
192
- v0 = 0.0088 * evalPoly(R3B_PS_V_TABLE, -0.298, 0.816, p / 100, s / 5.3);
265
+ catch (error) {
266
+ if (saturationFailure !== null && error instanceof IF97Error)
267
+ throw saturationFailure;
268
+ throw error;
193
269
  }
194
- const sol = nelderMead((pair) => {
195
- const state = region3ByRhoT(1 / pair[0], pair[1]);
196
- return sumNormalizedResiduals([
197
- { actual: state.entropy, expected: s, tolerance: entropyTolerance },
198
- { actual: state.pressure, expected: p, tolerance: pressureTolerance },
199
- ]);
200
- }, [v0, T0], { maxIterations: 1000, minErrorDelta: 1e-8, minTolerance: 1e-9 });
201
- return validateBackwardState(region3ByRhoT(1 / sol.x[0], sol.x[1]), [
202
- { label: 'pressure', expected: p, tolerance: pressureTolerance },
203
- { label: 'entropy', expected: s },
204
- ], { solverName: 'solvePS', expectedRegion: Region.Region3 });
205
270
  }
206
271
  case Region.Region4: {
207
272
  const endpoints = saturationEndpointsAtPressure(p);
@@ -213,7 +278,8 @@ export function solvePS(p, s) {
213
278
  ], { solverName: 'solvePS', expectedRegion: Region.Region4 });
214
279
  }
215
280
  case Region.Region5: {
216
- const T = newtonRaphson((T_x) => region5(p, T_x).entropy - s, 1500);
281
+ const targetS = Math.min(s, region5(p, C.R5_T_MAX).entropy);
282
+ const T = bracketedNewton((T_x) => region5(p, T_x).entropy - targetS, C.R5_T_MIN + Number.EPSILON * C.R5_T_MIN, C.R5_T_MAX, 1500, { derivative: (T_x) => (region5(p, T_x).cp ?? Number.NaN) / T_x, tolerance: 1e-12 });
217
283
  return validateBackwardState(region5(p, T), [
218
284
  { label: 'pressure', expected: p },
219
285
  { label: 'entropy', expected: s },
@@ -223,4 +289,3 @@ export function solvePS(p, s) {
223
289
  throw new IF97Error(`Cannot determine region for P=${p} MPa, s=${s} kJ/(kg·K)`);
224
290
  }
225
291
  }
226
- //# sourceMappingURL=ps.js.map
@@ -1,5 +1,6 @@
1
+ import type { FormulationSeamResolution } from '../core/formulation-seams.js';
1
2
  import type { BasicProperties } from '../types.js';
2
- import { Region } from '../types.js';
3
+ import { IF97Error, Region } from '../types.js';
3
4
  export interface BackwardConstraint {
4
5
  label: 'pressure' | 'temperature' | 'enthalpy' | 'entropy';
5
6
  expected: number;
@@ -8,6 +9,10 @@ export interface BackwardConstraint {
8
9
  export interface BackwardValidationOptions {
9
10
  solverName: string;
10
11
  expectedRegion?: Region;
12
+ seam?: FormulationSeamResolution;
11
13
  }
14
+ /** Inspect a candidate without moving it across a formulation or domain boundary. */
15
+ export declare function backwardCandidateError(state: BasicProperties, constraints: readonly BackwardConstraint[], options: BackwardValidationOptions): IF97Error | null;
16
+ export declare function normalizeBackwardState(state: BasicProperties, constraints: readonly BackwardConstraint[], solverName: string): BasicProperties;
12
17
  export declare function validateBackwardState(state: BasicProperties, constraints: readonly BackwardConstraint[], options: BackwardValidationOptions): BasicProperties;
13
- //# sourceMappingURL=solution-validation.d.ts.map
18
+ export declare function validateRegion3PTReconstruction(state: BasicProperties, solverName: string): BasicProperties;
@@ -1,7 +1,13 @@
1
1
  import * as C from '../constants.js';
2
- import { detectRegionPT } from '../core/region-detector.js';
2
+ import { detectRegionPT, nativeB23TemperatureBracket, nativeSaturationTemperatureBracket } from '../core/region-detector.js';
3
+ import { solveRegion3PTBasic } from '../core/region3-pt.js';
4
+ import { nextPositiveFloat } from '../core/floating-point.js';
5
+ import { region2 } from '../regions/region2.js';
6
+ import { region1 } from '../regions/region1.js';
3
7
  import { saturationPressure } from '../regions/region4.js';
8
+ import { region5 } from '../regions/region5.js';
4
9
  import { IF97Error, Region } from '../types.js';
10
+ import { NumericalStateError, RegionMismatchError } from '../errors.js';
5
11
  import { backwardConstraintTolerance, region4SaturationPressureTolerance, } from './tolerances.js';
6
12
  function propertyValue(state, label) {
7
13
  switch (label) {
@@ -17,69 +23,188 @@ function propertyValue(state, label) {
17
23
  return Number.NaN;
18
24
  }
19
25
  }
20
- function validateBasicEnvelope(state, solverName) {
26
+ function boundaryTolerance(value, boundary) {
27
+ return 1e-9 * Math.max(1, Math.abs(value), Math.abs(boundary));
28
+ }
29
+ function snapUpFromBelow(value, boundary) {
30
+ return value < boundary && boundary - value <= boundaryTolerance(value, boundary)
31
+ ? boundary
32
+ : value;
33
+ }
34
+ function snapDownFromAbove(value, boundary) {
35
+ return value > boundary && value - boundary <= boundaryTolerance(value, boundary)
36
+ ? boundary
37
+ : value;
38
+ }
39
+ function normalizeComputedPT(state) {
40
+ if (state.region === Region.Region4)
41
+ return state;
42
+ let pressure = state.pressure < C.P_MIN &&
43
+ C.P_MIN - state.pressure <= backwardConstraintTolerance('pressure', C.P_MIN)
44
+ ? C.P_MIN
45
+ : state.pressure;
46
+ pressure = snapDownFromAbove(pressure, state.region === Region.Region5 ? C.R5_P_MAX : C.P_MAX);
47
+ let temperature = snapUpFromBelow(state.temperature, C.T_MIN);
48
+ if (state.region === Region.Region1) {
49
+ temperature = snapDownFromAbove(temperature, C.R2_T_MIN);
50
+ }
51
+ else if (state.region === Region.Region2) {
52
+ temperature = snapDownFromAbove(temperature, C.R2_T_MAX);
53
+ }
54
+ temperature = snapDownFromAbove(temperature, C.T_MAX);
55
+ if (pressure === state.pressure && temperature === state.temperature)
56
+ return state;
57
+ switch (state.region) {
58
+ case Region.Region1: return region1(pressure, temperature);
59
+ case Region.Region2: return region2(pressure, temperature);
60
+ case Region.Region3: return solveRegion3PTBasic(pressure, temperature);
61
+ case Region.Region5: return region5(pressure, temperature);
62
+ }
63
+ }
64
+ function normalizeB23State(state) {
65
+ if ((state.region !== Region.Region2 && state.region !== Region.Region3) ||
66
+ state.pressure <= C.B23_P_MIN || state.pressure > C.P_MAX ||
67
+ (state.temperature <= C.R2_T_MIN &&
68
+ (state.region !== Region.Region2 || state.pressure <= saturationPressure(C.R2_T_MIN))) ||
69
+ state.temperature >= C.B23_T_MAX ||
70
+ detectRegionPT(state.pressure, state.temperature) === state.region) {
71
+ return state;
72
+ }
73
+ const [lower, upper] = nativeB23TemperatureBracket(state.pressure);
74
+ const vaporT = state.region === Region.Region2 && state.pressure < C.Pc
75
+ ? nativeSaturationTemperatureBracket(state.pressure)[1] : upper;
76
+ const temperature = state.region === Region.Region3 ? lower : Math.max(upper, vaporT);
77
+ if (Math.abs(temperature - state.temperature) > boundaryTolerance(temperature, state.temperature)) {
78
+ return state;
79
+ }
80
+ return state.region === Region.Region3
81
+ ? solveRegion3PTBasic(state.pressure, temperature)
82
+ : region2(state.pressure, temperature);
83
+ }
84
+ function normalizeRegion3LowerState(state) {
85
+ if (state.region !== Region.Region3 || !(state.temperature <= C.R2_T_MIN) ||
86
+ C.R2_T_MIN - state.temperature > boundaryTolerance(state.temperature, C.R2_T_MIN))
87
+ return state;
88
+ return solveRegion3PTBasic(state.pressure, nextPositiveFloat(C.R2_T_MIN, 1));
89
+ }
90
+ function normalizeSaturationState(state, constraints, solverName) {
91
+ if ((state.region !== Region.Region1 && state.region !== Region.Region2) ||
92
+ state.pressure > saturationPressure(C.R2_T_MIN) ||
93
+ state.temperature < C.T_MIN || state.temperature > C.R2_T_MIN ||
94
+ detectRegionPT(state.pressure, state.temperature) === state.region) {
95
+ return state;
96
+ }
97
+ if (!constraints.some(({ label }) => label === 'pressure')) {
98
+ const psat = saturationPressure(state.temperature);
99
+ const pressure = state.region === Region.Region1 ? psat : nextPositiveFloat(psat, -1);
100
+ const projected = state.region === Region.Region1
101
+ ? region1(pressure, state.temperature)
102
+ : region2(pressure, state.temperature);
103
+ if (Math.abs(pressure - state.pressure) <= backwardConstraintTolerance('pressure', psat) &&
104
+ backwardCandidateError(projected, constraints, { solverName }) === null)
105
+ return projected;
106
+ }
107
+ const [liquidT, vaporT] = nativeSaturationTemperatureBracket(state.pressure);
108
+ const temperature = state.region === Region.Region1 ? liquidT : vaporT;
109
+ if (Math.abs(temperature - state.temperature) > boundaryTolerance(temperature, state.temperature)) {
110
+ return state;
111
+ }
112
+ return state.region === Region.Region1
113
+ ? region1(state.pressure, temperature)
114
+ : region2(state.pressure, temperature);
115
+ }
116
+ function basicEnvelopeError(state, solverName) {
21
117
  if (!Number.isFinite(state.pressure) || !Number.isFinite(state.temperature) ||
22
- !Number.isFinite(state.specificVolume) || state.specificVolume <= 0) {
23
- throw new IF97Error(`${solverName} produced an invalid thermodynamic state`);
118
+ !Number.isFinite(state.specificVolume)) {
119
+ return new NumericalStateError(`${solverName} produced an invalid thermodynamic state`);
24
120
  }
121
+ if (state.specificVolume <= 0)
122
+ return new IF97Error(`${solverName} produced an invalid thermodynamic state`);
25
123
  if (state.pressure < C.P_MIN || state.pressure > C.P_MAX ||
26
124
  state.temperature < C.T_MIN || state.temperature > C.T_MAX) {
27
- throw new IF97Error(`${solverName} produced a state outside the IF97 PT envelope`);
125
+ return new IF97Error(`${solverName} produced a state outside the IF97 PT envelope`);
28
126
  }
127
+ if (state.region === Region.Region3) {
128
+ const stability = [state.cp, state.cv, state.isothermalCompressibility, state.speedOfSound];
129
+ if (!stability.every((value) => value !== null && Number.isFinite(value) && value > 0)) {
130
+ const ErrorType = stability.some((value) => value === null || !Number.isFinite(value))
131
+ ? NumericalStateError : IF97Error;
132
+ return new ErrorType(`${solverName}: Region 3 numerical inconsistency: computed EOS state at p=${state.pressure} MPa, T=${state.temperature} K failed stability checks`);
133
+ }
134
+ }
135
+ return null;
29
136
  }
30
- function validateRegionConsistency(state, solverName, expectedRegion) {
137
+ function regionConsistencyError(state, solverName, expectedRegion) {
31
138
  if (expectedRegion !== undefined && state.region !== expectedRegion) {
32
- throw new IF97Error(`${solverName} returned Region ${state.region}, expected Region ${expectedRegion}`);
139
+ return new IF97Error(`${solverName} returned Region ${state.region}, expected Region ${expectedRegion}`);
33
140
  }
34
141
  if (state.region === Region.Region4) {
35
142
  if (state.quality === null || !Number.isFinite(state.quality) || state.quality < 0 || state.quality > 1) {
36
- throw new IF97Error(`${solverName} produced an invalid Region 4 quality`);
143
+ return new IF97Error(`${solverName} produced an invalid Region 4 quality`);
37
144
  }
38
145
  const psat = saturationPressure(state.temperature);
39
146
  const pTolerance = region4SaturationPressureTolerance(state.pressure);
40
147
  if (Math.abs(psat - state.pressure) > pTolerance) {
41
- throw new IF97Error(`${solverName} produced a Region 4 state off the saturation curve`);
148
+ return new IF97Error(`${solverName} produced a Region 4 state off the saturation curve`);
42
149
  }
43
- return;
150
+ return null;
44
151
  }
45
152
  const detectedRegion = detectRegionPT(state.pressure, state.temperature);
46
153
  if (detectedRegion !== state.region) {
47
- throw new IF97Error(`${solverName} produced a PT state in Region ${detectedRegion}, not Region ${state.region}`);
154
+ return new RegionMismatchError(solverName, detectedRegion, state.region);
48
155
  }
156
+ return null;
49
157
  }
50
- function validateConstraints(state, constraints, solverName) {
158
+ function constraintError(state, constraints, solverName) {
51
159
  for (const constraint of constraints) {
52
160
  const actual = propertyValue(state, constraint.label);
53
161
  const tolerance = constraint.tolerance ?? backwardConstraintTolerance(constraint.label, constraint.expected);
54
- if (Math.abs(actual - constraint.expected) > tolerance) {
55
- throw new IF97Error(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
162
+ if (!Number.isFinite(actual)) {
163
+ return new NumericalStateError(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
164
+ }
165
+ if (!Number.isFinite(constraint.expected) || !Number.isFinite(tolerance) ||
166
+ tolerance < 0 || Math.abs(actual - constraint.expected) > tolerance) {
167
+ return new IF97Error(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
56
168
  }
57
169
  }
170
+ return null;
58
171
  }
59
- function withExactConstraints(state, constraints) {
60
- const normalized = { ...state };
61
- for (const constraint of constraints) {
62
- switch (constraint.label) {
63
- case 'pressure':
64
- normalized.pressure = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
65
- break;
66
- case 'temperature':
67
- normalized.temperature = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
68
- break;
69
- case 'enthalpy':
70
- normalized.enthalpy = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
71
- break;
72
- case 'entropy':
73
- normalized.entropy = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
74
- break;
75
- }
172
+ /** Inspect a candidate without moving it across a formulation or domain boundary. */
173
+ export function backwardCandidateError(state, constraints, options) {
174
+ return basicEnvelopeError(state, options.solverName) ??
175
+ constraintError(state, constraints, options.solverName) ??
176
+ regionConsistencyError(state, options.solverName, options.expectedRegion);
177
+ }
178
+ export function normalizeBackwardState(state, constraints, solverName) {
179
+ const computed = normalizeComputedPT(state);
180
+ return constraints.some(({ label }) => label === 'temperature')
181
+ ? computed
182
+ : normalizeSaturationState(normalizeB23State(normalizeRegion3LowerState(computed)), constraints, solverName);
183
+ }
184
+ export function validateBackwardState(state, constraints, options) {
185
+ const seam = options.seam;
186
+ if (seam !== undefined && state !== seam.state) {
187
+ throw new IF97Error(`${options.solverName} supplied an unrelated formulation seam state`);
76
188
  }
189
+ const checkedConstraints = seam?.kind === 'gap'
190
+ ? constraints.map((constraint) => constraint.label === 'enthalpy' || constraint.label === 'entropy'
191
+ ? { ...constraint, tolerance: seam.budgets[constraint.label] }
192
+ : constraint)
193
+ : constraints;
194
+ const initialError = constraintError(state, checkedConstraints, options.solverName);
195
+ if (initialError !== null)
196
+ throw initialError;
197
+ const normalized = normalizeBackwardState(state, checkedConstraints, options.solverName);
198
+ const error = backwardCandidateError(normalized, checkedConstraints, options);
199
+ if (error !== null)
200
+ throw error;
77
201
  return normalized;
78
202
  }
79
- export function validateBackwardState(state, constraints, options) {
80
- validateBasicEnvelope(state, options.solverName);
81
- validateRegionConsistency(state, options.solverName, options.expectedRegion);
82
- validateConstraints(state, constraints, options.solverName);
83
- return withExactConstraints(state, constraints);
203
+ export function validateRegion3PTReconstruction(state, solverName) {
204
+ return validateBackwardState(solveRegion3PTBasic(state.pressure, state.temperature), [
205
+ { label: 'pressure', expected: state.pressure },
206
+ { label: 'temperature', expected: state.temperature },
207
+ { label: 'enthalpy', expected: state.enthalpy },
208
+ { label: 'entropy', expected: state.entropy },
209
+ ], { solverName, expectedRegion: Region.Region3 });
84
210
  }
85
- //# sourceMappingURL=solution-validation.js.map
@@ -1,16 +1,13 @@
1
1
  /**
2
2
  * Backward equation T,H → state.
3
3
  *
4
- * Given temperature and specific enthalpy, determines the IAPWS-IF97 region
5
- * and solves for pressure using a bracketed Newton method, then returns
6
- * the full set of basic thermodynamic properties.
4
+ * Returns a state only when temperature and enthalpy determine it uniquely.
7
5
  */
8
6
  import type { BasicProperties } from '../types.js';
9
7
  /**
10
8
  * Solve for full basic thermodynamic state from temperature and enthalpy.
11
9
  *
12
- * Detects the IAPWS-IF97 region, then inverts h(p, T) to recover pressure
13
- * within the appropriate bracket for that region.
10
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
14
11
  *
15
12
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
16
13
  * the enthalpy surface is nearly flat there, making pressure recovery unreliable.
@@ -20,6 +17,6 @@ import type { BasicProperties } from '../types.js';
20
17
  * @returns Basic thermodynamic properties (without transport)
21
18
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
22
19
  * @throws {IF97Error} if T is within the critical exclusion band
20
+ * @throws {AmbiguousStateError} if multiple physical states match T and h
23
21
  */
24
22
  export declare function solveTH(T: number, h: number): BasicProperties;
25
- //# sourceMappingURL=th.d.ts.map
@@ -1,23 +1,11 @@
1
1
  import * as C from '../constants.js';
2
- import { IF97Error, OutOfRangeError, Region } from '../types.js';
3
- import { assertFiniteNumber } from '../core/input-validation.js';
4
- import { detectRegionTH } from '../core/region-detector.js';
5
- import { solveRegion3PTBasic } from '../core/region3-pt.js';
6
- import { region1 } from '../regions/region1.js';
7
- import { region2 } from '../regions/region2.js';
8
- import { region5 } from '../regions/region5.js';
9
- import { boundary23_T_to_P } from '../regions/boundaries.js';
10
- import { saturationPressure } from '../regions/region4.js';
11
- import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtTemperature, } from '../saturation/common.js';
12
- import { assertRegion4StateAllowed } from '../saturation/region4-boundaries.js';
13
- import { solveFixedTemperaturePressure } from './fixed-temperature-solver.js';
14
- import { validateBackwardState } from './solution-validation.js';
15
- const extractH = (s) => s.enthalpy;
2
+ import { IF97Error, OutOfRangeError } from '../types.js';
3
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
4
+ import { uniqueFixedTemperatureState } from './fixed-temperature-candidates.js';
16
5
  /**
17
6
  * Solve for full basic thermodynamic state from temperature and enthalpy.
18
7
  *
19
- * Detects the IAPWS-IF97 region, then inverts h(p, T) to recover pressure
20
- * within the appropriate bracket for that region.
8
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
21
9
  *
22
10
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
23
11
  * the enthalpy surface is nearly flat there, making pressure recovery unreliable.
@@ -27,6 +15,7 @@ const extractH = (s) => s.enthalpy;
27
15
  * @returns Basic thermodynamic properties (without transport)
28
16
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
29
17
  * @throws {IF97Error} if T is within the critical exclusion band
18
+ * @throws {AmbiguousStateError} if multiple physical states match T and h
30
19
  */
31
20
  export function solveTH(T, h) {
32
21
  assertFiniteNumber('Temperature', T);
@@ -34,67 +23,15 @@ export function solveTH(T, h) {
34
23
  if (T < C.T_MIN || T > C.T_MAX) {
35
24
  throw new OutOfRangeError('Temperature', T, C.T_MIN, C.T_MAX);
36
25
  }
37
- if (h < C.H_MIN || h > C.H_MAX) {
26
+ if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
38
27
  throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
39
28
  }
40
29
  if (Math.abs(T - C.Tc) < C.CRITICAL_T_EXCLUSION_BAND) {
41
30
  throw new IF97Error(`solveTH does not support temperatures within ${C.CRITICAL_T_EXCLUSION_BAND} K of the critical-point temperature Tc=${C.Tc} K; received T=${T} K`);
42
31
  }
43
- const region = detectRegionTH(T, h);
44
- switch (region) {
45
- case Region.Region1:
46
- return validateBackwardState(solveFixedTemperaturePressure(region1, T, h, saturationPressure(T), C.P_MAX, extractH, 'solveTH'), [
47
- { label: 'temperature', expected: T },
48
- { label: 'enthalpy', expected: h },
49
- ], {
50
- solverName: 'solveTH',
51
- expectedRegion: Region.Region1,
52
- });
53
- case Region.Region2: {
54
- const upper = T <= C.Tc
55
- ? saturationPressure(T)
56
- : T <= C.B23_T_MAX
57
- ? boundary23_T_to_P(T)
58
- : C.P_MAX;
59
- return validateBackwardState(solveFixedTemperaturePressure(region2, T, h, C.P_MIN, upper, extractH, 'solveTH'), [
60
- { label: 'temperature', expected: T },
61
- { label: 'enthalpy', expected: h },
62
- ], {
63
- solverName: 'solveTH',
64
- expectedRegion: Region.Region2,
65
- });
66
- }
67
- case Region.Region3: {
68
- const lower = T <= C.Tc
69
- ? saturationPressure(T)
70
- : boundary23_T_to_P(T);
71
- return validateBackwardState(solveFixedTemperaturePressure(solveRegion3PTBasic, T, h, lower, C.P_MAX, extractH, 'solveTH'), [
72
- { label: 'temperature', expected: T },
73
- { label: 'enthalpy', expected: h },
74
- ], {
75
- solverName: 'solveTH',
76
- expectedRegion: Region.Region3,
77
- });
78
- }
79
- case Region.Region4: {
80
- const endpoints = saturationEndpointsAtTemperature(T);
81
- assertRegion4StateAllowed(endpoints.pressure, endpoints.temperature, 'solveTH');
82
- const x = qualityFromSaturationProperty(endpoints.liquid.enthalpy, endpoints.vapor.enthalpy, h);
83
- return validateBackwardState(mixSaturationState(endpoints, x), [
84
- { label: 'temperature', expected: T },
85
- { label: 'enthalpy', expected: h },
86
- ], { solverName: 'solveTH', expectedRegion: Region.Region4 });
87
- }
88
- case Region.Region5:
89
- return validateBackwardState(solveFixedTemperaturePressure(region5, T, h, C.P_MIN, C.R5_P_MAX, extractH, 'solveTH'), [
90
- { label: 'temperature', expected: T },
91
- { label: 'enthalpy', expected: h },
92
- ], {
93
- solverName: 'solveTH',
94
- expectedRegion: Region.Region5,
95
- });
96
- default:
97
- throw new IF97Error(`solveTH cannot determine a valid IF97 state for T=${T} K, h=${h} kJ/kg`);
32
+ const state = uniqueFixedTemperatureState(T, h, 'enthalpy');
33
+ if (state === null) {
34
+ throw new IF97Error(`solveTH cannot determine a valid IF97 state for T=${T} K, h=${h} kJ/kg`);
98
35
  }
36
+ return state;
99
37
  }
100
- //# sourceMappingURL=th.js.map
@@ -1,6 +1,8 @@
1
1
  export type BackwardToleranceLabel = 'pressure' | 'temperature' | 'enthalpy' | 'entropy';
2
2
  export declare const BACKWARD_REGION4_QUALITY_TOLERANCE = 0.000001;
3
+ export declare const BACKWARD_HS_NEWTON_TOLERANCE = 1e-12;
4
+ export declare const BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE = 1e-13;
5
+ export declare function fixedTemperatureRootTolerance(property: 'enthalpy' | 'entropy'): number;
3
6
  export declare function backwardConstraintTolerance(label: BackwardToleranceLabel, expected: number): number;
4
7
  export declare function region4SaturationPressureTolerance(pressure: number): number;
5
8
  export declare function backwardSpecificVolumeTolerance(expected: number): number;
6
- //# sourceMappingURL=tolerances.d.ts.map