iapws-if97 2.2.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/README.md +69 -246
  3. package/dist/backward/candidate-resolution.d.ts +8 -0
  4. package/dist/backward/candidate-resolution.js +40 -0
  5. package/dist/backward/fixed-temperature-candidates.d.ts +4 -0
  6. package/dist/backward/fixed-temperature-candidates.js +188 -0
  7. package/dist/backward/fixed-temperature-solver.d.ts +3 -3
  8. package/dist/backward/fixed-temperature-solver.js +38 -17
  9. package/dist/backward/fixed-temperature-stationary.d.ts +4 -0
  10. package/dist/backward/fixed-temperature-stationary.js +146 -0
  11. package/dist/backward/hs-region.d.ts +50 -0
  12. package/dist/backward/hs-region.js +695 -0
  13. package/dist/backward/hs-state.d.ts +15 -0
  14. package/dist/backward/hs-state.js +316 -0
  15. package/dist/backward/hs.js +28 -242
  16. package/dist/backward/ph.d.ts +2 -0
  17. package/dist/backward/ph.js +72 -21
  18. package/dist/backward/pressure-property-region.d.ts +13 -0
  19. package/dist/backward/pressure-property-region.js +262 -0
  20. package/dist/backward/ps.d.ts +6 -0
  21. package/dist/backward/ps.js +81 -28
  22. package/dist/backward/solution-validation.d.ts +7 -1
  23. package/dist/backward/solution-validation.js +138 -41
  24. package/dist/backward/th.d.ts +3 -5
  25. package/dist/backward/th.js +10 -71
  26. package/dist/backward/tolerances.d.ts +3 -0
  27. package/dist/backward/tolerances.js +9 -3
  28. package/dist/backward/ts.d.ts +3 -5
  29. package/dist/backward/ts.js +10 -71
  30. package/dist/constants.d.ts +11 -15
  31. package/dist/constants.js +14 -15
  32. package/dist/core/floating-point.d.ts +2 -0
  33. package/dist/core/floating-point.js +7 -0
  34. package/dist/core/formulation-seams.d.ts +27 -0
  35. package/dist/core/formulation-seams.js +289 -0
  36. package/dist/core/input-validation.d.ts +1 -0
  37. package/dist/core/input-validation.js +5 -0
  38. package/dist/core/region-detector.d.ts +3 -40
  39. package/dist/core/region-detector.js +37 -762
  40. package/dist/core/region3-pt.d.ts +1 -1
  41. package/dist/core/region3-pt.js +42 -16
  42. package/dist/core/solve-input-normalization.js +4 -1
  43. package/dist/core/solver.d.ts +7 -10
  44. package/dist/core/solver.js +26 -17
  45. package/dist/detect.d.ts +16 -2
  46. package/dist/detect.js +36 -2
  47. package/dist/errors.d.ts +11 -0
  48. package/dist/errors.js +16 -0
  49. package/dist/gas-constant.d.ts +5 -0
  50. package/dist/gas-constant.js +5 -0
  51. package/dist/index.d.ts +4 -5
  52. package/dist/index.js +10 -21
  53. package/dist/regions/region1.d.ts +2 -1
  54. package/dist/regions/region1.js +1 -1
  55. package/dist/regions/region2.d.ts +2 -1
  56. package/dist/regions/region2.js +1 -1
  57. package/dist/regions/region3-data.js +3 -3
  58. package/dist/regions/region3-subregions.js +15 -10
  59. package/dist/regions/region3.d.ts +4 -1
  60. package/dist/regions/region3.js +32 -2
  61. package/dist/regions/region4.d.ts +2 -0
  62. package/dist/regions/region4.js +39 -12
  63. package/dist/regions/region5.d.ts +2 -1
  64. package/dist/regions/region5.js +2 -2
  65. package/dist/saturation/common.d.ts +2 -8
  66. package/dist/saturation/common.js +64 -36
  67. package/dist/saturation/region4-boundaries.js +13 -13
  68. package/dist/saturation/region4-hs.js +282 -58
  69. package/dist/saturation/two-phase.d.ts +2 -2
  70. package/dist/saturation/two-phase.js +7 -7
  71. package/dist/solvers/bracketed-newton.d.ts +6 -3
  72. package/dist/solvers/bracketed-newton.js +15 -8
  73. package/dist/solvers/extended-arithmetic.d.ts +6 -0
  74. package/dist/solvers/extended-arithmetic.js +38 -0
  75. package/dist/transport/properties.d.ts +4 -2
  76. package/dist/transport/properties.js +14 -4
  77. package/dist/types.d.ts +18 -0
  78. package/dist/types.js +14 -0
  79. package/package.json +4 -4
  80. package/dist/core/public-normalization.d.ts +0 -3
  81. package/dist/core/public-normalization.js +0 -53
  82. /package/dist/{backward → core}/thermodynamic-derivatives.d.ts +0 -0
  83. /package/dist/{backward → core}/thermodynamic-derivatives.js +0 -0
package/dist/constants.js CHANGED
@@ -4,12 +4,9 @@
4
4
  * Physical constants, critical point properties, and range limits
5
5
  * for the IAPWS Industrial Formulation 1997.
6
6
  */
7
+ import { region5 } from './regions/region5.js';
7
8
  // ─── Fundamental Constants ──────────────────────────────────────────────────
8
- /** Specific gas constant for water [kJ/(kg·K)].
9
- * IAPWS-IF97, §2 (Table 1). Fixed value for IF97 internal consistency.
10
- * Do not replace transport correlations with this value; thermal conductivity
11
- * critical enhancement intentionally uses `R_IAPWS_2011_THERMAL`. */
12
- export const R = 0.461526;
9
+ export { R } from './gas-constant.js';
13
10
  /** Specific gas constant used by the IAPWS 2011 thermal conductivity λ₂ correlation [kJ/(kg·K)].
14
11
  * Intentionally differs from IF97's `R`; keep both constants distinct and named. */
15
12
  export const R_IAPWS_2011_THERMAL = 0.46151805;
@@ -18,13 +15,13 @@ export const R_IAPWS_2011_THERMAL = 0.46151805;
18
15
  export const Tc = 647.096;
19
16
  /** Temperature exclusion half-band around the critical point [K] */
20
17
  export const CRITICAL_T_EXCLUSION_BAND = 1e-3;
21
- /** Region 4 endpoint snap tolerance for temperatures [K] */
18
+ /** Region 4 tolerance for outward temperature-boundary roundoff [K] */
22
19
  export const REGION4_TEMPERATURE_TOLERANCE = 1e-9;
23
20
  /** Safety margin used when clamping Region 4 temperatures below Tc [K] */
24
21
  export const REGION4_SUBCRITICAL_TEMPERATURE_MARGIN = 1e-8;
25
22
  /** Critical pressure [MPa] */
26
23
  export const Pc = 22.064;
27
- /** Region 4 endpoint snap tolerance for pressures [MPa] */
24
+ /** Relative tolerance for outward Region 4 pressure-boundary roundoff */
28
25
  export const REGION4_PRESSURE_TOLERANCE = 1e-9;
29
26
  /** Tolerance for identifying the critical h-s endpoint in Region 4 */
30
27
  export const REGION4_CRITICAL_HS_TOLERANCE = 1e-6;
@@ -48,14 +45,16 @@ export const P_MAX = 100.0;
48
45
  export const T_MIN = 273.15;
49
46
  /** Maximum temperature [K] (2273.15 K = 2000 °C) */
50
47
  export const T_MAX = 2273.15;
51
- /** Minimum specific entropy [kJ/(kg·K)] */
52
- export const S_MIN = -0.00015454959194;
53
- /** Maximum specific entropy [kJ/(kg·K)] */
54
- export const S_MAX = 13.904956083429227;
55
- /** Minimum specific enthalpy [kJ/kg] */
56
- export const H_MIN = -0.041587825987;
57
- /** Maximum specific enthalpy [kJ/kg] */
58
- export const H_MAX = 7376.980263598506;
48
+ // Match the Region 5 equation's rounding in the current runtime.
49
+ const REGION5_UPPER = region5(P_MIN, T_MAX);
50
+ /** Minimum specific entropy [kJ/(kg·K)], at T_MIN and P_MAX in Region 1. */
51
+ export const S_MIN = -0.00858228709261889;
52
+ /** Maximum specific entropy [kJ/(kg·K)], at T_MAX and P_MIN in Region 5. */
53
+ export const S_MAX = REGION5_UPPER.entropy;
54
+ /** Minimum specific enthalpy [kJ/kg], at T_MIN and P_MIN in Region 1. */
55
+ export const H_MIN = -0.041587825987655716;
56
+ /** Maximum specific enthalpy [kJ/kg], at T_MAX and P_MIN in Region 5. */
57
+ export const H_MAX = REGION5_UPPER.enthalpy;
59
58
  // ─── Region 2 Boundaries ───────────────────────────────────────────────────
60
59
  /** Region 2 minimum temperature for high-P subregions [K] (623.15 K = 350 °C) */
61
60
  export const R2_T_MIN = 623.15;
@@ -0,0 +1,2 @@
1
+ /** Adjacent IEEE-754 double; value must be positive and finite. */
2
+ export declare function nextPositiveFloat(value: number, direction: 1 | -1): number;
@@ -0,0 +1,7 @@
1
+ const bits = new DataView(new ArrayBuffer(8));
2
+ /** Adjacent IEEE-754 double; value must be positive and finite. */
3
+ export function nextPositiveFloat(value, direction) {
4
+ bits.setFloat64(0, value);
5
+ bits.setBigUint64(0, bits.getBigUint64(0) + BigInt(direction));
6
+ return bits.getFloat64(0);
7
+ }
@@ -0,0 +1,27 @@
1
+ import type { BasicProperties } from '../types.js';
2
+ export type FormulationSeam = '13' | '23' | '25';
3
+ export interface FormulationSeamPair {
4
+ seam: FormulationSeam;
5
+ pressure: number;
6
+ temperature: number;
7
+ /** Equations on the lower and higher temperature sides at fixed pressure. */
8
+ lower: BasicProperties;
9
+ upper: BasicProperties;
10
+ canonical: BasicProperties;
11
+ }
12
+ export interface FormulationSeamResolution {
13
+ pair: FormulationSeamPair;
14
+ kind: 'canonical' | 'gap' | 'overlap';
15
+ state: BasicProperties;
16
+ budgets: {
17
+ enthalpy: number;
18
+ entropy: number;
19
+ };
20
+ }
21
+ export declare function formulationSeamAtPressure(p: number, seam: FormulationSeam): FormulationSeamPair | null;
22
+ /** B13/B25 cannot be crossed while retaining an independently supplied temperature. */
23
+ export declare function formulationSeamAtTemperature(T: number): FormulationSeamPair | null;
24
+ export declare function findPressureSeam(p: number, target: number, property: 'enthalpy' | 'entropy'): FormulationSeamResolution | null;
25
+ export declare function matchFixedSeam(pair: FormulationSeamPair, property: 'enthalpy' | 'entropy', target: number, coordinate?: 'pressure' | 'temperature'): FormulationSeamResolution | null;
26
+ /** Locate both constraints on one bounded ribbon between independently fitted EOS. */
27
+ export declare function findHSSeam(h: number, s: number): FormulationSeamResolution | null;
@@ -0,0 +1,289 @@
1
+ import * as C from '../constants.js';
2
+ import { boundary23_T_to_P } from '../regions/boundaries.js';
3
+ import { region1 } from '../regions/region1.js';
4
+ import { region2 } from '../regions/region2.js';
5
+ import { region5 } from '../regions/region5.js';
6
+ import { saturationPressure } from '../regions/region4.js';
7
+ import { bracketedNewton } from '../solvers/bracketed-newton.js';
8
+ import { gibbsPropertyDerivatives } from './thermodynamic-derivatives.js';
9
+ import { solveRegion3PTBasic } from './region3-pt.js';
10
+ import { detectRegionPT, nativeB23TemperatureBracket } from './region-detector.js';
11
+ import { nextPositiveFloat } from './floating-point.js';
12
+ function pairAt(seam, p, T) {
13
+ const r3 = () => solveRegion3PTBasic(p, T);
14
+ const lower = seam === '13' ? region1(p, T) : seam === '23' ? r3() : region2(p, T);
15
+ const upper = seam === '13' ? r3() : seam === '23' ? region2(p, T) : region5(p, T);
16
+ return {
17
+ seam, pressure: p, temperature: T,
18
+ lower, upper, canonical: lower,
19
+ };
20
+ }
21
+ export function formulationSeamAtPressure(p, seam) {
22
+ if (!Number.isFinite(p) || p < C.P_MIN || p > C.P_MAX)
23
+ return null;
24
+ if (seam === '25')
25
+ return p <= C.R5_P_MAX ? pairAt(seam, p, C.R2_T_MAX) : null;
26
+ if (p <= C.B23_P_MIN)
27
+ return null;
28
+ if (seam === '13')
29
+ return p > saturationPressure(C.R2_T_MIN) ? pairAt(seam, p, C.R2_T_MIN) : null;
30
+ const [T] = nativeB23TemperatureBracket(p);
31
+ return T <= C.R2_T_MIN || (T < C.Tc && p >= saturationPressure(T)) ? null : pairAt(seam, p, T);
32
+ }
33
+ /** B13/B25 cannot be crossed while retaining an independently supplied temperature. */
34
+ export function formulationSeamAtTemperature(T) {
35
+ if (!Number.isFinite(T) || T <= C.R2_T_MIN || T >= C.B23_T_MAX)
36
+ return null;
37
+ const p = boundary23_T_to_P(T);
38
+ return p > C.B23_P_MIN && p <= C.P_MAX &&
39
+ (T >= C.Tc || p < saturationPressure(T)) ? pairAt('23', p, T) : null;
40
+ }
41
+ export function findPressureSeam(p, target, property) {
42
+ if (!Number.isFinite(p) || !Number.isFinite(target) || p < C.P_MIN || p > C.P_MAX)
43
+ return null;
44
+ for (const seam of ['13', '23', '25']) {
45
+ if (seam === '25' ? p > C.R5_P_MAX : p <= C.B23_P_MIN)
46
+ continue;
47
+ let reference;
48
+ if (seam === '13') {
49
+ if (p <= saturationPressure(C.R2_T_MIN))
50
+ continue;
51
+ reference = region1(p, C.R2_T_MIN);
52
+ }
53
+ else if (seam === '23') {
54
+ const [T] = nativeB23TemperatureBracket(p);
55
+ if (T < C.Tc && p >= saturationPressure(T))
56
+ continue;
57
+ reference = region2(p, T);
58
+ }
59
+ else {
60
+ reference = region2(p, C.R2_T_MAX);
61
+ }
62
+ // IF97 Table 43 consistency limits only prefilter; acceptance uses the actual EOS span.
63
+ if (Math.abs(reference[property] - target) > (property === 'enthalpy' ? 0.2 : 0.0002))
64
+ continue;
65
+ const pair = formulationSeamAtPressure(p, seam);
66
+ const match = pair === null ? null : matchFixedSeam(pair, property, target);
67
+ if (match !== null)
68
+ return match;
69
+ }
70
+ return null;
71
+ }
72
+ function numericalAllowance(value) {
73
+ return 128 * Number.EPSILON * Math.max(1, Math.abs(value));
74
+ }
75
+ function resolution(pair, kind, hsReconstruction = false) {
76
+ return {
77
+ pair, kind, state: pair.canonical,
78
+ budgets: {
79
+ enthalpy: Math.abs(pair.upper.enthalpy - pair.lower.enthalpy) +
80
+ (hsReconstruction ? 1e-9 : numericalAllowance(pair.canonical.enthalpy)),
81
+ entropy: Math.abs(pair.upper.entropy - pair.lower.entropy) +
82
+ (hsReconstruction ? 1e-12 : numericalAllowance(pair.canonical.entropy)),
83
+ },
84
+ };
85
+ }
86
+ export function matchFixedSeam(pair, property, target, coordinate = 'pressure') {
87
+ if (coordinate === 'temperature' && pair.seam !== '23')
88
+ return null;
89
+ if (pair.seam === '23' && pair.pressure === C.P_MAX) {
90
+ const corner = region2(C.P_MAX, C.B23_T_MAX);
91
+ // The exact PT corner is R2, above the native quadratic's last R3 float.
92
+ if (target === corner[property])
93
+ return resolution({ ...pair, canonical: corner }, 'canonical');
94
+ }
95
+ const lower = pair.lower[property];
96
+ const upper = pair.upper[property];
97
+ if (!Number.isFinite(target) || target < Math.min(lower, upper) || target > Math.max(lower, upper))
98
+ return null;
99
+ if (target === pair.canonical[property])
100
+ return resolution(pair, 'canonical');
101
+ const interior = coordinate === 'temperature'
102
+ ? region2(nextPositiveFloat(pair.pressure, -1), pair.temperature)
103
+ : pair.seam === '13' ? solveRegion3PTBasic(pair.pressure, nextPositiveFloat(pair.temperature, 1))
104
+ : pair.seam === '23' ? region2(pair.pressure, nextPositiveFloat(pair.temperature, 1))
105
+ : region5(pair.pressure, nextPositiveFloat(pair.temperature, 1));
106
+ // EOS rounding can put the first admissible float inside the endpoint's gap.
107
+ if (coordinate === 'pressure' && detectRegionPT(interior.pressure, interior.temperature) === interior.region &&
108
+ Math.abs(interior[property] - target) <= (property === 'enthalpy' ? 1e-9 : 1e-12)) {
109
+ return resolution({ ...pair, canonical: interior }, 'canonical');
110
+ }
111
+ if (coordinate === 'temperature') {
112
+ if (target !== upper && interior[property] === target)
113
+ return null;
114
+ const first = gibbsPropertyDerivatives(pair.lower);
115
+ const second = gibbsPropertyDerivatives(pair.upper);
116
+ const derivative = property === 'enthalpy' ? 'dhDp' : 'dsDp';
117
+ if (first[derivative] >= 0 || second[derivative] >= 0)
118
+ return null;
119
+ }
120
+ return resolution(pair, upper > lower ? 'gap' : 'overlap');
121
+ }
122
+ function tangent(pair, state) {
123
+ const derivatives = gibbsPropertyDerivatives(state);
124
+ if (pair.seam !== '23')
125
+ return [derivatives.dhDp, derivatives.dsDp];
126
+ // Derivative of the native quadratic B23 pressure boundary.
127
+ const dpDT = -1.1671859879975 + 2 * 0.0010192970039326 * pair.temperature;
128
+ return [
129
+ derivatives.dhDT + derivatives.dhDp * dpDT,
130
+ derivatives.dsDT + derivatives.dsDp * dpDT,
131
+ ];
132
+ }
133
+ let hsDomains;
134
+ function b23LowerTemperature() {
135
+ let lower = C.R2_T_MIN;
136
+ let upper = C.Tc;
137
+ while (true) {
138
+ const middle = (lower + upper) / 2;
139
+ if (middle === lower || middle === upper)
140
+ return upper;
141
+ if (boundary23_T_to_P(middle) < saturationPressure(middle))
142
+ upper = middle;
143
+ else
144
+ lower = middle;
145
+ }
146
+ }
147
+ function pairAtParameter(seam, parameter) {
148
+ return seam === '23'
149
+ ? pairAt(seam, boundary23_T_to_P(parameter), parameter)
150
+ : pairAt(seam, parameter, seam === '13' ? C.R2_T_MIN : C.R2_T_MAX);
151
+ }
152
+ function hsSeamDomains() {
153
+ if (hsDomains !== undefined)
154
+ return hsDomains;
155
+ hsDomains = ['13', '23', '25'].map((seam) => {
156
+ const lower = seam === '25' ? C.P_MIN : seam === '13'
157
+ ? nextPositiveFloat(saturationPressure(C.R2_T_MIN), 1) : b23LowerTemperature();
158
+ const upper = seam === '25' ? C.R5_P_MAX : seam === '13'
159
+ ? C.P_MAX : nativeB23TemperatureBracket(C.P_MAX)[0];
160
+ const nodes = [lower, upper];
161
+ {
162
+ // Partition both equations at their folds before inverting either property.
163
+ for (const side of ['lower', 'upper']) {
164
+ for (const propertyIndex of [0, 1]) {
165
+ let previous = lower;
166
+ const first = pairAtParameter(seam, previous);
167
+ let previousDerivative = tangent(first, first[side])[propertyIndex];
168
+ for (let i = 1; i <= 16; i++) {
169
+ const current = lower + (upper - lower) * i / 16;
170
+ const pair = pairAtParameter(seam, current);
171
+ const currentDerivative = tangent(pair, pair[side])[propertyIndex];
172
+ if (previousDerivative * currentDerivative < 0) {
173
+ nodes.push(bracketedNewton((q) => {
174
+ const at = pairAtParameter(seam, q);
175
+ return tangent(at, at[side])[propertyIndex];
176
+ }, previous, current, undefined, { tolerance: 1e-13 }));
177
+ }
178
+ previous = current;
179
+ previousDerivative = currentDerivative;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ nodes.sort((a, b) => a - b);
185
+ const states = nodes.flatMap((q) => {
186
+ const pair = pairAtParameter(seam, q);
187
+ return [pair.lower, pair.upper];
188
+ });
189
+ return {
190
+ seam, nodes,
191
+ minimumEntropy: Math.min(...states.map((state) => state.entropy)),
192
+ maximumEntropy: Math.max(...states.map((state) => state.entropy)),
193
+ minimumEnthalpy: Math.min(...states.map((state) => state.enthalpy)),
194
+ maximumEnthalpy: Math.max(...states.map((state) => state.enthalpy)),
195
+ };
196
+ });
197
+ return hsDomains;
198
+ }
199
+ /** Locate both constraints on one bounded ribbon between independently fitted EOS. */
200
+ export function findHSSeam(h, s) {
201
+ if (!Number.isFinite(h) || !Number.isFinite(s))
202
+ return null;
203
+ const corner = region2(C.P_MAX, C.B23_T_MAX);
204
+ if (h === corner.enthalpy && s === corner.entropy) {
205
+ return resolution({ ...formulationSeamAtPressure(C.P_MAX, '23'), canonical: corner }, 'canonical');
206
+ }
207
+ for (const domain of hsSeamDomains()) {
208
+ const entropyNoise = numericalAllowance(s);
209
+ const enthalpyNoise = numericalAllowance(h);
210
+ if (h < domain.minimumEnthalpy - enthalpyNoise || h > domain.maximumEnthalpy + enthalpyNoise)
211
+ continue;
212
+ if (s < domain.minimumEntropy - entropyNoise || s > domain.maximumEntropy + entropyNoise)
213
+ continue;
214
+ const cache = new Map();
215
+ const at = (q) => {
216
+ let pair = cache.get(q);
217
+ if (pair === undefined) {
218
+ pair = pairAtParameter(domain.seam, q);
219
+ cache.set(q, pair);
220
+ }
221
+ return pair;
222
+ };
223
+ const bounds = [...domain.nodes];
224
+ for (let i = 1; i < domain.nodes.length; i++) {
225
+ const lower = domain.nodes[i - 1];
226
+ const upper = domain.nodes[i];
227
+ for (const side of ['lower', 'upper']) {
228
+ const first = at(lower)[side].entropy - s;
229
+ const last = at(upper)[side].entropy - s;
230
+ if (first * last < 0) {
231
+ bounds.push(bracketedNewton((q) => at(q)[side].entropy - s, lower, upper, undefined, {
232
+ derivative: (q) => { const pair = at(q); return tangent(pair, pair[side])[1]; },
233
+ tolerance: 1e-13,
234
+ }));
235
+ }
236
+ }
237
+ }
238
+ bounds.sort((a, b) => a - b);
239
+ const hScale = Math.max(1, Math.abs(h));
240
+ const sScale = Math.max(1, Math.abs(s));
241
+ const crossResidual = (q) => {
242
+ const pair = at(q);
243
+ const dh = (pair.upper.enthalpy - pair.lower.enthalpy) / hScale;
244
+ const ds = (pair.upper.entropy - pair.lower.entropy) / sScale;
245
+ const length = Math.hypot(dh, ds);
246
+ return length === 0 ? (pair.lower.entropy - s) / sScale
247
+ : ((h - pair.lower.enthalpy) / hScale * ds - (s - pair.lower.entropy) / sScale * dh) / length;
248
+ };
249
+ const matchAt = (q) => {
250
+ const pair = at(q);
251
+ const dh = pair.upper.enthalpy - pair.lower.enthalpy;
252
+ const ds = pair.upper.entropy - pair.lower.entropy;
253
+ const scaledH = dh / hScale;
254
+ const scaledS = ds / sScale;
255
+ const squareLength = scaledH * scaledH + scaledS * scaledS;
256
+ const fraction = squareLength === 0 ? 0 : Math.max(0, Math.min(1, (((h - pair.lower.enthalpy) / hScale) * scaledH +
257
+ ((s - pair.lower.entropy) / sScale) * scaledS) / squareLength));
258
+ // Region 3 density inversion introduces cancellation beyond machine-noise in h/s.
259
+ if (Math.abs(pair.lower.enthalpy + fraction * dh - h) > 1e-9 ||
260
+ Math.abs(pair.lower.entropy + fraction * ds - s) > 1e-12)
261
+ return null;
262
+ if (Math.abs(pair.canonical.enthalpy - h) <= numericalAllowance(h) &&
263
+ Math.abs(pair.canonical.entropy - s) <= numericalAllowance(s))
264
+ return resolution(pair, 'canonical');
265
+ const direction = tangent(pair, pair.lower);
266
+ return resolution(pair, direction[0] * ds - direction[1] * dh > 0 ? 'gap' : 'overlap', true);
267
+ };
268
+ for (let i = 0; i < bounds.length; i++) {
269
+ const exact = matchAt(bounds[i]);
270
+ if (exact !== null)
271
+ return exact;
272
+ if (i === 0 || bounds[i] === bounds[i - 1])
273
+ continue;
274
+ const lower = bounds[i - 1];
275
+ const upper = bounds[i];
276
+ const middle = at((lower + upper) / 2);
277
+ if (s < Math.min(middle.lower.entropy, middle.upper.entropy) - entropyNoise ||
278
+ s > Math.max(middle.lower.entropy, middle.upper.entropy) + entropyNoise)
279
+ continue;
280
+ if (crossResidual(lower) * crossResidual(upper) >= 0)
281
+ continue;
282
+ const root = bracketedNewton(crossResidual, lower, upper, undefined, { tolerance: 4 * Number.EPSILON });
283
+ const match = matchAt(root);
284
+ if (match !== null)
285
+ return match;
286
+ }
287
+ }
288
+ return null;
289
+ }
@@ -1 +1,2 @@
1
1
  export declare function assertFiniteNumber(parameter: string, value: unknown): asserts value is number;
2
+ export declare function withinClosedLowerBound(value: number, boundary: number, property: 'enthalpy' | 'entropy'): boolean;
@@ -4,3 +4,8 @@ export function assertFiniteNumber(parameter, value) {
4
4
  throw new IF97Error(`${parameter} must be a finite number`);
5
5
  }
6
6
  }
7
+ // shortcut: EOS cancellation floors are empirical; remeasure if the EOS evaluation changes.
8
+ export function withinClosedLowerBound(value, boundary, property) {
9
+ const floor = property === 'enthalpy' ? 1e-12 : 1e-14;
10
+ return value >= boundary - Math.max(floor, 8 * Number.EPSILON * Math.max(1, Math.abs(boundary)));
11
+ }
@@ -4,43 +4,6 @@ import { Region } from '../types.js';
4
4
  * @returns Region number (1–5) or -1 if out of range
5
5
  */
6
6
  export declare function detectRegionPT(p: number, T: number): Region | -1;
7
- /**
8
- * Detect region for P-H inputs.
9
- */
10
- export declare function detectRegionPH(p: number, h: number): Region | -1;
11
- /**
12
- * Detect region for P-S inputs.
13
- */
14
- export declare function detectRegionPS(p: number, s: number): Region | -1;
15
- /**
16
- * Detect the IAPWS-IF97 region for given T and H.
17
- *
18
- * Uses explicit enthalpy comparisons (including saturation-boundary proximity
19
- * checks via `isClose`) rather than the generic helper, because enthalpy is
20
- * not monotonic across regions at some temperatures.
21
- *
22
- * @param T - Temperature [K]
23
- * @param h - Specific enthalpy [kJ/kg]
24
- * @returns Region number (1–5) or -1 if out of range
25
- */
26
- export declare function detectRegionTH(T: number, h: number): Region | -1;
27
- /**
28
- * Detect the IAPWS-IF97 region for given T and S.
29
- *
30
- * Delegates to the generic temperature-property detector since entropy
31
- * is monotonic across regions at fixed temperature.
32
- *
33
- * @param T - Temperature [K]
34
- * @param s - Specific entropy [kJ/(kg·K)]
35
- * @returns Region number (1–5) or -1 if out of range
36
- */
37
- export declare function detectRegionTS(T: number, s: number): Region | -1;
38
- /**
39
- * Detect the IAPWS-IF97 region for given H and S.
40
- * Implements the zone-based detection from the IAPWS supplementary release.
41
- *
42
- * @param h - Specific enthalpy [kJ/kg]
43
- * @param s - Specific entropy [kJ/(kg·K)]
44
- * @returns Region number (1–5) or -1 if out of range
45
- */
46
- export declare function detectRegionHS(h: number, s: number): Region | -1;
7
+ export declare function nativeSaturationTemperatureBracket(p: number): readonly [number, number];
8
+ /** Invert the native B23 boundary; rounded Eq. (6) cannot determine PT ownership. */
9
+ export declare function nativeB23TemperatureBracket(p: number): readonly [number, number];