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
@@ -7,19 +7,20 @@
7
7
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations
8
8
  */
9
9
  import { Region, IF97Error, OutOfRangeError } from '../types.js';
10
+ import { SaturationEndpointError } from '../errors.js';
10
11
  import * as C from '../constants.js';
11
12
  import { region1 } from '../regions/region1.js';
12
13
  import { region2 } from '../regions/region2.js';
13
- import { region3ByRhoT } from '../regions/region3.js';
14
+ import { region3ByRhoT, region3PressureResidual } from '../regions/region3.js';
14
15
  import { region5 } from '../regions/region5.js';
15
16
  import { saturationTemperature } from '../regions/region4.js';
16
- import { assertFiniteNumber } from '../core/input-validation.js';
17
- import { detectRegionPH } from '../core/region-detector.js';
17
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
18
+ import { assertPressurePropertyEnvelope, detectRegionPH, pressurePropertyBoundaryState, pressurePropertySeam } from './pressure-property-region.js';
18
19
  import { newtonRaphson } from '../solvers/newton-raphson.js';
19
- import { nelderMead } from '../solvers/nelder-mead.js';
20
- import { validateBackwardState } from './solution-validation.js';
21
- import { backwardConstraintTolerance } from './tolerances.js';
22
- import { sumNormalizedResiduals } from './objective-normalization.js';
20
+ import { bracketedNewton } from '../solvers/bracketed-newton.js';
21
+ import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
22
+ import { validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
23
+ import { helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
23
24
  import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtPressure, } from '../saturation/common.js';
24
25
  import { assertCriticalRegion4PHInput, assertRegion4StateAllowed, } from '../saturation/region4-boundaries.js';
25
26
  // ─── Backward Polynomial Evaluator ─────────────────────────────────────────
@@ -39,7 +40,7 @@ const R1_PH = [
39
40
  [3, 10, 6.6456186191635e-8], [3, 32, 8.0670734103027e-11], [4, 32, -9.3477771213947e-13],
40
41
  [5, 32, 5.8265442020601e-15], [6, 32, -1.5020185953503e-17],
41
42
  ];
42
- function r1BackwardT(p, h) {
43
+ export function r1BackwardT(p, h) {
43
44
  return evalPoly(R1_PH, 0, -1, p, h / 2500);
44
45
  }
45
46
  // ─── Region 2 Backward T(P,H) ─────────────────────────────────────────────
@@ -47,9 +48,10 @@ function r1BackwardT(p, h) {
47
48
  // For 4 < p <= 6.546699678 MPa the supplementary release selects subregion 2b
48
49
  // directly; above this threshold use the B2bc(h) boundary to discriminate 2b/2c.
49
50
  const R2_PH_B2BC_P_MIN = 6.546699678;
50
- // B2bc boundary (exported for future use)
51
+ /** B2bc boundary: h → p, discriminating subregions 2b and 2c.
52
+ * Ref: IAPWS-IF97, Eq. 20. Check point: h = 3516.004323 → p = 100 MPa. */
51
53
  export function b2bc_H_P(h) {
52
- return 0.90584278514723e-3 - 0.67955786399241 * h + 0.12809002730136e-3 * h * h;
54
+ return 0.90584278514723e3 - 0.67955786399241 * h + 0.12809002730136e-3 * h * h;
53
55
  }
54
56
  // Subregion 2a — Eq. 22
55
57
  const R2A_PH = [
@@ -60,7 +62,7 @@ const R2A_PH = [
60
62
  [2, 2, -0.31078046629583], [2, 7, 11.670873077107], [2, 36, 128127984.04046], [2, 38, -985549096.23276],
61
63
  [2, 40, 2822454697.3002], [2, 42, -3594897141.0703], [2, 44, 1722734991.3197], [3, 24, -13551.334240775],
62
64
  [3, 44, 12848734.66465], [4, 12, 1.3865724283226], [4, 32, 235988.32556514], [4, 44, -13105236.545054],
63
- [5, 32, 7399.9835474766], [5, 36, -551966.9703006], [5, 42, 3715408.5996233], [6, 34, 19127.7292396],
65
+ [5, 32, 7399.9835474766], [5, 36, -551966.9703006], [5, 42, 3715408.5996233], [6, 34, 19127.729239660],
64
66
  [6, 44, -415351.64835634], [7, 28, -62.459855192507],
65
67
  ];
66
68
  function r2aBackwardT(p, h) {
@@ -96,7 +98,7 @@ const R2C_PH = [
96
98
  function r2cBackwardT(p, h) {
97
99
  return evalPoly(R2C_PH, -25, 1.8, p, h / 2000);
98
100
  }
99
- function r2BackwardT(p, h) {
101
+ export function r2BackwardT(p, h) {
100
102
  let T;
101
103
  if (p <= C.R2_P_CRT) {
102
104
  T = r2aBackwardT(p, h);
@@ -173,22 +175,39 @@ const R3B_PH_V = [
173
175
  [-1, 5, -11.8035753702231], [0, 0, 0.169490044091791], [1, 0, -0.0179967222507787],
174
176
  [1, 1, 0.0371810116332674], [2, 2, -0.0536288335065096], [2, 6, 1.6069710109252],
175
177
  ];
176
- // R4: saturation pressure boundary from enthalpy
177
- const R4_H_PSAT = [
178
- [0, 0, 0.600073641753024], [1, 1, -9.36203654849857], [1, 3, 24.6590798594147],
179
- [1, 4, -107.014222858224], [1, 36, -9.15821315805768e13], [5, 3, -8625.32011700662],
180
- [7, 0, -23.5837344740032], [8, 24, 2.52304969384128e17], [14, 16, -3.89718771997719e18],
181
- [20, 16, -3.33775713645296e22], [22, 3, 3.56499469636328e10], [24, 18, -1.48547544720641e26],
182
- [28, 8, 3.30611514838798e18], [36, 24, 8.13641294467829e37],
183
- ];
184
- /** R4: enthalpy → saturation pressure for PH boundary detection */
185
- export function r4EnthalpyToPsat(h) {
186
- const mu = h / 2600;
187
- let p = 0;
188
- for (const [I, J, N] of R4_H_PSAT) {
189
- p += N * Math.pow(mu - 1.02, I) * Math.pow(mu - 0.608, J);
178
+ export function r3BackwardPHInitial(p, h) {
179
+ if (h < b3ab_P_to_H(p)) {
180
+ const T = 760 * evalPoly(R3A_PH_T, -0.240, 0.615, p / 100, h / 2300);
181
+ const v = 0.0028 * evalPoly(R3A_PH_V, -0.128, 0.727, p / 100, h / 2100);
182
+ return { rho: 1 / v, T };
190
183
  }
191
- return 22 * p;
184
+ const T = 860 * evalPoly(R3B_PH_T, -0.298, 0.720, p / 100, h / 2800);
185
+ const v = 0.0088 * evalPoly(R3B_PH_V, -0.0661, 0.720, p / 100, h / 2800);
186
+ return { rho: 1 / v, T };
187
+ }
188
+ function solveRegion3PH(p, h, rho0, T0, refinePressure = false) {
189
+ // Scaling the 1e-13 solver tolerance by 1e-4 resolves pressure below one ULP before PT reconstruction.
190
+ const pScale = Math.max(1, Math.abs(p)) * (refinePressure ? 1e-4 : 1);
191
+ const hScale = Math.max(1, Math.abs(h));
192
+ const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
193
+ const state = region3ByRhoT(candidateRho, candidateT);
194
+ const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
195
+ return {
196
+ residual: [
197
+ (refinePressure ? region3PressureResidual(candidateRho, candidateT, p) : state.pressure - p) / pScale,
198
+ (state.enthalpy - h) / hScale,
199
+ ],
200
+ jacobian: [
201
+ [derivatives.dpDrho / pScale, derivatives.dpDT / pScale],
202
+ [derivatives.dhDrho / hScale, derivatives.dhDT / hScale],
203
+ ],
204
+ };
205
+ }, [rho0, T0], {
206
+ // Near critical pressure, the default residual can cross a stability boundary.
207
+ tolerance: 1e-13,
208
+ isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
209
+ });
210
+ return region3ByRhoT(rho, T);
192
211
  }
193
212
  // ─── Main PH Solver ────────────────────────────────────────────────────────
194
213
  /**
@@ -202,52 +221,81 @@ export function solvePH(p, h) {
202
221
  if (p < C.P_MIN || p > C.P_MAX) {
203
222
  throw new OutOfRangeError('Pressure', p, C.P_MIN, C.P_MAX);
204
223
  }
205
- if (h < C.H_MIN || h > C.H_MAX) {
224
+ if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
206
225
  throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
207
226
  }
208
227
  assertCriticalRegion4PHInput(p, h, 'solvePH');
209
- const region = detectRegionPH(p, h);
228
+ assertPressurePropertyEnvelope(p, h, 'enthalpy');
229
+ const seam = pressurePropertySeam(p, h, 'enthalpy');
230
+ if (seam !== null && seam.kind !== 'overlap') {
231
+ return validateBackwardState(seam.state, [
232
+ { label: 'pressure', expected: p },
233
+ { label: 'enthalpy', expected: h },
234
+ ], { solverName: 'solvePH', expectedRegion: seam.state.region, seam });
235
+ }
236
+ if (seam === null) {
237
+ const boundary = pressurePropertyBoundaryState(p, h, 'enthalpy');
238
+ if (boundary !== null)
239
+ return boundary;
240
+ }
241
+ let region;
242
+ let saturationFailure = null;
243
+ try {
244
+ region = seam?.state.region ?? detectRegionPH(p, h);
245
+ }
246
+ catch (error) {
247
+ if (!(error instanceof SaturationEndpointError)) {
248
+ throw error;
249
+ }
250
+ saturationFailure = error;
251
+ region = Region.Region3;
252
+ }
210
253
  switch (region) {
211
254
  case Region.Region1: {
212
255
  const T0 = r1BackwardT(p, h);
213
- const T = newtonRaphson((T_x) => region1(p, T_x).enthalpy - h, T0);
256
+ const T = newtonRaphson((T_x) => region1(p, T_x).enthalpy - h, T0, (T_x) => region1(p, T_x).cp ?? Number.NaN);
214
257
  return validateBackwardState(region1(p, T), [
215
258
  { label: 'pressure', expected: p },
216
259
  { label: 'enthalpy', expected: h },
217
260
  ], { solverName: 'solvePH', expectedRegion: Region.Region1 });
218
261
  }
219
262
  case Region.Region2: {
263
+ const targetH = Math.min(h, region2(p, C.R2_T_MAX).enthalpy);
220
264
  const T0 = r2BackwardT(p, h);
221
- const T = newtonRaphson((T_x) => region2(p, T_x).enthalpy - h, T0);
265
+ const T = newtonRaphson((T_x) => region2(p, T_x).enthalpy - targetH, T0, (T_x) => region2(p, T_x).cp ?? Number.NaN);
222
266
  return validateBackwardState(region2(p, T), [
223
267
  { label: 'pressure', expected: p },
224
268
  { label: 'enthalpy', expected: h },
225
269
  ], { solverName: 'solvePH', expectedRegion: Region.Region2 });
226
270
  }
227
271
  case Region.Region3: {
228
- const hBound = b3ab_P_to_H(p);
229
- const pressureTolerance = 1e-5 * Math.max(1, Math.abs(p));
230
- const enthalpyTolerance = backwardConstraintTolerance('enthalpy', h);
231
- let T0, v0;
232
- if (h < hBound) {
233
- T0 = 760 * evalPoly(R3A_PH_T, -0.240, 0.615, p / 100, h / 2300);
234
- v0 = 0.0028 * evalPoly(R3A_PH_V, -0.128, 0.727, p / 100, h / 2100);
272
+ try {
273
+ const initial = r3BackwardPHInitial(p, h);
274
+ const validate = (state) => validateBackwardState(state, [
275
+ { label: 'pressure', expected: p },
276
+ { label: 'enthalpy', expected: h },
277
+ ], { solverName: 'solvePH', expectedRegion: Region.Region3 });
278
+ const validated = validate(solveRegion3PH(p, h, initial.rho, initial.T));
279
+ // Compressibility can amplify an accepted pressure residual beyond PT's density accuracy.
280
+ const relativeDensityError = Math.abs(region3PressureResidual(1 / validated.specificVolume, validated.temperature, p)) * validated.isothermalCompressibility;
281
+ if (saturationFailure === null && relativeDensityError <= 1e-12)
282
+ return validated;
283
+ try {
284
+ const refined = validate(solveRegion3PH(p, h, 1 / validated.specificVolume, validated.temperature, true));
285
+ validateRegion3PTReconstruction(refined, 'solvePH');
286
+ return refined;
287
+ }
288
+ catch (error) {
289
+ if (!(error instanceof IF97Error))
290
+ throw error;
291
+ return saturationFailure === null ? validated : validateRegion3PTReconstruction(validated, 'solvePH');
292
+ }
235
293
  }
236
- else {
237
- T0 = 860 * evalPoly(R3B_PH_T, -0.298, 0.720, p / 100, h / 2800);
238
- v0 = 0.0088 * evalPoly(R3B_PH_V, -0.0661, 0.720, p / 100, h / 2800);
294
+ catch (error) {
295
+ if (saturationFailure !== null && error instanceof IF97Error)
296
+ throw saturationFailure;
297
+ throw error;
239
298
  }
240
- const sol = nelderMead((pair) => {
241
- const state = region3ByRhoT(1 / pair[0], pair[1]);
242
- return sumNormalizedResiduals([
243
- { actual: state.enthalpy, expected: h, tolerance: enthalpyTolerance },
244
- { actual: state.pressure, expected: p, tolerance: pressureTolerance },
245
- ]);
246
- }, [v0, T0], { maxIterations: 1000, minErrorDelta: 1e-8, minTolerance: 1e-9 });
247
- return validateBackwardState(region3ByRhoT(1 / sol.x[0], sol.x[1]), [
248
- { label: 'pressure', expected: p, tolerance: pressureTolerance },
249
- { label: 'enthalpy', expected: h },
250
- ], { solverName: 'solvePH', expectedRegion: Region.Region3 });
251
299
  }
252
300
  case Region.Region4: {
253
301
  const endpoints = saturationEndpointsAtPressure(p);
@@ -259,7 +307,8 @@ export function solvePH(p, h) {
259
307
  ], { solverName: 'solvePH', expectedRegion: Region.Region4 });
260
308
  }
261
309
  case Region.Region5: {
262
- const T = newtonRaphson((T_x) => region5(p, T_x).enthalpy - h, 1500);
310
+ const targetH = Math.min(h, region5(p, C.R5_T_MAX).enthalpy);
311
+ const T = bracketedNewton((T_x) => region5(p, T_x).enthalpy - targetH, 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, tolerance: 1e-12 });
263
312
  return validateBackwardState(region5(p, T), [
264
313
  { label: 'pressure', expected: p },
265
314
  { label: 'enthalpy', expected: h },
@@ -269,4 +318,3 @@ export function solvePH(p, h) {
269
318
  throw new IF97Error(`Cannot determine region for P=${p} MPa, h=${h} kJ/kg`);
270
319
  }
271
320
  }
272
- //# sourceMappingURL=ph.js.map
@@ -0,0 +1,13 @@
1
+ import { Region } from '../types.js';
2
+ import type { BasicProperties } from '../types.js';
3
+ import { type FormulationSeamResolution } from '../core/formulation-seams.js';
4
+ /** Resolve the native saturation/B23 intersection before choosing one inverse region. */
5
+ export declare function pressurePropertyBoundaryState(p: number, target: number, property: 'enthalpy' | 'entropy'): BasicProperties | null;
6
+ /** Reject proven fixed-pressure envelope violations without classifying interior gaps. */
7
+ export declare function assertPressurePropertyEnvelope(p: number, target: number, property: 'enthalpy' | 'entropy'): void;
8
+ /** Share the fixed-pressure seam policy between detectors and backward solvers. */
9
+ export declare function pressurePropertySeam(p: number, target: number, property: 'enthalpy' | 'entropy'): FormulationSeamResolution | null;
10
+ /** Detect region for P-H inputs. */
11
+ export declare function detectRegionPH(p: number, h: number): Region | -1;
12
+ /** Detect region for P-S inputs. */
13
+ export declare function detectRegionPS(p: number, s: number): Region | -1;
@@ -0,0 +1,262 @@
1
+ import * as C from '../constants.js';
2
+ import { SaturationEndpointError } from '../errors.js';
3
+ import { region1 } from '../regions/region1.js';
4
+ import { region2 } from '../regions/region2.js';
5
+ import { region5 } from '../regions/region5.js';
6
+ import { boundary23_T_to_P } from '../regions/boundaries.js';
7
+ import { saturationPressure } from '../regions/region4.js';
8
+ import { saturationEndpointsAtPressure } from '../saturation/common.js';
9
+ import { OutOfRangeError, Region } from '../types.js';
10
+ import { detectRegionPT, nativeB23TemperatureBracket, nativeSaturationTemperatureBracket } from '../core/region-detector.js';
11
+ import { withinClosedLowerBound } from '../core/input-validation.js';
12
+ import { findPressureSeam } from '../core/formulation-seams.js';
13
+ import { nextPositiveFloat } from '../core/floating-point.js';
14
+ import { solveRegion3PTBasic } from '../core/region3-pt.js';
15
+ import { bracketedNewton } from '../solvers/bracketed-newton.js';
16
+ import { resolveBackwardCandidates } from './candidate-resolution.js';
17
+ import { BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE } from './tolerances.js';
18
+ const P13 = saturationPressure(C.R2_T_MIN);
19
+ const B23_P_AT_T13 = boundary23_T_to_P(C.R2_T_MIN);
20
+ /** Resolve the native saturation/B23 intersection before choosing one inverse region. */
21
+ export function pressurePropertyBoundaryState(p, target, property) {
22
+ if (p < C.B23_P_MIN || p > B23_P_AT_T13)
23
+ return null;
24
+ const references = [region1(p, C.R2_T_MIN), region2(p, C.R2_T_MIN)];
25
+ if (!references.some((state) => Math.abs(state[property] - target) <=
26
+ (property === 'enthalpy' ? 0.2 : 0.0002)))
27
+ return null;
28
+ const endpoints = saturationEndpointsAtPressure(p);
29
+ if (endpoints.liquid[property] <= target && target <= endpoints.vapor[property])
30
+ return null;
31
+ const tolerance = 128 * Number.EPSILON * Math.max(1, Math.abs(target));
32
+ const temperatures = new Set([
33
+ C.R2_T_MIN,
34
+ nextPositiveFloat(C.R2_T_MIN, -1),
35
+ nextPositiveFloat(C.R2_T_MIN, 1),
36
+ ...nativeSaturationTemperatureBracket(p),
37
+ ]);
38
+ const stateAt = (T) => {
39
+ const region = detectRegionPT(p, T);
40
+ return region === Region.Region1 ? region1(p, T)
41
+ : region === Region.Region2 ? region2(p, T)
42
+ : region === Region.Region3 ? solveRegion3PTBasic(p, T) : null;
43
+ };
44
+ return resolveBackwardCandidates([...temperatures].map((T) => () => {
45
+ const state = stateAt(T);
46
+ if (state === null)
47
+ return null;
48
+ if (Math.abs(state[property] - target) <= tolerance)
49
+ return state;
50
+ const slope = property === 'enthalpy' ? state.cp : state.cp / T;
51
+ const temperature = T + (target - state[property]) / slope;
52
+ if (detectRegionPT(p, temperature) !== state.region)
53
+ return null;
54
+ const corrected = stateAt(temperature);
55
+ return corrected !== null && Math.abs(corrected[property] - target) <= tolerance ? corrected : null;
56
+ }), [
57
+ { label: 'pressure', expected: p, tolerance: 0 },
58
+ { label: property, expected: target, tolerance },
59
+ ], { solverName: property === 'enthalpy' ? 'solvePH' : 'solvePS' });
60
+ }
61
+ /** Reject proven fixed-pressure envelope violations without classifying interior gaps. */
62
+ export function assertPressurePropertyEnvelope(p, target, property) {
63
+ const minimum = region1(p, C.T_MIN)[property];
64
+ const maximum = (p <= C.R5_P_MAX ? region5(p, C.R5_T_MAX) : region2(p, C.R2_T_MAX))[property];
65
+ const aboveMinimum = property === 'enthalpy'
66
+ ? target >= minimum || isClose(target, minimum)
67
+ : withinClosedLowerBound(target, minimum, 'entropy');
68
+ if (!aboveMinimum || !withinClosedUpperBound(target, maximum)) {
69
+ throw new OutOfRangeError(`${property === 'enthalpy' ? 'Enthalpy' : 'Entropy'} at p=${p} MPa`, target, minimum, maximum);
70
+ }
71
+ }
72
+ /** Share the fixed-pressure seam policy between detectors and backward solvers. */
73
+ export function pressurePropertySeam(p, target, property) {
74
+ const match = findPressureSeam(p, target, property);
75
+ if (match === null)
76
+ return null;
77
+ if (p >= C.Pt && p < C.Pc) {
78
+ try {
79
+ const endpoints = saturationEndpointsAtPressure(p);
80
+ if (target >= endpoints.liquid[property] && target <= endpoints.vapor[property])
81
+ return null;
82
+ }
83
+ catch (error) {
84
+ if (!(error instanceof SaturationEndpointError))
85
+ throw error;
86
+ }
87
+ }
88
+ const solverName = property === 'enthalpy' ? 'solvePH' : 'solvePS';
89
+ const constraints = [{ label: 'pressure', expected: p }, { label: property, expected: target }];
90
+ const state = resolveBackwardCandidates([
91
+ () => match.state[property] === target ? match.state : null,
92
+ () => pressureSeamRoot(p, target, property, match, match.pair.lower.region),
93
+ () => pressureSeamRoot(p, target, property, match, match.pair.upper.region),
94
+ ], constraints, { solverName, seam: match });
95
+ const resolved = state ?? (match.kind === 'canonical'
96
+ ? resolveBackwardCandidates([() => match.state], constraints, { solverName }) : null);
97
+ return resolved === null ? null : resolved === match.state ? match : { ...match, kind: 'canonical', state: resolved };
98
+ }
99
+ function pressureSeamRoot(p, target, property, seam, region) {
100
+ let lower;
101
+ let upper;
102
+ let evaluate;
103
+ if (region === Region.Region1) {
104
+ lower = C.T_MIN;
105
+ upper = C.R2_T_MIN;
106
+ evaluate = (T) => region1(p, T);
107
+ }
108
+ else if (region === Region.Region2) {
109
+ lower = p > C.B23_P_MIN ? nativeB23TemperatureBracket(p)[1] : C.T_MIN;
110
+ if (p < C.Pc)
111
+ lower = Math.max(lower, nativeSaturationTemperatureBracket(p)[1]);
112
+ upper = C.R2_T_MAX;
113
+ evaluate = (T) => region2(p, T);
114
+ }
115
+ else if (region === Region.Region3) {
116
+ lower = nextPositiveFloat(C.R2_T_MIN, 1);
117
+ upper = nativeB23TemperatureBracket(p)[0];
118
+ if (p < C.Pc) {
119
+ const [liquidT, vaporT] = nativeSaturationTemperatureBracket(p);
120
+ if (seam.pair.seam === '13')
121
+ upper = Math.min(upper, liquidT);
122
+ else
123
+ lower = Math.max(lower, vaporT);
124
+ }
125
+ evaluate = (T) => solveRegion3PTBasic(p, T);
126
+ }
127
+ else if (region === Region.Region5) {
128
+ lower = nextPositiveFloat(C.R5_T_MIN, 1);
129
+ upper = C.R5_T_MAX;
130
+ evaluate = (T) => region5(p, T);
131
+ }
132
+ else {
133
+ return null;
134
+ }
135
+ if (lower > upper)
136
+ return null;
137
+ const direction = region === seam.pair.lower.region ? -1 : 1;
138
+ const boundary = direction < 0 ? upper : lower;
139
+ const first = evaluate(boundary);
140
+ if (direction * (target - first[property]) < 0)
141
+ return null;
142
+ if (target === first[property])
143
+ return first;
144
+ if (lower === upper)
145
+ return null;
146
+ const slope = property === 'enthalpy' ? first.cp : first.cp / boundary;
147
+ let distance = Math.abs(target - first[property]) / slope;
148
+ let bracket;
149
+ while (true) {
150
+ bracket = Math.max(lower, Math.min(upper, boundary + direction * distance));
151
+ if (bracket === boundary)
152
+ bracket = nextPositiveFloat(boundary, direction);
153
+ const last = evaluate(bracket);
154
+ if (direction * (last[property] - target) >= 0)
155
+ break;
156
+ if (bracket === lower || bracket === upper)
157
+ return null;
158
+ distance *= 2;
159
+ }
160
+ lower = Math.min(boundary, bracket);
161
+ upper = Math.max(boundary, bracket);
162
+ const temperature = bracketedNewton((T) => evaluate(T)[property] - target, lower, upper, undefined, {
163
+ derivative: (T) => {
164
+ const state = evaluate(T);
165
+ return property === 'enthalpy' ? state.cp : state.cp / T;
166
+ },
167
+ tolerance: BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE,
168
+ residualTolerance: 0,
169
+ });
170
+ return evaluate(temperature);
171
+ }
172
+ /** Detect region for P-H inputs. */
173
+ export function detectRegionPH(p, h) {
174
+ return detectRegionPressureProperty(p, h, 'enthalpy');
175
+ }
176
+ /** Detect region for P-S inputs. */
177
+ export function detectRegionPS(p, s) {
178
+ return detectRegionPressureProperty(p, s, 'entropy');
179
+ }
180
+ function detectRegionPressureProperty(p, target, property) {
181
+ if (!Number.isFinite(p) || !Number.isFinite(target))
182
+ return -1;
183
+ const seam = pressurePropertySeam(p, target, property);
184
+ if (seam !== null)
185
+ return seam.state.region;
186
+ const boundary = pressurePropertyBoundaryState(p, target, property);
187
+ if (boundary !== null)
188
+ return boundary.region;
189
+ // Preserve PH's existing lower-bound tolerance; PS absorbs EOS rounding noise.
190
+ const aboveMinimum = (minimum) => property === 'enthalpy'
191
+ ? minimum <= target || isClose(target, minimum)
192
+ : withinClosedLowerBound(target, minimum, 'entropy');
193
+ if (p >= C.P_MIN && p <= P13) {
194
+ const endpoints = p >= C.Pt ? saturationEndpointsAtPressure(p) : null;
195
+ if (endpoints !== null && endpoints.liquid[property] <= target && target <= endpoints.vapor[property]) {
196
+ return Region.Region4;
197
+ }
198
+ const [liquidT, vaporT] = endpoints === null ? nativeSaturationTemperatureBracket(p)
199
+ : [endpoints.temperature, endpoints.temperature];
200
+ const lower = region1(p, C.T_MIN)[property];
201
+ const boundary14 = endpoints?.liquid[property] ?? region1(p, liquidT)[property];
202
+ const boundary24 = endpoints?.vapor[property] ?? region2(p, vaporT)[property];
203
+ const boundary25 = region2(p, C.R2_T_MAX)[property];
204
+ if (aboveMinimum(lower) && target <= boundary14)
205
+ return Region.Region1;
206
+ if (withinClosedLowerBound(target, boundary24, property) && withinClosedUpperBound(target, boundary25))
207
+ return Region.Region2;
208
+ if (p <= C.R5_P_MAX && region5(p, C.R5_T_MIN)[property] < target &&
209
+ withinClosedUpperBound(target, region5(p, C.R5_T_MAX)[property]))
210
+ return Region.Region5;
211
+ }
212
+ else if (p > P13 && p <= C.P_MAX) {
213
+ const lower = region1(p, C.T_MIN)[property];
214
+ const boundary13 = region1(p, C.R2_T_MIN)[property];
215
+ const [, region2T] = nativeB23TemperatureBracket(p);
216
+ const boundary32 = region2(p, region2T)[property];
217
+ const boundary25 = region2(p, C.R2_T_MAX)[property];
218
+ if (p < C.Pc) {
219
+ try {
220
+ const endpoints = saturationEndpointsAtPressure(p);
221
+ if (endpoints.liquid[property] <= target && target <= endpoints.vapor[property]) {
222
+ return Region.Region4;
223
+ }
224
+ }
225
+ catch (error) {
226
+ if (!(error instanceof SaturationEndpointError) ||
227
+ (target > boundary13 && target < boundary32))
228
+ throw error;
229
+ }
230
+ }
231
+ if (aboveMinimum(lower) && target <= boundary13)
232
+ return Region.Region1;
233
+ if (withinClosedLowerBound(target, boundary32, property) && withinClosedUpperBound(target, boundary25))
234
+ return Region.Region2;
235
+ if (p <= C.R5_P_MAX && region5(p, C.R5_T_MIN)[property] < target &&
236
+ withinClosedUpperBound(target, region5(p, C.R5_T_MAX)[property]))
237
+ return Region.Region5;
238
+ if (p < C.Pc) {
239
+ const endpoints = saturationEndpointsAtPressure(p);
240
+ const liquid = endpoints.liquid[property];
241
+ const vapor = endpoints.vapor[property];
242
+ if (boundary13 < target && target < liquid)
243
+ return Region.Region3;
244
+ if (liquid < target && target < vapor)
245
+ return Region.Region4;
246
+ if (vapor < target && target < boundary32)
247
+ return Region.Region3;
248
+ }
249
+ else if (boundary13 < target && target < boundary32) {
250
+ return Region.Region3;
251
+ }
252
+ }
253
+ return -1;
254
+ }
255
+ // Accept floating-point residue only at closed upper bounds.
256
+ function withinClosedUpperBound(value, boundary) {
257
+ return value <= boundary + 8 * Number.EPSILON * Math.max(1, Math.abs(boundary));
258
+ }
259
+ /** Check approximate equality with relative tolerance. */
260
+ function isClose(a, b) {
261
+ return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
262
+ }
@@ -4,10 +4,15 @@
4
4
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations
5
5
  */
6
6
  import type { BasicProperties } from '../types.js';
7
+ export declare function r1BackwardT(p: number, s: number): number;
8
+ export declare function r2BackwardT(p: number, s: number): number;
9
+ export declare function r3BackwardPSInitial(p: number, s: number): {
10
+ rho: number;
11
+ T: number;
12
+ };
7
13
  /**
8
14
  * Solve for thermodynamic state given P and S.
9
15
  * @param p - Pressure [MPa]
10
16
  * @param s - Specific entropy [kJ/(kg·K)]
11
17
  */
12
18
  export declare function solvePS(p: number, s: number): BasicProperties;
13
- //# sourceMappingURL=ps.d.ts.map