iapws-if97 2.1.5 → 2.2.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 (87) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +134 -94
  3. package/dist/backward/fixed-temperature-solver.d.ts +6 -6
  4. package/dist/backward/fixed-temperature-solver.js +34 -8
  5. package/dist/backward/hs.d.ts +1 -2
  6. package/dist/backward/hs.js +66 -56
  7. package/dist/backward/ph.d.ts +8 -5
  8. package/dist/backward/ph.js +43 -46
  9. package/dist/backward/ps.d.ts +0 -1
  10. package/dist/backward/ps.js +30 -18
  11. package/dist/backward/solution-validation.d.ts +0 -1
  12. package/dist/backward/solution-validation.js +33 -5
  13. package/dist/backward/th.d.ts +0 -1
  14. package/dist/backward/th.js +4 -6
  15. package/dist/backward/thermodynamic-derivatives.d.ts +17 -0
  16. package/dist/backward/thermodynamic-derivatives.js +27 -0
  17. package/dist/backward/tolerances.d.ts +0 -1
  18. package/dist/backward/tolerances.js +0 -1
  19. package/dist/backward/ts.d.ts +0 -1
  20. package/dist/backward/ts.js +4 -6
  21. package/dist/boundaries.d.ts +0 -1
  22. package/dist/boundaries.js +0 -1
  23. package/dist/constants.d.ts +0 -9
  24. package/dist/constants.js +3 -13
  25. package/dist/core/input-validation.d.ts +0 -1
  26. package/dist/core/input-validation.js +0 -1
  27. package/dist/core/public-normalization.d.ts +0 -1
  28. package/dist/core/public-normalization.js +0 -1
  29. package/dist/core/region-detector.d.ts +0 -1
  30. package/dist/core/region-detector.js +232 -65
  31. package/dist/core/region3-pt.d.ts +12 -1
  32. package/dist/core/region3-pt.js +38 -9
  33. package/dist/core/solve-input-normalization.d.ts +0 -1
  34. package/dist/core/solve-input-normalization.js +0 -1
  35. package/dist/core/solver.d.ts +0 -1
  36. package/dist/core/solver.js +0 -1
  37. package/dist/detect.d.ts +0 -1
  38. package/dist/detect.js +0 -1
  39. package/dist/index.d.ts +0 -1
  40. package/dist/index.js +0 -1
  41. package/dist/regions/boundaries.d.ts +0 -1
  42. package/dist/regions/boundaries.js +0 -1
  43. package/dist/regions/region1.d.ts +0 -1
  44. package/dist/regions/region1.js +0 -1
  45. package/dist/regions/region2.d.ts +0 -1
  46. package/dist/regions/region2.js +0 -1
  47. package/dist/regions/region3-data.d.ts +0 -1
  48. package/dist/regions/region3-data.js +0 -1
  49. package/dist/regions/region3-eval.d.ts +0 -1
  50. package/dist/regions/region3-eval.js +0 -1
  51. package/dist/regions/region3-subregions.d.ts +0 -1
  52. package/dist/regions/region3-subregions.js +0 -1
  53. package/dist/regions/region3.d.ts +0 -1
  54. package/dist/regions/region3.js +0 -1
  55. package/dist/regions/region4.d.ts +0 -1
  56. package/dist/regions/region4.js +0 -1
  57. package/dist/regions/region5.d.ts +0 -1
  58. package/dist/regions/region5.js +0 -1
  59. package/dist/regions.d.ts +0 -1
  60. package/dist/regions.js +0 -1
  61. package/dist/saturation/common.d.ts +0 -1
  62. package/dist/saturation/common.js +6 -13
  63. package/dist/saturation/region4-boundaries.d.ts +0 -5
  64. package/dist/saturation/region4-boundaries.js +4 -5
  65. package/dist/saturation/region4-hs.d.ts +2 -1
  66. package/dist/saturation/region4-hs.js +277 -108
  67. package/dist/saturation/two-phase.d.ts +0 -1
  68. package/dist/saturation/two-phase.js +0 -1
  69. package/dist/saturation.d.ts +0 -1
  70. package/dist/saturation.js +0 -1
  71. package/dist/solvers/bracketed-newton.d.ts +5 -4
  72. package/dist/solvers/bracketed-newton.js +5 -6
  73. package/dist/solvers/damped-newton-2d.d.ts +12 -0
  74. package/dist/solvers/damped-newton-2d.js +68 -0
  75. package/dist/solvers/newton-raphson.d.ts +0 -1
  76. package/dist/solvers/newton-raphson.js +0 -1
  77. package/dist/transport/properties.d.ts +0 -1
  78. package/dist/transport/properties.js +0 -1
  79. package/dist/transport.d.ts +0 -1
  80. package/dist/transport.js +0 -1
  81. package/dist/types.d.ts +0 -1
  82. package/dist/types.js +0 -1
  83. package/package.json +17 -9
  84. package/dist/backward/objective-normalization.d.ts +0 -7
  85. package/dist/backward/objective-normalization.js +0 -8
  86. package/dist/solvers/nelder-mead.d.ts +0 -17
  87. package/dist/solvers/nelder-mead.js +0 -116
@@ -7,9 +7,9 @@ import { boundary23_P_to_T, boundary23_T_to_P } from '../regions/boundaries.js';
7
7
  import { region1 } from '../regions/region1.js';
8
8
  import { region2 } from '../regions/region2.js';
9
9
  import { region5 } from '../regions/region5.js';
10
- import { saturationEndpointsAtPressure, saturationEndpointsAtTemperature } from '../saturation/common.js';
10
+ import { saturationEndpointsAtPressure, saturationEndpointsAtTemperature, } from '../saturation/common.js';
11
11
  import { tryRegion4HSState } from '../saturation/region4-hs.js';
12
- import { Region } from '../types.js';
12
+ import { IF97Error, Region } from '../types.js';
13
13
  import { bracketedNewton } from '../solvers/bracketed-newton.js';
14
14
  import { solveRegion3PTBasic } from './region3-pt.js';
15
15
  /**
@@ -331,8 +331,17 @@ function h1Sat(s) {
331
331
  * Region 1 enthalpy maximum would admit impossible h-s pairs.
332
332
  */
333
333
  function h1UpperBoundAtPmax(s) {
334
- const temperature = bracketedNewton((T) => region1(C.P_MAX, T).entropy - s, C.T_MIN, C.R2_T_MIN, 350);
335
- return region1(C.P_MAX, temperature).enthalpy;
334
+ let cachedTemperature = Number.NaN;
335
+ let cachedState = null;
336
+ const stateAt = (T) => {
337
+ if (T !== cachedTemperature || cachedState === null) {
338
+ cachedTemperature = T;
339
+ cachedState = region1(C.P_MAX, T);
340
+ }
341
+ return cachedState;
342
+ };
343
+ const temperature = bracketedNewton((T) => stateAt(T).entropy - s, C.T_MIN, C.R2_T_MIN, 350, { derivative: (T) => (stateAt(T).cp ?? Number.NaN) / T });
344
+ return stateAt(temperature).enthalpy;
336
345
  }
337
346
  /**
338
347
  * Region 1 / Region 3 boundary enthalpy (Eq. 1, B13).
@@ -465,6 +474,175 @@ function backward2cPhs(h, s) {
465
474
  }
466
475
  return 100 * P * P * P * P;
467
476
  }
477
+ const HS_R1_PMAX_623 = region1(C.P_MAX, C.R2_T_MIN);
478
+ const HS_R1_B23_MIN = region1(C.B23_P_MIN, C.R2_T_MIN);
479
+ const HS_R2_PMAX_TMAX = region2(C.P_MAX, C.R2_T_MAX);
480
+ const HS_R2_PMIN_TMAX = region2(C.P_MIN, C.R2_T_MAX);
481
+ const HS_R2_B23_MIN = region2(C.B23_P_MIN, C.R2_T_MIN);
482
+ const HS_R2_B23_MAX = region2(C.P_MAX, C.B23_T_MAX);
483
+ const HS_R2_R5_PMAX = region2(C.R5_P_MAX, C.R2_T_MAX);
484
+ const HS_R5_LOWER = region5(C.R5_P_MAX, C.R5_T_MIN);
485
+ const HS_R5_PMIN_TMIN = region5(C.P_MIN, C.R5_T_MIN);
486
+ const HS_R5_UPPER = region5(C.P_MIN, C.R5_T_MAX);
487
+ const HS_TRIPLE_POINT_ENDPOINTS = saturationEndpointsAtTemperature(C.Tt);
488
+ const HS_TRIPLE_LIQUID = HS_TRIPLE_POINT_ENDPOINTS.liquid;
489
+ const HS_TRIPLE_VAPOR = HS_TRIPLE_POINT_ENDPOINTS.vapor;
490
+ const HS_S13 = HS_R1_PMAX_623.entropy;
491
+ const HS_S13S = HS_R1_B23_MIN.entropy;
492
+ const HS_S_TP_MAX = HS_R2_PMAX_TMAX.entropy;
493
+ const HS_S2AB = region2(C.R2_P_CRT, C.R2_T_MAX).entropy;
494
+ // Covers the 0.0073 kJ/kg maximum boundary-equation error reported by IAPWS.
495
+ const HS_SATURATION_BOUNDARY_CONFIRMATION_BAND = 0.01;
496
+ const HS_SATURATION_BOUNDARY_ENTHALPY_TOLERANCE = 1e-8;
497
+ const HS_SATURATION_INVERSION_UPPER_T = C.Tc - C.REGION4_SUBCRITICAL_TEMPERATURE_MARGIN;
498
+ function hSatLow(s) {
499
+ return HS_TRIPLE_LIQUID.enthalpy +
500
+ (s - HS_TRIPLE_LIQUID.entropy) /
501
+ (HS_TRIPLE_VAPOR.entropy - HS_TRIPLE_LIQUID.entropy) *
502
+ (HS_TRIPLE_VAPOR.enthalpy - HS_TRIPLE_LIQUID.enthalpy);
503
+ }
504
+ function phaseState(endpoints, phase) {
505
+ return phase === 'liquid' ? endpoints.liquid : endpoints.vapor;
506
+ }
507
+ /**
508
+ * Invert one saturation endpoint's monotone entropy curve without scanning.
509
+ * The Clapeyron relation supplies dp_sat/dT, and the Gibbs Maxwell relation
510
+ * supplies the endpoint derivative along that curve.
511
+ */
512
+ function saturationBoundaryEnthalpyAtEntropy(s, phase) {
513
+ let lower = C.Tt;
514
+ let upper = HS_SATURATION_INVERSION_UPPER_T;
515
+ const tripleEntropy = phaseState(HS_TRIPLE_POINT_ENDPOINTS, phase).entropy;
516
+ const criticalEntropy = C.R3_S_CRT;
517
+ const fraction = phase === 'liquid'
518
+ ? (s - tripleEntropy) / (criticalEntropy - tripleEntropy)
519
+ : (tripleEntropy - s) / (tripleEntropy - criticalEntropy);
520
+ const boundedFraction = Math.max(0, Math.min(1, fraction));
521
+ let temperature = lower + boundedFraction * (upper - lower);
522
+ for (let iteration = 0; iteration < 12; iteration += 1) {
523
+ const endpoints = saturationEndpointsAtTemperature(temperature);
524
+ const state = phaseState(endpoints, phase);
525
+ const residual = state.entropy - s;
526
+ if (Math.abs(residual) <= 1e-12 * Math.max(1, Math.abs(s))) {
527
+ return state.enthalpy;
528
+ }
529
+ const residualIncreasesWithTemperature = phase === 'liquid';
530
+ if ((residual < 0) === residualIncreasesWithTemperature)
531
+ lower = temperature;
532
+ else
533
+ upper = temperature;
534
+ if (upper - lower <= 10 * C.REGION4_TEMPERATURE_TOLERANCE) {
535
+ return state.enthalpy;
536
+ }
537
+ const entropySpan = endpoints.vapor.entropy - endpoints.liquid.entropy;
538
+ const volumeSpan = endpoints.vapor.specificVolume - endpoints.liquid.specificVolume;
539
+ const dpSatDT = entropySpan / (1000 * volumeSpan);
540
+ const cp = state.cp;
541
+ const alpha = state.isobaricExpansion;
542
+ const derivative = cp === null || alpha === null
543
+ ? Number.NaN
544
+ : cp / temperature - 1000 * state.specificVolume * alpha * dpSatDT;
545
+ const candidate = temperature - residual / derivative;
546
+ temperature = Number.isFinite(candidate) && candidate > lower && candidate < upper
547
+ ? candidate
548
+ : (lower + upper) / 2;
549
+ }
550
+ return null;
551
+ }
552
+ /** Replace the supplementary polynomial only in its narrow error band. */
553
+ function refinedSaturationBoundaryEnthalpy(h, s, approximateBoundaryH) {
554
+ if (Math.abs(h - approximateBoundaryH) > HS_SATURATION_BOUNDARY_CONFIRMATION_BAND) {
555
+ return approximateBoundaryH;
556
+ }
557
+ const phase = s <= C.R3_S_CRT ? 'liquid' : 'vapor';
558
+ let exactBoundaryH;
559
+ try {
560
+ exactBoundaryH = saturationBoundaryEnthalpyAtEntropy(s, phase);
561
+ }
562
+ catch (error) {
563
+ if (error instanceof IF97Error)
564
+ return approximateBoundaryH;
565
+ throw error;
566
+ }
567
+ return exactBoundaryH === null
568
+ ? approximateBoundaryH
569
+ : exactBoundaryH + HS_SATURATION_BOUNDARY_ENTHALPY_TOLERANCE;
570
+ }
571
+ function confirmedRegion4BoundaryState(h, s, approximateBoundaryH) {
572
+ if (Math.abs(h - approximateBoundaryH) > HS_SATURATION_BOUNDARY_CONFIRMATION_BAND) {
573
+ return null;
574
+ }
575
+ try {
576
+ return tryRegion4HSState(h, s) !== null;
577
+ }
578
+ catch (error) {
579
+ if (error instanceof IF97Error)
580
+ return false;
581
+ throw error;
582
+ }
583
+ }
584
+ function r2R5BoundaryEnthalpy(s, lowEntropy, highEntropy, stateAtPressure) {
585
+ if (s < lowEntropy || s > highEntropy)
586
+ return null;
587
+ let lower = C.P_MIN;
588
+ let upper = C.R5_P_MAX;
589
+ const fraction = (highEntropy - s) / (highEntropy - lowEntropy);
590
+ let p = Math.exp(Math.log(lower) + fraction * (Math.log(upper) - Math.log(lower)));
591
+ for (let iteration = 0; iteration < 12; iteration += 1) {
592
+ const state = stateAtPressure(p);
593
+ const residual = state.entropy - s;
594
+ if (Math.abs(residual) <= 1e-12 * Math.max(1, Math.abs(s))) {
595
+ return state.enthalpy;
596
+ }
597
+ if (residual > 0)
598
+ lower = p;
599
+ else
600
+ upper = p;
601
+ const alpha = state.isobaricExpansion;
602
+ const derivative = alpha === null ? Number.NaN : -1000 * state.specificVolume * alpha;
603
+ const candidate = p - residual / derivative;
604
+ p = Number.isFinite(candidate) && candidate > lower && candidate < upper
605
+ ? candidate
606
+ : (lower + upper) / 2;
607
+ }
608
+ return stateAtPressure(p).enthalpy;
609
+ }
610
+ function classifyRegion5HS(h, s) {
611
+ if (!(HS_R5_LOWER.entropy < s && s <= HS_R5_UPPER.entropy &&
612
+ HS_R5_LOWER.enthalpy < h && h <= HS_R5_UPPER.enthalpy)) {
613
+ return null;
614
+ }
615
+ const region2BoundaryH = r2R5BoundaryEnthalpy(s, HS_R2_R5_PMAX.entropy, HS_R2_PMIN_TMAX.entropy, (p) => region2(p, C.R2_T_MAX));
616
+ const region5BoundaryH = r2R5BoundaryEnthalpy(s, HS_R5_LOWER.entropy, HS_R5_PMIN_TMIN.entropy, (p) => region5(p, C.R5_T_MIN));
617
+ if (region2BoundaryH === null) {
618
+ return region5BoundaryH === null || h > region5BoundaryH
619
+ ? Region.Region5
620
+ : null;
621
+ }
622
+ if (region5BoundaryH === null) {
623
+ return h > region2BoundaryH ? Region.Region5 : null;
624
+ }
625
+ const tolerance = 1e-10 * Math.max(1, Math.abs(h), Math.abs(region2BoundaryH), Math.abs(region5BoundaryH));
626
+ if (region5BoundaryH > region2BoundaryH + tolerance) {
627
+ if (h > region5BoundaryH + tolerance)
628
+ return Region.Region5;
629
+ if (h > region2BoundaryH + tolerance)
630
+ return -1;
631
+ return null;
632
+ }
633
+ if (region2BoundaryH > region5BoundaryH + tolerance) {
634
+ if (h > region2BoundaryH + tolerance)
635
+ return Region.Region5;
636
+ if (h <= region5BoundaryH + tolerance)
637
+ return null;
638
+ // The independently fitted Region 2 and 5 equations overlap slightly at
639
+ // 1073.15 K. H-S is non-unique there; choose the branch nearest its seam.
640
+ return h <= (region2BoundaryH + region5BoundaryH) / 2
641
+ ? Region.Region5
642
+ : null;
643
+ }
644
+ return h > region2BoundaryH + tolerance ? Region.Region5 : null;
645
+ }
468
646
  /**
469
647
  * Detect the IAPWS-IF97 region for given H and S.
470
648
  * Implements the zone-based detection from the IAPWS supplementary release.
@@ -474,50 +652,30 @@ function backward2cPhs(h, s) {
474
652
  * @returns Region number (1–5) or -1 if out of range
475
653
  */
476
654
  export function detectRegionHS(h, s) {
477
- // Pre-compute boundary reference points
478
- const s13 = region1(C.P_MAX, C.R2_T_MIN).entropy;
479
- const s13s = region1(C.B23_P_MIN, C.R2_T_MIN).entropy;
480
- const sTPmax = region2(C.P_MAX, C.R2_T_MAX).entropy;
481
- const s2ab = region2(C.R2_P_CRT, C.R2_T_MAX).entropy;
482
- const region4State = tryRegion4HSState(h, s);
483
- if (region4State !== null) {
655
+ if (s >= HS_TRIPLE_LIQUID.entropy && s <= HS_TRIPLE_VAPOR.entropy &&
656
+ isClose(h, hSatLow(s))) {
484
657
  return Region.Region4;
485
658
  }
486
- const triplePointEndpoints = saturationEndpointsAtTemperature(C.Tt);
487
- const _sL = triplePointEndpoints.liquid;
488
- const h4l = _sL.enthalpy;
489
- const s4l = _sL.entropy;
490
- const _sV = triplePointEndpoints.vapor;
491
- const h4v = _sV.enthalpy;
492
- const s4v = _sV.entropy;
493
- const _Pmax = region2(C.P_MIN, C.R2_T_MAX);
494
- const smax = _Pmax.entropy;
495
- // Check Region 5 first (before R2 overlap zones)
496
- const r5lo = region5(C.R5_P_MAX, C.R5_T_MIN);
497
- const r5hi = region5(C.P_MIN, C.R5_T_MAX);
498
- if (r5lo.entropy < s && s <= r5hi.entropy &&
499
- r5lo.enthalpy < h && h <= r5hi.enthalpy) {
500
- return Region.Region5;
501
- }
502
- // Helper: linear interpolation for saturation lower bound
503
- const hSatLow = (sVal) => h4l + (sVal - s4l) / (s4v - s4l) * (h4v - h4l);
659
+ const highTemperatureRegion = classifyRegion5HS(h, s);
660
+ if (highTemperatureRegion !== null)
661
+ return highTemperatureRegion;
504
662
  // Zone 1: smin <= s <= s13 (Region 1 / Region 4)
505
- if (s >= _sL.entropy && s <= s13) {
663
+ if (s >= HS_TRIPLE_LIQUID.entropy && s <= HS_S13) {
506
664
  const hmin = hSatLow(s);
507
- const hs = h1Sat(s);
665
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h1Sat(s));
508
666
  const hmax = h1UpperBoundAtPmax(s);
509
- if (h >= hmin && h < hs)
667
+ if (h >= hmin && h <= hs)
510
668
  return Region.Region4;
511
669
  if (h >= hs && h <= hmax)
512
670
  return Region.Region1;
513
671
  }
514
672
  // Zone 2: s13 < s <= s13s (Region 1 / Region 3 / Region 4)
515
- else if (s > s13 && s <= s13s) {
673
+ else if (s > HS_S13 && s <= HS_S13S) {
516
674
  const hmin = hSatLow(s);
517
- const hs = h1Sat(s);
675
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h1Sat(s));
518
676
  const h13 = h13Boundary(s);
519
- const hmax = region1(C.P_MAX, C.R2_T_MIN).enthalpy * 1.1; // generous upper bound
520
- if (h >= hmin && h < hs)
677
+ const hmax = 2800; // Region 3 upper bound
678
+ if (h >= hmin && h <= hs)
521
679
  return Region.Region4;
522
680
  if (h >= hs && h < h13)
523
681
  return Region.Region1;
@@ -525,11 +683,16 @@ export function detectRegionHS(h, s) {
525
683
  return Region.Region3;
526
684
  }
527
685
  // Zone 3: s13s < s <= R3_CRT_S (Region 3 / Region 4)
528
- else if (s > s13s && s <= C.R3_S_CRT) {
686
+ else if (s > HS_S13S && s <= C.R3_S_CRT) {
529
687
  const hmin = hSatLow(s);
530
- const hs = h3aSat(s);
531
688
  const hmax = 2800; // generous upper bound for R3
532
- if (h >= hmin && h < hs)
689
+ const hs = h3aSat(s);
690
+ const confirmedRegion4 = confirmedRegion4BoundaryState(h, s, hs);
691
+ if (confirmedRegion4 === true)
692
+ return Region.Region4;
693
+ if (confirmedRegion4 === false && h >= hmin && h <= hmax)
694
+ return Region.Region3;
695
+ if (h >= hmin && h <= hs)
533
696
  return Region.Region4;
534
697
  if (h >= hs && h <= hmax)
535
698
  return Region.Region3;
@@ -537,9 +700,14 @@ export function detectRegionHS(h, s) {
537
700
  // Zone 4: R3_CRT_S < s < B23_S_CURVE_MIN (Region 3 / Region 4)
538
701
  else if (s > C.R3_S_CRT && s < C.B23_S_CURVE_MIN) {
539
702
  const hmin = hSatLow(s);
540
- const hs = h2c3bSat(s);
541
703
  const hmax = 2800; // generous upper bound for R3
542
- if (h >= hmin && h < hs)
704
+ const hs = h2c3bSat(s);
705
+ const confirmedRegion4 = confirmedRegion4BoundaryState(h, s, hs);
706
+ if (confirmedRegion4 === true)
707
+ return Region.Region4;
708
+ if (confirmedRegion4 === false && h >= hmin && h <= hmax)
709
+ return Region.Region3;
710
+ if (h >= hmin && h <= hs)
543
711
  return Region.Region4;
544
712
  if (h >= hs && h <= hmax)
545
713
  return Region.Region3;
@@ -547,11 +715,11 @@ export function detectRegionHS(h, s) {
547
715
  // Zone 5: B23_S_CURVE_MIN <= s < B23_S_CURVE_MAX (Region 2 / Region 3 / Region 4)
548
716
  else if (s >= C.B23_S_CURVE_MIN && s < C.B23_S_CURVE_MAX) {
549
717
  const hmin = hSatLow(s);
550
- const hs = h2c3bSat(s);
551
- const h23max = region2(C.P_MAX, C.B23_T_MAX).enthalpy;
552
- const h23min = region2(C.B23_P_MIN, C.R2_T_MIN).enthalpy;
553
- const hmax = region2(C.P_MAX, C.R2_T_MAX).enthalpy;
554
- if (hmin <= h && h < hs)
718
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h2c3bSat(s));
719
+ const h23max = HS_R2_B23_MAX.enthalpy;
720
+ const h23min = HS_R2_B23_MIN.enthalpy;
721
+ const hmax = HS_R2_PMAX_TMAX.enthalpy;
722
+ if (hmin <= h && h <= hs)
555
723
  return Region.Region4;
556
724
  if (hs <= h && h < h23min)
557
725
  return Region.Region3;
@@ -570,50 +738,49 @@ export function detectRegionHS(h, s) {
570
738
  // Zone 6: B23_S_CURVE_MAX <= s < R2_S_CRT (Region 2 / Region 4)
571
739
  else if (s >= C.B23_S_CURVE_MAX && s < C.R2_S_CRT) {
572
740
  const hmin = hSatLow(s);
573
- const hs = h2c3bSat(s);
574
- const hmax = region2(C.P_MAX, C.R2_T_MAX).enthalpy;
575
- if (hmin <= h && h < hs)
741
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h2c3bSat(s));
742
+ const hmax = HS_R2_PMAX_TMAX.enthalpy;
743
+ if (hmin <= h && h <= hs)
576
744
  return Region.Region4;
577
745
  if (hs <= h && h <= hmax)
578
746
  return Region.Region2;
579
747
  }
580
748
  // Zone 7: R2_S_CRT <= s < sTPmax (Region 2 / Region 4, use h2abSat)
581
- else if (s >= C.R2_S_CRT && s < sTPmax) {
749
+ else if (s >= C.R2_S_CRT && s < HS_S_TP_MAX) {
582
750
  const hmin = hSatLow(s);
583
- const hs = h2abSat(s);
584
- const hmax = region2(C.P_MAX, C.R2_T_MAX).enthalpy;
585
- if (hmin <= h && h < hs)
751
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s));
752
+ const hmax = HS_R2_PMAX_TMAX.enthalpy;
753
+ if (hmin <= h && h <= hs)
586
754
  return Region.Region4;
587
755
  if (hs <= h && h <= hmax)
588
756
  return Region.Region2;
589
757
  }
590
758
  // Zone 8: sTPmax <= s < s2ab (Region 2 / Region 4)
591
- else if (s >= sTPmax && s < s2ab) {
759
+ else if (s >= HS_S_TP_MAX && s < HS_S2AB) {
592
760
  const hmin = hSatLow(s);
593
- const hs = h2abSat(s);
594
- const hmax = region2(C.P_MIN, C.R2_T_MAX).enthalpy;
595
- if (hmin <= h && h < hs)
761
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s));
762
+ const hmax = HS_R2_PMIN_TMAX.enthalpy;
763
+ if (hmin <= h && h <= hs)
596
764
  return Region.Region4;
597
765
  if (hs <= h && h <= hmax)
598
766
  return Region.Region2;
599
767
  }
600
768
  // Zone 9: s2ab <= s < s4v (Region 2 / Region 4)
601
- else if (s >= s2ab && s < s4v) {
769
+ else if (s >= HS_S2AB && s < HS_TRIPLE_VAPOR.entropy) {
602
770
  const hmin = hSatLow(s);
603
- const hs = h2abSat(s);
604
- const hmax = region2(C.P_MIN, C.R2_T_MAX).enthalpy;
605
- if (hmin <= h && h < hs)
771
+ const hs = refinedSaturationBoundaryEnthalpy(h, s, h2abSat(s));
772
+ const hmax = HS_R2_PMIN_TMAX.enthalpy;
773
+ if (hmin <= h && h <= hs)
606
774
  return Region.Region4;
607
775
  if (hs <= h && h <= hmax)
608
776
  return Region.Region2;
609
777
  }
610
778
  // Zone 10: s4v <= s <= smax (Region 2 only, superheated low-P)
611
- else if (s >= s4v && s <= smax) {
612
- const hmin = h4v;
613
- const hmax = region2(C.P_MIN, C.R2_T_MAX).enthalpy;
779
+ else if (s >= HS_TRIPLE_VAPOR.entropy && s <= HS_R2_PMIN_TMAX.entropy) {
780
+ const hmin = HS_TRIPLE_VAPOR.enthalpy;
781
+ const hmax = HS_R2_PMIN_TMAX.enthalpy;
614
782
  if (hmin <= h && h <= hmax)
615
783
  return Region.Region2;
616
784
  }
617
785
  return -1;
618
786
  }
619
- //# sourceMappingURL=region-detector.js.map
@@ -5,6 +5,18 @@
5
5
  * iterating on density. This module provides that inversion.
6
6
  */
7
7
  import type { BasicProperties } from '../types.js';
8
+ /**
9
+ * Solve for the Region 3 state at a target pressure and temperature.
10
+ *
11
+ * Newton-Raphson on p(ρ, T) − p_target = 0 using the analytic derivative
12
+ * (∂p/∂ρ)_T = 1 / (ρ κ_T).
13
+ *
14
+ * @param p - Target pressure [MPa]
15
+ * @param T - Temperature [K]
16
+ * @param rho0 - Initial density guess [kg/m³]
17
+ * @returns Basic thermodynamic properties at the converged density
18
+ */
19
+ export declare function solveRegion3StateAtPressure(p: number, T: number, rho0: number): BasicProperties;
8
20
  /**
9
21
  * Compute Region 3 basic properties from pressure and temperature.
10
22
  *
@@ -17,4 +29,3 @@ import type { BasicProperties } from '../types.js';
17
29
  * @throws {IF97Error} if the density solve does not converge
18
30
  */
19
31
  export declare function solveRegion3PTBasic(p: number, T: number): BasicProperties;
20
- //# sourceMappingURL=region3-pt.d.ts.map
@@ -3,6 +3,43 @@ import { IF97Error } from '../types.js';
3
3
  import { region3ByRhoT } from '../regions/region3.js';
4
4
  import { region3Volume } from '../regions/region3-subregions.js';
5
5
  import { newtonRaphson } from '../solvers/newton-raphson.js';
6
+ /**
7
+ * Solve for the Region 3 state at a target pressure and temperature.
8
+ *
9
+ * Newton-Raphson on p(ρ, T) − p_target = 0 using the analytic derivative
10
+ * (∂p/∂ρ)_T = 1 / (ρ κ_T).
11
+ *
12
+ * @param p - Target pressure [MPa]
13
+ * @param T - Temperature [K]
14
+ * @param rho0 - Initial density guess [kg/m³]
15
+ * @returns Basic thermodynamic properties at the converged density
16
+ */
17
+ export function solveRegion3StateAtPressure(p, T, rho0) {
18
+ // Keyed by ρ, so the residual and its derivative at the same point share one
19
+ // equation evaluation regardless of the order the solver calls them in.
20
+ let cachedRho = Number.NaN;
21
+ let cachedState = null;
22
+ const stateAt = (rho) => {
23
+ if (rho !== cachedRho || cachedState === null) {
24
+ cachedRho = rho;
25
+ cachedState = region3ByRhoT(rho, T);
26
+ }
27
+ return cachedState;
28
+ };
29
+ const rho = newtonRaphson((rhoGuess) => {
30
+ if (!Number.isFinite(rhoGuess) || rhoGuess <= 0) {
31
+ return Number.NaN;
32
+ }
33
+ return stateAt(rhoGuess).pressure - p;
34
+ }, rho0, (rhoGuess) => {
35
+ if (!Number.isFinite(rhoGuess) || rhoGuess <= 0) {
36
+ return Number.NaN;
37
+ }
38
+ const kappa = stateAt(rhoGuess).isothermalCompressibility;
39
+ return kappa === null ? Number.NaN : 1 / (rhoGuess * kappa);
40
+ });
41
+ return stateAt(rho);
42
+ }
6
43
  /**
7
44
  * Compute Region 3 basic properties from pressure and temperature.
8
45
  *
@@ -18,18 +55,10 @@ export function solveRegion3PTBasic(p, T) {
18
55
  if (p === C.Pc && T === C.Tc) {
19
56
  return region3ByRhoT(C.RHOc, T);
20
57
  }
21
- const v0 = region3Volume(p, T);
22
- const rho = newtonRaphson((rhoGuess) => {
23
- if (!Number.isFinite(rhoGuess) || rhoGuess <= 0) {
24
- return Number.NaN;
25
- }
26
- return region3ByRhoT(rhoGuess, T).pressure - p;
27
- }, 1 / v0);
28
- const state = region3ByRhoT(rho, T);
58
+ const state = solveRegion3StateAtPressure(p, T, 1 / region3Volume(p, T));
29
59
  const relErr = Math.abs(state.pressure - p) / p;
30
60
  if (relErr > 1e-6) {
31
61
  throw new IF97Error(`solveRegion3PTBasic: density solve did not converge — target P=${p} MPa, got P=${state.pressure.toFixed(6)} MPa (relErr=${relErr.toExponential(2)})`);
32
62
  }
33
63
  return state;
34
64
  }
35
- //# sourceMappingURL=region3-pt.js.map
@@ -33,4 +33,3 @@ export type CanonicalSolveInput = {
33
33
  s: number;
34
34
  };
35
35
  export declare function normalizeSolveInput(input: SolveInput): CanonicalSolveInput;
36
- //# sourceMappingURL=solve-input-normalization.d.ts.map
@@ -71,4 +71,3 @@ export function normalizeSolveInput(input) {
71
71
  throw new IF97Error(`Unsupported solve mode: ${input.mode ?? 'undefined'}`);
72
72
  }
73
73
  }
74
- //# sourceMappingURL=solve-input-normalization.js.map
@@ -17,4 +17,3 @@ export declare function solvePT(p: number, T: number): SteamState;
17
17
  * and accepts either shorthand or long-form input field names.
18
18
  */
19
19
  export declare function solve(input: SolveInput): SteamState;
20
- //# sourceMappingURL=solver.d.ts.map
@@ -105,4 +105,3 @@ export function solve(input) {
105
105
  throw new IF97Error(`Unsupported solve mode: ${input.mode ?? 'undefined'}`);
106
106
  }
107
107
  }
108
- //# sourceMappingURL=solver.js.map
package/dist/detect.d.ts CHANGED
@@ -2,4 +2,3 @@
2
2
  * Public region-detection helper exports.
3
3
  */
4
4
  export { detectRegionPT, detectRegionPH, detectRegionPS, detectRegionHS, detectRegionTH, detectRegionTS, } from './core/region-detector.js';
5
- //# sourceMappingURL=detect.d.ts.map
package/dist/detect.js CHANGED
@@ -2,4 +2,3 @@
2
2
  * Public region-detection helper exports.
3
3
  */
4
4
  export { detectRegionPT, detectRegionPH, detectRegionPS, detectRegionHS, detectRegionTH, detectRegionTS, } from './core/region-detector.js';
5
- //# sourceMappingURL=detect.js.map
package/dist/index.d.ts CHANGED
@@ -15,4 +15,3 @@ export declare function solvePx(p: number, x: number): SteamState;
15
15
  export declare function solveTx(T: number, x: number): SteamState;
16
16
  export type { SteamState, SolveInput } from './types.js';
17
17
  export { Region, IF97Error, OutOfRangeError, ConvergenceError } from './types.js';
18
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -34,4 +34,3 @@ export function solveTx(T, x) {
34
34
  return normalizeSteamState(solveAny({ mode: 'Tx', T, x }));
35
35
  }
36
36
  export { Region, IF97Error, OutOfRangeError, ConvergenceError } from './types.js';
37
- //# sourceMappingURL=index.js.map
@@ -38,4 +38,3 @@ export declare function b3rx(p: number): number;
38
38
  export declare function b3uv(p: number): number;
39
39
  /** B3wx boundary: P → T */
40
40
  export declare function b3wx(p: number): number;
41
- //# sourceMappingURL=boundaries.d.ts.map
@@ -103,4 +103,3 @@ export function b3uv(p) {
103
103
  export function b3wx(p) {
104
104
  return polyEvalLn(p, [0, 1, 2, -1, -2], [7.2805260914538, 9.73505869861952e1, 1.47370491183191e1, 0.329196213998375e3, 8.73371668682417e2]);
105
105
  }
106
- //# sourceMappingURL=boundaries.js.map
@@ -15,4 +15,3 @@ import type { BasicProperties } from '../types.js';
15
15
  * @returns Basic thermodynamic properties
16
16
  */
17
17
  export declare function region1(p: number, T: number): BasicProperties;
18
- //# sourceMappingURL=region1.d.ts.map
@@ -92,4 +92,3 @@ export function region1(p, T) {
92
92
  isothermalCompressibility: -pi * g_pipi / g_pi / p,
93
93
  };
94
94
  }
95
- //# sourceMappingURL=region1.js.map
@@ -15,4 +15,3 @@ import type { BasicProperties } from '../types.js';
15
15
  * @returns Basic thermodynamic properties
16
16
  */
17
17
  export declare function region2(p: number, T: number): BasicProperties;
18
- //# sourceMappingURL=region2.d.ts.map
@@ -131,4 +131,3 @@ export function region2(p, T) {
131
131
  isothermalCompressibility: (1 - pi * pi * gr_pipi) / (1 + pi * gr_pi) / p,
132
132
  };
133
133
  }
134
- //# sourceMappingURL=region2.js.map
@@ -58,4 +58,3 @@ export declare const REGION3_SUBREGIONS: {
58
58
  readonly Z: SubregionConfig;
59
59
  };
60
60
  export type Region3SubregionName = keyof typeof REGION3_SUBREGIONS;
61
- //# sourceMappingURL=region3-data.d.ts.map
@@ -130,4 +130,3 @@ export const REGION3_SUBREGIONS = {
130
130
  Y: R3Y,
131
131
  Z: R3Z,
132
132
  };
133
- //# sourceMappingURL=region3-data.js.map
@@ -27,4 +27,3 @@ export interface SubregionConfig {
27
27
  * Evaluate a Region 3 subregion backward equation v(P,T).
28
28
  */
29
29
  export declare function evalSubregion(cfg: SubregionConfig, p: number, T: number): number;
30
- //# sourceMappingURL=region3-eval.d.ts.map
@@ -22,4 +22,3 @@ export function evalSubregion(cfg, p, T) {
22
22
  return cfg.vStar * Math.exp(v);
23
23
  return cfg.vStar * v;
24
24
  }
25
- //# sourceMappingURL=region3-eval.js.map
@@ -6,4 +6,3 @@ export declare function region3SatVolume(p: number, T: number, x: number): numbe
6
6
  * Pressure thresholds from IAPWS Supplementary Release on Backward
7
7
  * Equations v(P,T) for Region 3, Figures 2–5. */
8
8
  export declare function region3Volume(p: number, T: number): number;
9
- //# sourceMappingURL=region3-subregions.d.ts.map
@@ -168,4 +168,3 @@ export function region3Volume(p, T) {
168
168
  }
169
169
  throw new IF97Error('Region 3 v(P,T): pressure out of range');
170
170
  }
171
- //# sourceMappingURL=region3-subregions.js.map
@@ -19,4 +19,3 @@ import type { BasicProperties } from '../types.js';
19
19
  * @throws {IF97Error} if rho is non-finite or not strictly positive
20
20
  */
21
21
  export declare function region3ByRhoT(rho: number, T: number): BasicProperties;
22
- //# sourceMappingURL=region3.d.ts.map
@@ -119,4 +119,3 @@ export function region3ByRhoT(rho, T) {
119
119
  rho / R / T * 1000,
120
120
  };
121
121
  }
122
- //# sourceMappingURL=region3.js.map
@@ -26,4 +26,3 @@ export declare function saturationPressure(T: number): number;
26
26
  * @throws {OutOfRangeError} if P is outside valid range
27
27
  */
28
28
  export declare function saturationTemperature(p: number): number;
29
- //# sourceMappingURL=region4.d.ts.map
@@ -62,4 +62,3 @@ export function saturationTemperature(p) {
62
62
  const D = 2 * G / (-F - Math.sqrt(F * F - 4 * E * G));
63
63
  return normalizeRegion4Temperature((n[10] + D - Math.sqrt(Math.pow(n[10] + D, 2) - 4 * (n[9] + n[10] * D))) / 2);
64
64
  }
65
- //# sourceMappingURL=region4.js.map
@@ -15,4 +15,3 @@ import type { BasicProperties } from '../types.js';
15
15
  * @returns Basic thermodynamic properties
16
16
  */
17
17
  export declare function region5(p: number, T: number): BasicProperties;
18
- //# sourceMappingURL=region5.d.ts.map
@@ -89,4 +89,3 @@ export function region5(p, T) {
89
89
  isothermalCompressibility: (1 - pi * pi * gr_pipi) / (1 + pi * gr_pi) / p,
90
90
  };
91
91
  }
92
- //# sourceMappingURL=region5.js.map
package/dist/regions.d.ts CHANGED
@@ -5,4 +5,3 @@ export { region1 } from './regions/region1.js';
5
5
  export { region2 } from './regions/region2.js';
6
6
  export { region3ByRhoT } from './regions/region3.js';
7
7
  export { region5 } from './regions/region5.js';
8
- //# sourceMappingURL=regions.d.ts.map
package/dist/regions.js CHANGED
@@ -5,4 +5,3 @@ export { region1 } from './regions/region1.js';
5
5
  export { region2 } from './regions/region2.js';
6
6
  export { region3ByRhoT } from './regions/region3.js';
7
7
  export { region5 } from './regions/region5.js';
8
- //# sourceMappingURL=regions.js.map