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
@@ -1,21 +1,23 @@
1
1
  import * as C from '../constants.js';
2
2
  import { bracketedNewton } from '../solvers/bracketed-newton.js';
3
3
  import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
4
+ import { backwardConstraintTolerance } from '../backward/tolerances.js';
5
+ import { nextPositiveFloat } from '../core/floating-point.js';
4
6
  import { IF97Error } from '../types.js';
7
+ import { SaturationEndpointError } from '../errors.js';
5
8
  import { mixSaturationState, rawQualityFromSaturationProperty, saturationEndpointsAtTemperature, } from './common.js';
6
9
  import { assertRegion4StateAllowed, clampRegion4TemperatureBelowCritical, } from './region4-boundaries.js';
7
10
  const REGION4_QUALITY_TOLERANCE = 1e-6;
8
11
  const REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE = 1e-10;
9
12
  /** Lower entropy limit of the SR4-04 T_sat(h,s) equation [kJ/(kg·K)].
10
13
  * Published range of validity is s''(623.15 K) = 5.210887825 to
11
- * s''(273.15 K) = 9.155759395; both reproduce exactly from Region 2 on the
12
- * saturation line. Below this bound Eq. 9 is extrapolation, so the caller
14
+ * s''(273.15 K) = 9.155759395. Use the computed Region 2 endpoint rather
15
+ * than its rounded table value. Below this bound Eq. 9 is extrapolation, so the caller
13
16
  * falls through to the general Newton path instead.
14
17
  * Ref: IAPWS SR4-04(2014), Eq. 9. */
15
- const REGION4_HS_BACKWARD_MIN_ENTROPY = 5.210887825;
18
+ const REGION4_HS_BACKWARD_MIN_ENTROPY = saturationEndpointsAtTemperature(C.R2_T_MIN).vapor.entropy;
16
19
  const REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE = 1e-14;
17
20
  const REGION4_HS_NEWTON_UPPER = C.Tc - C.REGION4_SUBCRITICAL_TEMPERATURE_MARGIN;
18
- const REGION4_HS_NEAR_CRITICAL_UPPER = REGION4_HS_NEWTON_UPPER;
19
21
  const REGION4_TRIPLE_POINT_ENDPOINTS = saturationEndpointsAtTemperature(C.Tt);
20
22
  const REGION4_HS_T = [
21
23
  [0, 0, 0.179882673606601], [0, 3, -0.267507455199603], [0, 12, 1.16276722612600],
@@ -121,26 +123,47 @@ function entropyResidualEvaluation(h, s, temperatureGuess) {
121
123
  function entropyResidual(h, s, temperatureGuess) {
122
124
  return entropyResidualEvaluation(h, s, temperatureGuess).residual;
123
125
  }
124
- function gibbsResidualEvaluation(h, s, temperatureGuess) {
125
- const endpoints = saturationEndpointsAtTemperature(clampRegion4TemperatureBelowCritical(temperatureGuess));
126
- const T = endpoints.temperature;
127
- const { liquid, vapor } = endpoints;
128
- const deltaS = vapor.entropy - liquid.entropy;
129
- const deltaV = vapor.specificVolume - liquid.specificVolume;
130
- const pressureDerivative = deltaS / (1000 * deltaV);
131
- return {
132
- residual: liquid.enthalpy - T * liquid.entropy + T * s - h,
133
- derivative: 1000 * liquid.specificVolume * pressureDerivative - liquid.entropy + s,
134
- };
126
+ let region4HSValidCriticalUpper;
127
+ function region4HSCriticalUpper() {
128
+ if (region4HSValidCriticalUpper !== undefined)
129
+ return region4HSValidCriticalUpper;
130
+ let upper = REGION4_HS_NEWTON_UPPER;
131
+ try {
132
+ saturationEndpointsAtTemperature(upper);
133
+ }
134
+ catch (error) {
135
+ if (!(error instanceof SaturationEndpointError)) {
136
+ throw error;
137
+ }
138
+ let valid = C.Tc - 1e-4;
139
+ saturationEndpointsAtTemperature(valid);
140
+ for (let iteration = 0; iteration < 40; iteration += 1) {
141
+ const candidate = (valid + upper) / 2;
142
+ if (candidate === valid || candidate === upper)
143
+ break;
144
+ try {
145
+ saturationEndpointsAtTemperature(candidate);
146
+ valid = candidate;
147
+ }
148
+ catch (error) {
149
+ if (!(error instanceof SaturationEndpointError)) {
150
+ throw error;
151
+ }
152
+ upper = candidate;
153
+ }
154
+ }
155
+ upper = valid;
156
+ }
157
+ region4HSValidCriticalUpper = upper;
158
+ return upper;
135
159
  }
136
- function tryRegion4HSGibbsState(h, s) {
160
+ function tryRegion4HSEntropyState(h, s) {
137
161
  let cachedTemperature = Number.NaN;
138
162
  let cachedEvaluation = null;
139
163
  const evaluate = (temperature) => {
140
- const stableTemperature = Math.round(temperature / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
141
- if (stableTemperature !== cachedTemperature || cachedEvaluation === null) {
142
- cachedTemperature = stableTemperature;
143
- cachedEvaluation = gibbsResidualEvaluation(h, s, stableTemperature);
164
+ if (temperature !== cachedTemperature || cachedEvaluation === null) {
165
+ cachedTemperature = temperature;
166
+ cachedEvaluation = entropyResidualEvaluation(h, s, temperature);
144
167
  }
145
168
  return cachedEvaluation;
146
169
  };
@@ -156,38 +179,142 @@ function tryRegion4HSGibbsState(h, s) {
156
179
  if (!Number.isFinite(value.derivative) || value.derivative === 0)
157
180
  break;
158
181
  const candidate = temperature - value.residual / value.derivative;
159
- if (!(candidate >= C.Tt && candidate <= C.Tt + 0.1))
182
+ if (!(candidate >= C.Tt && candidate <= C.R2_T_MIN))
160
183
  break;
161
- temperature = Math.round(candidate / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
184
+ temperature = candidate;
162
185
  }
163
186
  }
164
- if (Math.abs(h - C.R3_H_CRT) >= 10 || Math.abs(s - C.R3_S_CRT) >= 0.1) {
165
- return null;
166
- }
167
- const initialTemperature = C.Tc - 1e-4;
168
- try {
169
- const initialState = finalizeRegion4HSState(h, s, initialTemperature);
170
- if (initialState !== null)
171
- return initialState;
172
- const root = bracketedNewton((T) => evaluate(T).residual, C.Tc - C.CRITICAL_T_EXCLUSION_BAND, REGION4_HS_NEAR_CRITICAL_UPPER, initialTemperature, {
173
- tolerance: C.REGION4_HS_RESIDUAL_TOLERANCE,
174
- derivative: (T) => evaluate(T).derivative,
175
- });
176
- const stableRoot = Math.round(root / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
177
- return finalizeRegion4HSState(h, s, stableRoot);
187
+ // Split the EOS seam; the valid anchor defers critical-ceiling calibration until needed.
188
+ const intervals = [[C.Tt, C.R2_T_MIN], [nextPositiveFloat(C.R2_T_MIN, 1), C.Tc - 1e-4]];
189
+ const liquid = s <= C.R3_S_CRT;
190
+ let numericalFailure = false;
191
+ let criticalInterval = false;
192
+ for (const [lower, initialUpper] of intervals) {
193
+ try {
194
+ const lowerValue = evaluate(lower);
195
+ if (Math.abs(lowerValue.residual) <= REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE) {
196
+ const state = finalizeRegion4HSState(h, s, lower);
197
+ if (state !== null)
198
+ return state;
199
+ }
200
+ if (s < lowerValue.liquidEntropy || s > lowerValue.vaporEntropy) {
201
+ // Split-EOS endpoint roundoff can exclude a numerically valid root.
202
+ if (Math.abs(lowerValue.residual) <= C.REGION4_HS_RESIDUAL_TOLERANCE)
203
+ numericalFailure = true;
204
+ continue;
205
+ }
206
+ const highTemperature = lower > C.R2_T_MIN;
207
+ if (highTemperature)
208
+ criticalInterval = true;
209
+ let upper = initialUpper;
210
+ let inside = lower;
211
+ let outside = upper;
212
+ let outsideValue = null;
213
+ let temperature = lower;
214
+ let value = lowerValue;
215
+ for (let iteration = 0; iteration < 100; iteration += 1) {
216
+ if (Math.abs(value.quality) <= REGION4_QUALITY_TOLERANCE ||
217
+ Math.abs(value.quality - 1) <= REGION4_QUALITY_TOLERANCE) {
218
+ const endpoint = exactEndpointHSState(h, s, temperature, value.quality < 0.5 ? 0 : 1);
219
+ if (endpoint !== null)
220
+ return endpoint;
221
+ const entropyQuality = rawQualityFromSaturationProperty(value.liquidEntropy, value.vaporEntropy, s);
222
+ if (endpointQuality(value.quality, entropyQuality) !== null &&
223
+ Math.abs(value.residual) <= REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE) {
224
+ const state = finalizeRegion4HSState(h, s, temperature);
225
+ if (state !== null)
226
+ return state;
227
+ }
228
+ }
229
+ const entropyInside = s >= value.liquidEntropy && s <= value.vaporEntropy;
230
+ if (entropyInside) {
231
+ inside = temperature;
232
+ if (temperature > lower && lowerValue.residual * value.residual <= 0) {
233
+ const root = bracketedNewton((T) => evaluate(T).residual, lower, inside, h / s, {
234
+ tolerance: REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE,
235
+ derivative: (T) => evaluate(T).derivative,
236
+ });
237
+ const quality = evaluate(root).quality;
238
+ if (Math.abs(quality) > REGION4_QUALITY_TOLERANCE &&
239
+ Math.abs(quality - 1) > REGION4_QUALITY_TOLERANCE) {
240
+ const state = finalizeRegion4HSState(h, s, root);
241
+ if (state !== null)
242
+ return state;
243
+ }
244
+ numericalFailure = true;
245
+ break;
246
+ }
247
+ if (temperature === upper) {
248
+ if (highTemperature && upper === initialUpper) {
249
+ upper = region4HSCriticalUpper();
250
+ if (upper > inside) {
251
+ outside = upper;
252
+ temperature = upper;
253
+ value = evaluate(temperature);
254
+ continue;
255
+ }
256
+ }
257
+ if (Math.abs(value.residual) <= REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE) {
258
+ numericalFailure = true;
259
+ }
260
+ break;
261
+ }
262
+ }
263
+ else {
264
+ outside = temperature;
265
+ outsideValue = value;
266
+ }
267
+ const midpoint = (inside + outside) / 2;
268
+ if (midpoint === inside || midpoint === outside) {
269
+ if (Math.abs(value.residual) <= C.REGION4_HS_RESIDUAL_TOLERANCE ||
270
+ (outsideValue !== null && lowerValue.residual * outsideValue.residual <= 0)) {
271
+ numericalFailure = true;
272
+ }
273
+ break;
274
+ }
275
+ const phaseResidual = (liquid ? value.liquidEntropy : value.vaporEntropy) - s;
276
+ const derivative = liquid ? value.liquidDsDT : value.vaporDsDT;
277
+ // A rounded entropy zero is still inside; retain an outside bound one float higher.
278
+ const candidate = phaseResidual === 0
279
+ ? nextPositiveFloat(temperature, 1)
280
+ : temperature - phaseResidual / derivative;
281
+ temperature = Number.isFinite(candidate) && candidate > inside && candidate < outside
282
+ ? candidate
283
+ : outsideValue === null ? outside : midpoint;
284
+ value = evaluate(temperature);
285
+ if (iteration === 99)
286
+ numericalFailure = true;
287
+ }
288
+ }
289
+ catch (error) {
290
+ if (!(error instanceof IF97Error))
291
+ throw error;
292
+ numericalFailure = true;
293
+ }
178
294
  }
179
- catch (error) {
180
- if (!(error instanceof IF97Error))
181
- throw error;
295
+ if (numericalFailure && criticalInterval) {
296
+ try {
297
+ const root = bracketedNewton((T) => evaluate(T).residual, 645, region4HSCriticalUpper(), C.Tc - 1e-4, {
298
+ tolerance: REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE,
299
+ derivative: (T) => evaluate(T).derivative,
300
+ });
301
+ const state = finalizeRegion4HSState(h, s, root);
302
+ if (state !== null)
303
+ return state;
304
+ }
305
+ catch (error) {
306
+ if (!(error instanceof IF97Error))
307
+ throw error;
308
+ }
182
309
  }
183
- return null;
310
+ return numericalFailure ? tryRegion4HSNewtonState(h, s) : null;
184
311
  }
185
312
  function tryRegion4HSNewtonState(h, s) {
186
313
  // h/s carries units of K and, because IF97 zeroes h and s at the triple-point
187
314
  // liquid, approximates the mean temperature of heat addition from that state —
188
315
  // close enough to land inside the dome for most inputs. It is clamped to the
189
316
  // saturation range and backed by fixed seeds spanning Tt to just below Tc.
190
- const seeds = [h / s, 300, 400, 500, 580, 630, 645];
317
+ const seeds = [h / s, 300, 400, 500, 580, 630, 645, nextPositiveFloat(C.R2_T_MIN, 1)];
191
318
  const tried = new Set();
192
319
  const hScale = Math.max(1, Math.abs(h));
193
320
  const sScale = Math.max(1, Math.abs(s));
@@ -221,13 +348,13 @@ function tryRegion4HSNewtonState(h, s) {
221
348
  ],
222
349
  };
223
350
  }, [seed, initialValue.quality], {
224
- tolerance: 1e-12,
225
- maxIterations: 30,
226
- isValid: ([T]) => T >= C.Tt && T <= REGION4_HS_NEWTON_UPPER,
351
+ tolerance: seed <= C.R2_T_MIN ? REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE : 1e-12,
352
+ maxIterations: 60,
353
+ isValid: ([T]) => T >= C.Tt && T <= REGION4_HS_NEWTON_UPPER &&
354
+ (seed <= C.R2_T_MIN ? T <= C.R2_T_MIN : T > C.R2_T_MIN),
227
355
  });
228
356
  if (isAdmissibleQuality(quality)) {
229
- const stableTemperature = Math.round(temperature / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
230
- const state = finalizeRegion4HSState(h, s, stableTemperature);
357
+ const state = finalizeRegion4HSState(h, s, temperature);
231
358
  if (state !== null)
232
359
  return state;
233
360
  }
@@ -244,11 +371,10 @@ function finalizeRegion4HSState(h, s, temperature) {
244
371
  const qualityFromEnthalpy = rawQualityFromSaturationProperty(endpoints.liquid.enthalpy, endpoints.vapor.enthalpy, h);
245
372
  const qualityFromEntropy = rawQualityFromSaturationProperty(endpoints.liquid.entropy, endpoints.vapor.entropy, s);
246
373
  const normalizedQualityFromEnthalpy = normalizeEndpointQuality(qualityFromEnthalpy);
247
- const normalizedQualityFromEntropy = normalizeEndpointQuality(qualityFromEntropy);
248
- if (!isAdmissibleQuality(normalizedQualityFromEnthalpy) || !isAdmissibleQuality(normalizedQualityFromEntropy)) {
374
+ if (!isAdmissibleQuality(qualityFromEnthalpy) || !isAdmissibleQuality(qualityFromEntropy)) {
249
375
  return null;
250
376
  }
251
- if (Math.abs(normalizedQualityFromEnthalpy - normalizedQualityFromEntropy) > REGION4_QUALITY_TOLERANCE) {
377
+ if (Math.abs(qualityFromEnthalpy - qualityFromEntropy) > REGION4_QUALITY_TOLERANCE) {
252
378
  return null;
253
379
  }
254
380
  const mixedEntropy = endpoints.liquid.entropy * (1 - qualityFromEnthalpy) +
@@ -262,8 +388,112 @@ function finalizeRegion4HSState(h, s, temperature) {
262
388
  catch {
263
389
  return null;
264
390
  }
265
- const exactEndpointQuality = endpointQuality(qualityFromEnthalpy, qualityFromEntropy);
266
- return mixSaturationState(endpoints, exactEndpointQuality ?? normalizedQualityFromEnthalpy);
391
+ let exactEndpointQuality = endpointQuality(qualityFromEnthalpy, qualityFromEntropy);
392
+ if (exactEndpointQuality !== null) {
393
+ const endpoint = exactEndpointQuality === 0 ? endpoints.liquid : endpoints.vapor;
394
+ if (Math.abs(endpoint.enthalpy - h) > backwardConstraintTolerance('enthalpy', h) ||
395
+ Math.abs(endpoint.entropy - s) > backwardConstraintTolerance('entropy', s)) {
396
+ if (qualityFromEnthalpy < 0 || qualityFromEnthalpy > 1 ||
397
+ qualityFromEntropy < 0 || qualityFromEntropy > 1)
398
+ return null;
399
+ exactEndpointQuality = null;
400
+ }
401
+ }
402
+ const state = mixSaturationState(endpoints, exactEndpointQuality ?? normalizedQualityFromEnthalpy);
403
+ // A quality clamp must not turn a single-phase input into an approximate endpoint.
404
+ if (qualityFromEnthalpy < 0 || qualityFromEnthalpy > 1 ||
405
+ qualityFromEntropy < 0 || qualityFromEntropy > 1 || state.quality === 0 || state.quality === 1) {
406
+ // Preserve the existing endpoint quality resolution in both reconstructed properties.
407
+ const enthalpyTolerance = Math.max(1e-9, (endpoints.vapor.enthalpy - endpoints.liquid.enthalpy) * REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE);
408
+ const entropyTolerance = Math.max(1e-12, (endpoints.vapor.entropy - endpoints.liquid.entropy) * REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE);
409
+ if ((state.quality !== 0 && state.quality !== 1) ||
410
+ Math.abs(state.enthalpy - h) > enthalpyTolerance || Math.abs(state.entropy - s) > entropyTolerance) {
411
+ return null;
412
+ }
413
+ }
414
+ if (Math.abs(state.enthalpy - h) > backwardConstraintTolerance('enthalpy', h) ||
415
+ Math.abs(state.entropy - s) > backwardConstraintTolerance('entropy', s)) {
416
+ return null;
417
+ }
418
+ const nearbyEndpoint = Math.abs(qualityFromEnthalpy) <= REGION4_QUALITY_TOLERANCE &&
419
+ Math.abs(qualityFromEntropy) <= REGION4_QUALITY_TOLERANCE ? 0 :
420
+ Math.abs(qualityFromEnthalpy - 1) <= REGION4_QUALITY_TOLERANCE &&
421
+ Math.abs(qualityFromEntropy - 1) <= REGION4_QUALITY_TOLERANCE ? 1 : null;
422
+ if (exactEndpointQuality === null && nearbyEndpoint !== null &&
423
+ state.quality !== null && state.quality > 0 && state.quality < 1) {
424
+ try {
425
+ const endpoint = exactEndpointHSState(h, s, temperature, nearbyEndpoint);
426
+ if (endpoint !== null)
427
+ return endpoint;
428
+ }
429
+ catch (error) {
430
+ if (!(error instanceof IF97Error))
431
+ throw error;
432
+ }
433
+ }
434
+ return state;
435
+ }
436
+ function exactEndpointHSState(h, s, initialTemperature, quality) {
437
+ const valueAt = (T) => entropyResidualEvaluation(h, s, T);
438
+ const enthalpyAt = (T) => {
439
+ const value = valueAt(T);
440
+ return quality === 0 ? value.liquidEnthalpy : value.vaporEnthalpy;
441
+ };
442
+ const derivativeAt = (T) => {
443
+ const value = valueAt(T);
444
+ return quality === 0 ? value.liquidDhDT : value.vaporDhDT;
445
+ };
446
+ let temperature = initialTemperature;
447
+ let previousTemperature = Number.NaN;
448
+ let previousResidual = Number.NaN;
449
+ for (let iteration = 0; iteration < 8; iteration += 1) {
450
+ const endpoints = saturationEndpointsAtTemperature(temperature);
451
+ const state = quality === 0 ? endpoints.liquid : endpoints.vapor;
452
+ if (state.enthalpy === h && state.entropy === s) {
453
+ assertRegion4StateAllowed(endpoints.pressure, endpoints.temperature, 'Region4HS');
454
+ return mixSaturationState(endpoints, quality);
455
+ }
456
+ const residual = state.enthalpy - h;
457
+ if (previousResidual * residual < 0) {
458
+ const root = bracketedNewton((T) => enthalpyAt(T) - h, previousTemperature, temperature, undefined, { tolerance: Number.EPSILON, derivative: derivativeAt });
459
+ let below = root;
460
+ let above = root;
461
+ // EOS cancellation can reverse the enthalpy sign before the exact endpoint float.
462
+ for (let neighbor = 0; neighbor < 8; neighbor += 1) {
463
+ for (const candidate of neighbor === 0 ? [root] : [below, above]) {
464
+ if (candidate < C.Tt || candidate > REGION4_HS_NEWTON_UPPER ||
465
+ (candidate <= C.R2_T_MIN) !== (initialTemperature <= C.R2_T_MIN))
466
+ continue;
467
+ const refined = saturationEndpointsAtTemperature(candidate);
468
+ const endpoint = quality === 0 ? refined.liquid : refined.vapor;
469
+ if (endpoint.enthalpy !== h || endpoint.entropy !== s)
470
+ continue;
471
+ assertRegion4StateAllowed(refined.pressure, refined.temperature, 'Region4HS');
472
+ return mixSaturationState(refined, quality);
473
+ }
474
+ below = nextPositiveFloat(below, -1);
475
+ above = nextPositiveFloat(above, 1);
476
+ }
477
+ return null;
478
+ }
479
+ const value = valueAt(temperature);
480
+ const derivative = residual === 0
481
+ ? quality === 0 ? value.liquidDsDT : value.vaporDsDT
482
+ : quality === 0 ? value.liquidDhDT : value.vaporDhDT;
483
+ const correction = (residual === 0 ? state.entropy - s : residual) / derivative;
484
+ const newtonTemperature = temperature - correction;
485
+ const candidate = newtonTemperature === temperature && correction !== 0
486
+ ? nextPositiveFloat(temperature, correction > 0 ? -1 : 1)
487
+ : newtonTemperature;
488
+ if (!Number.isFinite(candidate) || candidate === temperature || candidate < C.Tt ||
489
+ candidate > REGION4_HS_NEWTON_UPPER ||
490
+ (candidate <= C.R2_T_MIN) !== (initialTemperature <= C.R2_T_MIN))
491
+ return null;
492
+ previousTemperature = temperature;
493
+ previousResidual = residual;
494
+ temperature = candidate;
495
+ }
496
+ return null;
267
497
  }
268
498
  function isTriplePointHS(h, s) {
269
499
  const { liquid, vapor } = REGION4_TRIPLE_POINT_ENDPOINTS;
@@ -324,11 +554,5 @@ export function tryRegion4HSState(h, s) {
324
554
  const backwardState = tryRegion4HSBackwardState(h, s);
325
555
  if (backwardState !== null)
326
556
  return backwardState;
327
- const gibbsState = tryRegion4HSGibbsState(h, s);
328
- if (gibbsState !== null)
329
- return gibbsState;
330
- const newtonState = tryRegion4HSNewtonState(h, s);
331
- if (newtonState !== null)
332
- return newtonState;
333
- return null;
557
+ return tryRegion4HSEntropyState(h, s);
334
558
  }
@@ -9,13 +9,13 @@
9
9
  import type { BasicProperties } from '../types.js';
10
10
  /**
11
11
  * Solve for thermodynamic state given pressure and vapour quality.
12
- * @param p - Pressure [MPa], Pt <= p <= Pc
12
+ * @param p - Pressure [MPa], Pt <= p < Pc
13
13
  * @param x - Vapour quality [0, 1]
14
14
  */
15
15
  export declare function solvePx(p: number, x: number): BasicProperties;
16
16
  /**
17
17
  * Solve for thermodynamic state given temperature and vapour quality.
18
- * @param T - Temperature [K], T_MIN <= T <= Tc
18
+ * @param T - Temperature [K], Tt <= T < Tc
19
19
  * @param x - Vapour quality [0, 1]
20
20
  */
21
21
  export declare function solveTx(T: number, x: number): BasicProperties;
@@ -8,13 +8,12 @@
8
8
  */
9
9
  import { OutOfRangeError } from '../types.js';
10
10
  import { assertFiniteNumber } from '../core/input-validation.js';
11
- import { saturationPressure } from '../regions/region4.js';
12
- import { mixSaturationState, saturationEndpointsAtPressure } from './common.js';
13
- import { assertRegion4PressureStateInput, assertRegion4TemperatureStateInput, normalizeRegion4Pressure, } from './region4-boundaries.js';
11
+ import { mixSaturationState, saturationEndpointsAtPressure, saturationEndpointsAtTemperature, } from './common.js';
12
+ import { assertRegion4PressureStateInput, assertRegion4TemperatureStateInput, } from './region4-boundaries.js';
14
13
  // ─── solvePx ────────────────────────────────────────────────────────────────
15
14
  /**
16
15
  * Solve for thermodynamic state given pressure and vapour quality.
17
- * @param p - Pressure [MPa], Pt <= p <= Pc
16
+ * @param p - Pressure [MPa], Pt <= p < Pc
18
17
  * @param x - Vapour quality [0, 1]
19
18
  */
20
19
  export function solvePx(p, x) {
@@ -28,7 +27,7 @@ export function solvePx(p, x) {
28
27
  // ─── solveTx ────────────────────────────────────────────────────────────────
29
28
  /**
30
29
  * Solve for thermodynamic state given temperature and vapour quality.
31
- * @param T - Temperature [K], T_MIN <= T <= Tc
30
+ * @param T - Temperature [K], Tt <= T < Tc
32
31
  * @param x - Vapour quality [0, 1]
33
32
  */
34
33
  export function solveTx(T, x) {
@@ -38,6 +37,7 @@ export function solveTx(T, x) {
38
37
  throw new OutOfRangeError('Quality', x, 0, 1);
39
38
  }
40
39
  const temperature = assertRegion4TemperatureStateInput(T, 'solveTx');
41
- const p = normalizeRegion4Pressure(saturationPressure(temperature));
42
- return solvePx(p, x);
40
+ const endpoints = saturationEndpointsAtTemperature(temperature);
41
+ assertRegion4PressureStateInput(endpoints.pressure, 'solveTx');
42
+ return mixSaturationState(endpoints, x);
43
43
  }
@@ -1,7 +1,9 @@
1
1
  /** Configuration for the bracketed Newton solver. */
2
2
  export interface BracketedNewtonOptions {
3
- /** Convergence tolerance on |f(x)| (default 1e-9) */
3
+ /** Bracket-width tolerance scaled by max(1, |x|), also the default residual tolerance (default 1e-9). */
4
4
  tolerance?: number;
5
+ /** Tolerance on |f(x)| (defaults to tolerance); 0 requires exact zero or bracket convergence. */
6
+ residualTolerance?: number;
5
7
  /** Maximum number of iterations (default 100) */
6
8
  maxIterations?: number;
7
9
  /** Relative step size for central-difference derivative (default 1e-6) */
@@ -14,14 +16,15 @@ export interface BracketedNewtonOptions {
14
16
  *
15
17
  * Each iteration attempts a Newton step using the supplied analytical derivative,
16
18
  * or a central difference when none is supplied. If the Newton candidate falls
17
- * outside the current bracket, a bisection step is used instead.
19
+ * outside the current bracket, or two iterations fail to halve its width,
20
+ * a bisection step is used instead.
18
21
  *
19
22
  * @param f - Continuous function whose root is sought
20
23
  * @param lower - Lower bound of the bracket
21
24
  * @param upper - Upper bound of the bracket
22
25
  * @param initialGuess - Optional starting point (must lie within bracket)
23
26
  * @param options - Solver configuration
24
- * @returns The root x such that |f(x)| ≤ tolerance
27
+ * @returns A root satisfying the residual tolerance or bracket-width convergence criterion
25
28
  * @throws {ConvergenceError} if the bracket signs are the same or max iterations exceeded
26
29
  */
27
30
  export declare function bracketedNewton(f: (x: number) => number, lower: number, upper: number, initialGuess?: number, options?: BracketedNewtonOptions): number;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * A hybrid root-finder that combines Newton-Raphson steps with bisection
5
5
  * fallback. The bracket [lower, upper] is maintained so that f(a) and f(b)
6
- * have opposite signs, guaranteeing convergence even when the Newton step
7
- * would overshoot or the derivative estimate is poor.
6
+ * have opposite signs. A stagnation safeguard forces bisection when two
7
+ * iterations fail to halve the bracket.
8
8
  */
9
9
  import { ConvergenceError, IF97Error } from '../types.js';
10
10
  /**
@@ -22,27 +22,29 @@ function numericalDerivative(f, x, lower, upper, step) {
22
22
  *
23
23
  * Each iteration attempts a Newton step using the supplied analytical derivative,
24
24
  * or a central difference when none is supplied. If the Newton candidate falls
25
- * outside the current bracket, a bisection step is used instead.
25
+ * outside the current bracket, or two iterations fail to halve its width,
26
+ * a bisection step is used instead.
26
27
  *
27
28
  * @param f - Continuous function whose root is sought
28
29
  * @param lower - Lower bound of the bracket
29
30
  * @param upper - Upper bound of the bracket
30
31
  * @param initialGuess - Optional starting point (must lie within bracket)
31
32
  * @param options - Solver configuration
32
- * @returns The root x such that |f(x)| ≤ tolerance
33
+ * @returns A root satisfying the residual tolerance or bracket-width convergence criterion
33
34
  * @throws {ConvergenceError} if the bracket signs are the same or max iterations exceeded
34
35
  */
35
36
  export function bracketedNewton(f, lower, upper, initialGuess, options) {
36
37
  const tolerance = options?.tolerance ?? 1e-9;
38
+ const residualTolerance = options?.residualTolerance ?? tolerance;
37
39
  const maxIterations = options?.maxIterations ?? 100;
38
40
  const derivativeStep = options?.derivativeStep ?? 1e-6;
39
41
  let a = Math.min(lower, upper);
40
42
  let b = Math.max(lower, upper);
41
43
  let fa = f(a);
42
44
  let fb = f(b);
43
- if (Math.abs(fa) <= tolerance)
45
+ if (Math.abs(fa) <= residualTolerance)
44
46
  return a;
45
- if (Math.abs(fb) <= tolerance)
47
+ if (Math.abs(fb) <= residualTolerance)
46
48
  return b;
47
49
  if (fa * fb > 0) {
48
50
  throw new IF97Error(`bracketedNewton: f(${a}) and f(${b}) have the same sign (${fa.toExponential(3)}, ${fb.toExponential(3)}); bracket does not contain a root`);
@@ -51,9 +53,11 @@ export function bracketedNewton(f, lower, upper, initialGuess, options) {
51
53
  if (!(x > a && x < b)) {
52
54
  x = (a + b) / 2;
53
55
  }
56
+ let previousWidth = b - a;
57
+ let widthTwoStepsAgo = previousWidth;
54
58
  for (let iter = 0; iter < maxIterations; iter++) {
55
59
  const fx = f(x);
56
- if (Math.abs(fx) <= tolerance) {
60
+ if (Math.abs(fx) <= residualTolerance) {
57
61
  return x;
58
62
  }
59
63
  if (fa * fx < 0) {
@@ -75,9 +79,12 @@ export function bracketedNewton(f, lower, upper, initialGuess, options) {
75
79
  const newtonCandidate = derivative === null || !Number.isFinite(derivative) || derivative === 0
76
80
  ? NaN
77
81
  : x - fx / derivative;
78
- x = Number.isFinite(newtonCandidate) && newtonCandidate > a && newtonCandidate < b
82
+ const sufficientProgress = iter === 0 || b - a <= widthTwoStepsAgo / 2;
83
+ x = sufficientProgress && Number.isFinite(newtonCandidate) && newtonCandidate > a && newtonCandidate < b
79
84
  ? newtonCandidate
80
85
  : midpoint;
86
+ widthTwoStepsAgo = previousWidth;
87
+ previousWidth = b - a;
81
88
  }
82
89
  throw new ConvergenceError('bracketedNewton', maxIterations);
83
90
  }
@@ -0,0 +1,6 @@
1
+ export type DoubleDouble = readonly [number, number];
2
+ export declare function addExtended(a: DoubleDouble, b: DoubleDouble): DoubleDouble;
3
+ export declare function multiplyExtended(a: DoubleDouble, b: DoubleDouble): DoubleDouble;
4
+ export declare function divideExtended(a: DoubleDouble, b: DoubleDouble): DoubleDouble;
5
+ export declare function subtractExtended(a: DoubleDouble, b: DoubleDouble): DoubleDouble;
6
+ export declare function sqrtExtended(value: DoubleDouble): DoubleDouble;
@@ -0,0 +1,38 @@
1
+ function twoSum(a, b) {
2
+ const sum = a + b;
3
+ const recoveredB = sum - a;
4
+ return [sum, (a - (sum - recoveredB)) + (b - recoveredB)];
5
+ }
6
+ function twoProduct(a, b) {
7
+ const product = a * b;
8
+ // Dekker's split for binary64: 2^27 + 1.
9
+ const splitA = 134217729 * a;
10
+ const highA = splitA - (splitA - a);
11
+ const lowA = a - highA;
12
+ const splitB = 134217729 * b;
13
+ const highB = splitB - (splitB - b);
14
+ const lowB = b - highB;
15
+ return [product, ((highA * highB - product) + highA * lowB + lowA * highB) + lowA * lowB];
16
+ }
17
+ export function addExtended(a, b) {
18
+ const [sum, error] = twoSum(a[0], b[0]);
19
+ return twoSum(sum, error + a[1] + b[1]);
20
+ }
21
+ export function multiplyExtended(a, b) {
22
+ const [product, error] = twoProduct(a[0], b[0]);
23
+ return twoSum(product, error + a[0] * b[1] + a[1] * b[0]);
24
+ }
25
+ export function divideExtended(a, b) {
26
+ const quotient = a[0] / b[0];
27
+ const product = multiplyExtended([quotient, 0], b);
28
+ const residual = addExtended(a, [-product[0], -product[1]]);
29
+ return addExtended([quotient, 0], [residual[0] / b[0], 0]);
30
+ }
31
+ export function subtractExtended(a, b) {
32
+ return addExtended(a, [-b[0], -b[1]]);
33
+ }
34
+ export function sqrtExtended(value) {
35
+ const initial = Math.sqrt(value[0]);
36
+ const residual = subtractExtended(value, multiplyExtended([initial, 0], [initial, 0]));
37
+ return addExtended([initial, 0], divideExtended(residual, [2 * initial, 0]));
38
+ }
@@ -11,6 +11,7 @@
11
11
  * Dynamic viscosity of water/steam [Pa·s]
12
12
  * @param T Temperature [K]
13
13
  * @param rho Density [kg/m³]
14
+ * @throws {IF97Error} If the calculated viscosity is non-finite or non-positive.
14
15
  */
15
16
  export declare function viscosity(T: number, rho: number): number;
16
17
  /**
@@ -22,7 +23,7 @@ export declare function viscosity(T: number, rho: number): number;
22
23
  *
23
24
  * @param T Temperature [K]
24
25
  * @param rho Density [kg/m³]
25
- * @param cp Isobaric heat capacity [kJ/(kg·K)] (optional)
26
+ * @param cp Positive isobaric heat capacity [kJ/(kg·K)] (optional)
26
27
  * @param cv Isochoric heat capacity [kJ/(kg·K)] (optional)
27
28
  * @param drhodP_T Isothermal density derivative ∂ρ/∂P|_T [kg/m³/MPa] (optional)
28
29
  * @param mu Dynamic viscosity [Pa·s] (optional)
@@ -36,8 +37,9 @@ export declare function thermalConductivity(T: number, rho: number, cp?: number,
36
37
  export declare function surfaceTension(T: number): number | null;
37
38
  /**
38
39
  * Static dielectric constant [dimensionless]
39
- * @param T Temperature [K]
40
+ * @param T Temperature [K], must be greater than 228 K
40
41
  * @param rho Density [kg/m³]
42
+ * @throws {IF97Error} If T is at or below the equation's 228 K singularity.
41
43
  */
42
44
  export declare function dielectricConstant(T: number, rho: number): number;
43
45
  /**