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
@@ -48,4 +48,3 @@ export declare function qualityFromSaturationProperty(liquidValue: number, vapor
48
48
  * @param qualityInput - Vapour quality (0–1)
49
49
  */
50
50
  export declare function mixSaturationState(endpoints: SaturationEndpoints, qualityInput: number): BasicProperties;
51
- //# sourceMappingURL=common.d.ts.map
@@ -5,27 +5,21 @@ import { region2 } from '../regions/region2.js';
5
5
  import { region3ByRhoT } from '../regions/region3.js';
6
6
  import { saturationPressure, saturationTemperature } from '../regions/region4.js';
7
7
  import { region3SatVolume } from '../regions/region3-subregions.js';
8
- import { newtonRaphson } from '../solvers/newton-raphson.js';
8
+ import { solveRegion3StateAtPressure } from '../core/region3-pt.js';
9
9
  import { normalizeRegion4Pressure, normalizeRegion4Temperature, } from './region4-boundaries.js';
10
10
  /** Tag a single-phase state as Region 4 with the given vapour quality. */
11
11
  function withRegion4Metadata(state, quality) {
12
12
  return { ...state, region: Region.Region4, quality };
13
13
  }
14
14
  /**
15
- * Iteratively solve for Region 3 density at saturation.
15
+ * Solve for the Region 3 saturation endpoint state.
16
16
  *
17
17
  * @param p - Saturation pressure [MPa]
18
18
  * @param T - Saturation temperature [K]
19
19
  * @param x - Phase indicator: 0 = liquid, 1 = vapour
20
20
  */
21
- function solveR3Density(p, T, x) {
22
- const v0 = region3SatVolume(p, T, x);
23
- return newtonRaphson((rho) => {
24
- if (!Number.isFinite(rho) || rho <= 0) {
25
- return Number.NaN;
26
- }
27
- return region3ByRhoT(rho, T).pressure - p;
28
- }, 1 / v0);
21
+ function solveR3Endpoint(p, T, x) {
22
+ return solveRegion3StateAtPressure(p, T, 1 / region3SatVolume(p, T, x));
29
23
  }
30
24
  /** Clamp vapour quality to [0, 1], snapping near-zero/one values to the boundary. */
31
25
  function clampQuality(x) {
@@ -69,8 +63,8 @@ export function saturationEndpointsAtPressure(p) {
69
63
  return {
70
64
  pressure,
71
65
  temperature,
72
- liquid: region3ByRhoT(solveR3Density(pressure, temperature, 0), temperature),
73
- vapor: region3ByRhoT(solveR3Density(pressure, temperature, 1), temperature),
66
+ liquid: solveR3Endpoint(pressure, temperature, 0),
67
+ vapor: solveR3Endpoint(pressure, temperature, 1),
74
68
  };
75
69
  }
76
70
  return {
@@ -150,4 +144,3 @@ export function mixSaturationState(endpoints, qualityInput) {
150
144
  isothermalCompressibility: null,
151
145
  };
152
146
  }
153
- //# sourceMappingURL=common.js.map
@@ -1,14 +1,9 @@
1
1
  export declare function normalizeRegion4Pressure(p: number): number;
2
2
  export declare function normalizeRegion4Temperature(T: number): number;
3
3
  export declare function clampRegion4TemperatureBelowCritical(T: number): number;
4
- export declare function isRegion4CriticalPressure(p: number): boolean;
5
- export declare function isRegion4CriticalTemperature(T: number): boolean;
6
4
  export declare function assertRegion4PressureStateInput(p: number, solverName: string): number;
7
5
  export declare function assertRegion4TemperatureStateInput(T: number, solverName: string): number;
8
6
  export declare function assertRegion4StateAllowed(pressure: number, temperature: number, solverName: string): void;
9
- export declare function isCriticalRegion4Enthalpy(h: number): boolean;
10
- export declare function isCriticalRegion4Entropy(s: number): boolean;
11
7
  export declare function assertCriticalRegion4PHInput(p: number, h: number, solverName: string): void;
12
8
  export declare function assertCriticalRegion4PSInput(p: number, s: number, solverName: string): void;
13
9
  export declare function assertCriticalRegion4HSInput(h: number, s: number, solverName: string): void;
14
- //# sourceMappingURL=region4-boundaries.d.ts.map
@@ -21,10 +21,10 @@ export function normalizeRegion4Temperature(T) {
21
21
  export function clampRegion4TemperatureBelowCritical(T) {
22
22
  return Math.max(C.Tt, Math.min(T, C.Tc - C.REGION4_SUBCRITICAL_TEMPERATURE_MARGIN));
23
23
  }
24
- export function isRegion4CriticalPressure(p) {
24
+ function isRegion4CriticalPressure(p) {
25
25
  return normalizeRegion4Pressure(p) === C.Pc;
26
26
  }
27
- export function isRegion4CriticalTemperature(T) {
27
+ function isRegion4CriticalTemperature(T) {
28
28
  return normalizeRegion4Temperature(T) === C.Tc;
29
29
  }
30
30
  function criticalPointMessage(solverName) {
@@ -55,10 +55,10 @@ export function assertRegion4StateAllowed(pressure, temperature, solverName) {
55
55
  throw new IF97Error(criticalPointMessage(solverName));
56
56
  }
57
57
  }
58
- export function isCriticalRegion4Enthalpy(h) {
58
+ function isCriticalRegion4Enthalpy(h) {
59
59
  return Math.abs(h - C.R3_H_CRT) <= C.REGION4_CRITICAL_HS_TOLERANCE;
60
60
  }
61
- export function isCriticalRegion4Entropy(s) {
61
+ function isCriticalRegion4Entropy(s) {
62
62
  return Math.abs(s - C.R3_S_CRT) <= C.REGION4_CRITICAL_HS_TOLERANCE;
63
63
  }
64
64
  export function assertCriticalRegion4PHInput(p, h, solverName) {
@@ -76,4 +76,3 @@ export function assertCriticalRegion4HSInput(h, s, solverName) {
76
76
  throw new IF97Error(criticalPointMessage(solverName));
77
77
  }
78
78
  }
79
- //# sourceMappingURL=region4-boundaries.js.map
@@ -1,3 +1,4 @@
1
1
  import type { BasicProperties } from '../types.js';
2
+ /** IAPWS SR4-04(2014), Eq. 9. Valid for Region 4 states at T <= 623.15 K. */
3
+ export declare function region4BackwardTemperatureHS(h: number, s: number): number;
2
4
  export declare function tryRegion4HSState(h: number, s: number): BasicProperties | null;
3
- //# sourceMappingURL=region4-hs.d.ts.map
@@ -1,19 +1,54 @@
1
1
  import * as C from '../constants.js';
2
2
  import { bracketedNewton } from '../solvers/bracketed-newton.js';
3
+ import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
4
+ import { IF97Error } from '../types.js';
3
5
  import { mixSaturationState, rawQualityFromSaturationProperty, saturationEndpointsAtTemperature, } from './common.js';
4
6
  import { assertRegion4StateAllowed, clampRegion4TemperatureBelowCritical, } from './region4-boundaries.js';
5
7
  const REGION4_QUALITY_TOLERANCE = 1e-6;
6
- function isClose(a, b) {
7
- return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
8
+ const REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE = 1e-10;
9
+ /** Lower entropy limit of the SR4-04 T_sat(h,s) equation [kJ/(kg·K)].
10
+ * 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
13
+ * falls through to the general Newton path instead.
14
+ * Ref: IAPWS SR4-04(2014), Eq. 9. */
15
+ const REGION4_HS_BACKWARD_MIN_ENTROPY = 5.210887825;
16
+ const REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE = 1e-14;
17
+ const REGION4_HS_NEWTON_UPPER = C.Tc - C.REGION4_SUBCRITICAL_TEMPERATURE_MARGIN;
18
+ const REGION4_HS_NEAR_CRITICAL_UPPER = REGION4_HS_NEWTON_UPPER;
19
+ const REGION4_TRIPLE_POINT_ENDPOINTS = saturationEndpointsAtTemperature(C.Tt);
20
+ const REGION4_HS_T = [
21
+ [0, 0, 0.179882673606601], [0, 3, -0.267507455199603], [0, 12, 1.16276722612600],
22
+ [1, 0, 0.147545428713616], [1, 1, -0.512871635973248], [1, 2, 0.421333567697984],
23
+ [1, 5, 0.563749522189870], [2, 0, 0.429274443819153], [2, 5, -3.35704552142140],
24
+ [2, 8, 10.8890916499278], [3, 0, -0.248483390456012], [3, 2, 0.304153221906390],
25
+ [3, 3, -0.494819763939905], [3, 4, 1.07551674933261], [4, 0, 0.0733888415457688],
26
+ [4, 1, 0.0140170545411085], [5, 1, -0.106110975998808], [5, 2, 0.0168324361811875],
27
+ [5, 4, 1.25028363714877], [5, 16, 1013.16840309509], [6, 6, -1.51791558000712],
28
+ [6, 8, 52.4277865990866], [6, 22, 23049.5545563912], [8, 1, 0.0249459806365456],
29
+ [10, 20, 2107964.67412137], [10, 36, 366836848.613065], [12, 24, -144814105.365163],
30
+ [14, 1, -0.00179276373003590], [14, 28, 4899556021.00459], [16, 12, 471.262212070518],
31
+ [16, 32, -82929439019.8652], [18, 14, -1715.45662263191], [18, 22, 3557776.82973575],
32
+ [18, 36, 586062760258.436], [20, 24, -12988763.5078195], [28, 36, 31724744937.1057],
33
+ ];
34
+ /** IAPWS SR4-04(2014), Eq. 9. Valid for Region 4 states at T <= 623.15 K. */
35
+ export function region4BackwardTemperatureHS(h, s) {
36
+ const eta = h / 2800;
37
+ const sigma = s / 9.2;
38
+ let theta = 0;
39
+ for (const [I, J, n] of REGION4_HS_T) {
40
+ theta += n * Math.pow(eta - 0.119, I) * Math.pow(sigma - 1.07, J);
41
+ }
42
+ return 550 * theta;
8
43
  }
9
44
  function normalizeEndpointQuality(quality) {
10
45
  if (!Number.isFinite(quality)) {
11
46
  return Number.NaN;
12
47
  }
13
- if (Math.abs(quality) <= REGION4_QUALITY_TOLERANCE) {
48
+ if (quality < 0 && quality >= -REGION4_QUALITY_TOLERANCE) {
14
49
  return 0;
15
50
  }
16
- if (Math.abs(quality - 1) <= REGION4_QUALITY_TOLERANCE) {
51
+ if (quality > 1 && quality <= 1 + REGION4_QUALITY_TOLERANCE) {
17
52
  return 1;
18
53
  }
19
54
  return quality;
@@ -23,13 +58,186 @@ function isAdmissibleQuality(quality) {
23
58
  && quality >= -REGION4_QUALITY_TOLERANCE
24
59
  && quality <= 1 + REGION4_QUALITY_TOLERANCE;
25
60
  }
26
- function entropyResidual(h, s, temperatureGuess) {
61
+ function endpointQuality(qualityFromEnthalpy, qualityFromEntropy) {
62
+ if (Math.abs(qualityFromEnthalpy) <= REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE &&
63
+ Math.abs(qualityFromEntropy) <= REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE) {
64
+ return 0;
65
+ }
66
+ if (Math.abs(qualityFromEnthalpy - 1) <= REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE &&
67
+ Math.abs(qualityFromEntropy - 1) <= REGION4_ENDPOINT_QUALITY_SNAP_TOLERANCE) {
68
+ return 1;
69
+ }
70
+ return null;
71
+ }
72
+ function entropyResidualEvaluation(h, s, temperatureGuess) {
27
73
  const endpoints = saturationEndpointsAtTemperature(clampRegion4TemperatureBelowCritical(temperatureGuess));
28
- const quality = rawQualityFromSaturationProperty(endpoints.liquid.enthalpy, endpoints.vapor.enthalpy, h);
74
+ const T = endpoints.temperature;
75
+ const { liquid, vapor } = endpoints;
76
+ const quality = rawQualityFromSaturationProperty(liquid.enthalpy, vapor.enthalpy, h);
29
77
  if (!Number.isFinite(quality)) {
30
- return Number.NaN;
78
+ return {
79
+ residual: Number.NaN,
80
+ derivative: Number.NaN,
81
+ quality,
82
+ liquidEnthalpy: liquid.enthalpy,
83
+ vaporEnthalpy: vapor.enthalpy,
84
+ liquidEntropy: liquid.entropy,
85
+ vaporEntropy: vapor.entropy,
86
+ liquidDhDT: Number.NaN,
87
+ vaporDhDT: Number.NaN,
88
+ liquidDsDT: Number.NaN,
89
+ vaporDsDT: Number.NaN,
90
+ };
91
+ }
92
+ const deltaH = vapor.enthalpy - liquid.enthalpy;
93
+ const deltaS = vapor.entropy - liquid.entropy;
94
+ const deltaV = vapor.specificVolume - liquid.specificVolume;
95
+ const pressureDerivative = deltaS / (1000 * deltaV);
96
+ const phaseDerivatives = (state) => {
97
+ const cp = state.cp ?? Number.NaN;
98
+ const alpha = state.isobaricExpansion ?? Number.NaN;
99
+ return [
100
+ cp + 1000 * state.specificVolume * (1 - T * alpha) * pressureDerivative,
101
+ cp / T - 1000 * state.specificVolume * alpha * pressureDerivative,
102
+ ];
103
+ };
104
+ const [liquidDhDT, liquidDsDT] = phaseDerivatives(liquid);
105
+ const [vaporDhDT, vaporDsDT] = phaseDerivatives(vapor);
106
+ const qualityDerivative = -(liquidDhDT + quality * (vaporDhDT - liquidDhDT)) / deltaH;
107
+ return {
108
+ residual: liquid.entropy * (1 - quality) + vapor.entropy * quality - s,
109
+ derivative: liquidDsDT + quality * (vaporDsDT - liquidDsDT) + qualityDerivative * deltaS,
110
+ quality,
111
+ liquidEnthalpy: liquid.enthalpy,
112
+ vaporEnthalpy: vapor.enthalpy,
113
+ liquidEntropy: liquid.entropy,
114
+ vaporEntropy: vapor.entropy,
115
+ liquidDhDT,
116
+ vaporDhDT,
117
+ liquidDsDT,
118
+ vaporDsDT,
119
+ };
120
+ }
121
+ function entropyResidual(h, s, temperatureGuess) {
122
+ return entropyResidualEvaluation(h, s, temperatureGuess).residual;
123
+ }
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
+ };
135
+ }
136
+ function tryRegion4HSGibbsState(h, s) {
137
+ let cachedTemperature = Number.NaN;
138
+ let cachedEvaluation = null;
139
+ 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);
144
+ }
145
+ return cachedEvaluation;
146
+ };
147
+ if (h < 10 && s < 0.1) {
148
+ let temperature = C.Tt;
149
+ for (let iteration = 0; iteration < 20; iteration += 1) {
150
+ const value = entropyResidualEvaluation(h, s, temperature);
151
+ if (Math.abs(value.residual) <= REGION4_HS_ENDPOINT_RESIDUAL_TOLERANCE) {
152
+ const state = finalizeRegion4HSState(h, s, temperature);
153
+ if (state !== null)
154
+ return state;
155
+ }
156
+ if (!Number.isFinite(value.derivative) || value.derivative === 0)
157
+ break;
158
+ const candidate = temperature - value.residual / value.derivative;
159
+ if (!(candidate >= C.Tt && candidate <= C.Tt + 0.1))
160
+ break;
161
+ temperature = Math.round(candidate / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
162
+ }
163
+ }
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);
31
178
  }
32
- return endpoints.liquid.entropy * (1 - quality) + endpoints.vapor.entropy * quality - s;
179
+ catch (error) {
180
+ if (!(error instanceof IF97Error))
181
+ throw error;
182
+ }
183
+ return null;
184
+ }
185
+ function tryRegion4HSNewtonState(h, s) {
186
+ // h/s carries units of K and, because IF97 zeroes h and s at the triple-point
187
+ // liquid, approximates the mean temperature of heat addition from that state —
188
+ // close enough to land inside the dome for most inputs. It is clamped to the
189
+ // saturation range and backed by fixed seeds spanning Tt to just below Tc.
190
+ const seeds = [h / s, 300, 400, 500, 580, 630, 645];
191
+ const tried = new Set();
192
+ const hScale = Math.max(1, Math.abs(h));
193
+ const sScale = Math.max(1, Math.abs(s));
194
+ for (const rawSeed of seeds) {
195
+ const seed = Math.max(C.Tt, Math.min(REGION4_HS_NEWTON_UPPER, rawSeed));
196
+ if (!Number.isFinite(seed) || tried.has(seed))
197
+ continue;
198
+ tried.add(seed);
199
+ try {
200
+ const initialValue = entropyResidualEvaluation(h, s, seed);
201
+ if (!Number.isFinite(initialValue.quality))
202
+ continue;
203
+ const [temperature, quality] = dampedNewton2D(([T, x]) => {
204
+ const value = entropyResidualEvaluation(h, s, T);
205
+ const deltaH = value.vaporEnthalpy - value.liquidEnthalpy;
206
+ const deltaS = value.vaporEntropy - value.liquidEntropy;
207
+ return {
208
+ residual: [
209
+ (value.liquidEnthalpy * (1 - x) + value.vaporEnthalpy * x - h) / hScale,
210
+ (value.liquidEntropy * (1 - x) + value.vaporEntropy * x - s) / sScale,
211
+ ],
212
+ jacobian: [
213
+ [
214
+ (value.liquidDhDT * (1 - x) + value.vaporDhDT * x) / hScale,
215
+ deltaH / hScale,
216
+ ],
217
+ [
218
+ (value.liquidDsDT * (1 - x) + value.vaporDsDT * x) / sScale,
219
+ deltaS / sScale,
220
+ ],
221
+ ],
222
+ };
223
+ }, [seed, initialValue.quality], {
224
+ tolerance: 1e-12,
225
+ maxIterations: 30,
226
+ isValid: ([T]) => T >= C.Tt && T <= REGION4_HS_NEWTON_UPPER,
227
+ });
228
+ if (isAdmissibleQuality(quality)) {
229
+ const stableTemperature = Math.round(temperature / C.REGION4_TEMPERATURE_TOLERANCE) * C.REGION4_TEMPERATURE_TOLERANCE;
230
+ const state = finalizeRegion4HSState(h, s, stableTemperature);
231
+ if (state !== null)
232
+ return state;
233
+ }
234
+ }
235
+ catch (error) {
236
+ if (!(error instanceof IF97Error))
237
+ throw error;
238
+ }
239
+ }
240
+ return null;
33
241
  }
34
242
  function finalizeRegion4HSState(h, s, temperature) {
35
243
  const endpoints = saturationEndpointsAtTemperature(temperature);
@@ -43,7 +251,9 @@ function finalizeRegion4HSState(h, s, temperature) {
43
251
  if (Math.abs(normalizedQualityFromEnthalpy - normalizedQualityFromEntropy) > REGION4_QUALITY_TOLERANCE) {
44
252
  return null;
45
253
  }
46
- if (Math.abs(entropyResidual(h, s, temperature)) > C.REGION4_HS_RESIDUAL_TOLERANCE) {
254
+ const mixedEntropy = endpoints.liquid.entropy * (1 - qualityFromEnthalpy) +
255
+ endpoints.vapor.entropy * qualityFromEnthalpy;
256
+ if (Math.abs(mixedEntropy - s) > C.REGION4_HS_RESIDUAL_TOLERANCE) {
47
257
  return null;
48
258
  }
49
259
  try {
@@ -52,114 +262,73 @@ function finalizeRegion4HSState(h, s, temperature) {
52
262
  catch {
53
263
  return null;
54
264
  }
55
- return mixSaturationState(endpoints, normalizedQualityFromEnthalpy);
265
+ const exactEndpointQuality = endpointQuality(qualityFromEnthalpy, qualityFromEntropy);
266
+ return mixSaturationState(endpoints, exactEndpointQuality ?? normalizedQualityFromEnthalpy);
56
267
  }
57
- function endpointStateForPhase(h, s, temperature, phase) {
58
- let endpoints;
59
- try {
60
- endpoints = saturationEndpointsAtTemperature(temperature);
61
- }
62
- catch {
63
- return null;
64
- }
65
- const endpoint = endpoints[phase];
66
- if (!isClose(endpoint.enthalpy, h) || !isClose(endpoint.entropy, s)) {
268
+ function isTriplePointHS(h, s) {
269
+ const { liquid, vapor } = REGION4_TRIPLE_POINT_ENDPOINTS;
270
+ const quality = rawQualityFromSaturationProperty(liquid.entropy, vapor.entropy, s);
271
+ if (quality < 0 || quality > 1)
272
+ return false;
273
+ const expectedH = liquid.enthalpy * (1 - quality) + vapor.enthalpy * quality;
274
+ return Math.abs(expectedH - h) <= C.REGION4_HS_RESIDUAL_TOLERANCE;
275
+ }
276
+ function tryRegion4HSBackwardState(h, s) {
277
+ if (s < REGION4_HS_BACKWARD_MIN_ENTROPY)
67
278
  return null;
68
- }
69
- try {
70
- assertRegion4StateAllowed(endpoints.pressure, endpoints.temperature, 'Region4HS');
71
- }
72
- catch {
279
+ const lower = C.Tt;
280
+ const upper = C.R2_T_MIN;
281
+ const rawSeed = region4BackwardTemperatureHS(h, s);
282
+ if (!Number.isFinite(rawSeed) || rawSeed < lower - 0.1 || rawSeed > upper + 0.1) {
73
283
  return null;
74
284
  }
75
- return mixSaturationState(endpoints, phase === 'liquid' ? 0 : 1);
76
- }
77
- function endpointEnthalpyResidual(h, temperatureGuess, phase) {
78
- let endpoints;
79
- try {
80
- endpoints = saturationEndpointsAtTemperature(clampRegion4TemperatureBelowCritical(temperatureGuess));
81
- }
82
- catch {
83
- return Number.NaN;
84
- }
85
- return endpoints[phase].enthalpy - h;
86
- }
87
- function tryRegion4HSEndpointState(h, s) {
88
- const lower = C.Tt;
89
- const upper = clampRegion4TemperatureBelowCritical(C.Tc);
90
- for (const phase of ['liquid', 'vapor']) {
91
- const lowerState = endpointStateForPhase(h, s, lower, phase);
92
- if (lowerState !== null) {
93
- return lowerState;
94
- }
95
- const upperState = endpointStateForPhase(h, s, upper, phase);
96
- if (upperState !== null) {
97
- return upperState;
98
- }
99
- let previousTemperature = lower;
100
- let previousResidual = endpointEnthalpyResidual(h, lower, phase);
101
- for (let i = 1; i <= C.REGION4_HS_BRACKET_SEGMENTS; i++) {
102
- const temperature = lower + (upper - lower) * (i / C.REGION4_HS_BRACKET_SEGMENTS);
103
- const residual = endpointEnthalpyResidual(h, temperature, phase);
104
- if (!Number.isFinite(previousResidual) || !Number.isFinite(residual)) {
105
- previousTemperature = temperature;
106
- previousResidual = residual;
107
- continue;
108
- }
109
- if (isClose(previousResidual, 0)) {
110
- return endpointStateForPhase(h, s, previousTemperature, phase);
111
- }
112
- if (isClose(residual, 0)) {
113
- return endpointStateForPhase(h, s, temperature, phase);
114
- }
115
- if (previousResidual * residual < 0) {
116
- const root = bracketedNewton((candidateTemperature) => endpointEnthalpyResidual(h, candidateTemperature, phase), previousTemperature, temperature, (previousTemperature + temperature) / 2, { tolerance: C.REGION4_HS_RESIDUAL_TOLERANCE });
117
- return endpointStateForPhase(h, s, root, phase);
118
- }
119
- previousTemperature = temperature;
120
- previousResidual = residual;
285
+ const seed = Math.max(lower, Math.min(upper, rawSeed));
286
+ const seedState = finalizeRegion4HSState(h, s, seed);
287
+ if (seedState !== null)
288
+ return seedState;
289
+ let radius = 0.01;
290
+ for (let attempt = 0; attempt < 8; attempt += 1) {
291
+ const bracketLower = Math.max(lower, seed - radius);
292
+ const bracketUpper = Math.min(upper, seed + radius);
293
+ const lowerResidual = entropyResidual(h, s, bracketLower);
294
+ const upperResidual = entropyResidual(h, s, bracketUpper);
295
+ if (Number.isFinite(lowerResidual) && Number.isFinite(upperResidual) &&
296
+ lowerResidual * upperResidual <= 0) {
297
+ let cachedTemperature = Number.NaN;
298
+ let cachedEvaluation = { residual: Number.NaN, derivative: Number.NaN };
299
+ const evaluate = (candidateTemperature) => {
300
+ if (candidateTemperature !== cachedTemperature) {
301
+ cachedTemperature = candidateTemperature;
302
+ cachedEvaluation = entropyResidualEvaluation(h, s, candidateTemperature);
303
+ }
304
+ return cachedEvaluation;
305
+ };
306
+ const root = bracketedNewton((candidateTemperature) => evaluate(candidateTemperature).residual, bracketLower, bracketUpper, seed, {
307
+ tolerance: C.REGION4_HS_RESIDUAL_TOLERANCE,
308
+ derivative: (candidateTemperature) => evaluate(candidateTemperature).derivative,
309
+ });
310
+ return finalizeRegion4HSState(h, s, root);
121
311
  }
312
+ if (bracketLower === lower && bracketUpper === upper)
313
+ break;
314
+ radius *= 2;
122
315
  }
123
316
  return null;
124
317
  }
125
318
  export function tryRegion4HSState(h, s) {
126
- const endpointState = tryRegion4HSEndpointState(h, s);
127
- if (endpointState !== null) {
128
- return endpointState;
129
- }
130
- const lower = C.Tt;
131
- const upper = clampRegion4TemperatureBelowCritical(C.Tc);
132
- const lowerState = finalizeRegion4HSState(h, s, lower);
133
- if (lowerState !== null) {
134
- return lowerState;
135
- }
136
- const upperState = finalizeRegion4HSState(h, s, upper);
137
- if (upperState !== null) {
138
- return upperState;
139
- }
140
- let previousTemperature = lower;
141
- let previousResidual = entropyResidual(h, s, lower);
142
- for (let i = 1; i <= C.REGION4_HS_BRACKET_SEGMENTS; i++) {
143
- const temperature = lower + (upper - lower) * (i / C.REGION4_HS_BRACKET_SEGMENTS);
144
- const residual = entropyResidual(h, s, temperature);
145
- if (!Number.isFinite(previousResidual) || !Number.isFinite(residual)) {
146
- previousTemperature = temperature;
147
- previousResidual = residual;
148
- continue;
149
- }
150
- if (isClose(previousResidual, 0)) {
151
- return finalizeRegion4HSState(h, s, previousTemperature);
152
- }
153
- if (isClose(residual, 0)) {
154
- return finalizeRegion4HSState(h, s, temperature);
155
- }
156
- if (previousResidual * residual < 0) {
157
- const root = bracketedNewton((candidateTemperature) => entropyResidual(h, s, candidateTemperature), previousTemperature, temperature, (previousTemperature + temperature) / 2, { tolerance: C.REGION4_HS_RESIDUAL_TOLERANCE });
158
- return finalizeRegion4HSState(h, s, root);
159
- }
160
- previousTemperature = temperature;
161
- previousResidual = residual;
319
+ if (isTriplePointHS(h, s)) {
320
+ const triplePointState = finalizeRegion4HSState(h, s, C.Tt);
321
+ if (triplePointState !== null)
322
+ return triplePointState;
162
323
  }
324
+ const backwardState = tryRegion4HSBackwardState(h, s);
325
+ if (backwardState !== null)
326
+ 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;
163
333
  return null;
164
334
  }
165
- //# sourceMappingURL=region4-hs.js.map
@@ -19,4 +19,3 @@ export declare function solvePx(p: number, x: number): BasicProperties;
19
19
  * @param x - Vapour quality [0, 1]
20
20
  */
21
21
  export declare function solveTx(T: number, x: number): BasicProperties;
22
- //# sourceMappingURL=two-phase.d.ts.map
@@ -41,4 +41,3 @@ export function solveTx(T, x) {
41
41
  const p = normalizeRegion4Pressure(saturationPressure(temperature));
42
42
  return solvePx(p, x);
43
43
  }
44
- //# sourceMappingURL=two-phase.js.map
@@ -7,4 +7,3 @@
7
7
  * critical-point rules.
8
8
  */
9
9
  export { saturationPressure, saturationTemperature } from './regions/region4.js';
10
- //# sourceMappingURL=saturation.d.ts.map
@@ -7,4 +7,3 @@
7
7
  * critical-point rules.
8
8
  */
9
9
  export { saturationPressure, saturationTemperature } from './regions/region4.js';
10
- //# sourceMappingURL=saturation.js.map
@@ -6,13 +6,15 @@ export interface BracketedNewtonOptions {
6
6
  maxIterations?: number;
7
7
  /** Relative step size for central-difference derivative (default 1e-6) */
8
8
  derivativeStep?: number;
9
+ /** Optional analytical derivative. */
10
+ derivative?: (x: number) => number;
9
11
  }
10
12
  /**
11
13
  * Find a root of f(x) = 0 within the bracket [lower, upper].
12
14
  *
13
- * Each iteration attempts a Newton step using a central-difference derivative.
14
- * If the Newton candidate falls outside the current bracket or the derivative
15
- * is unavailable, a bisection step is used instead.
15
+ * Each iteration attempts a Newton step using the supplied analytical derivative,
16
+ * or a central difference when none is supplied. If the Newton candidate falls
17
+ * outside the current bracket, a bisection step is used instead.
16
18
  *
17
19
  * @param f - Continuous function whose root is sought
18
20
  * @param lower - Lower bound of the bracket
@@ -23,4 +25,3 @@ export interface BracketedNewtonOptions {
23
25
  * @throws {ConvergenceError} if the bracket signs are the same or max iterations exceeded
24
26
  */
25
27
  export declare function bracketedNewton(f: (x: number) => number, lower: number, upper: number, initialGuess?: number, options?: BracketedNewtonOptions): number;
26
- //# sourceMappingURL=bracketed-newton.d.ts.map
@@ -20,9 +20,9 @@ function numericalDerivative(f, x, lower, upper, step) {
20
20
  /**
21
21
  * Find a root of f(x) = 0 within the bracket [lower, upper].
22
22
  *
23
- * Each iteration attempts a Newton step using a central-difference derivative.
24
- * If the Newton candidate falls outside the current bracket or the derivative
25
- * is unavailable, a bisection step is used instead.
23
+ * Each iteration attempts a Newton step using the supplied analytical derivative,
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.
26
26
  *
27
27
  * @param f - Continuous function whose root is sought
28
28
  * @param lower - Lower bound of the bracket
@@ -69,9 +69,9 @@ export function bracketedNewton(f, lower, upper, initialGuess, options) {
69
69
  return midpoint;
70
70
  }
71
71
  const step = Math.min(derivativeStep * Math.max(1, Math.abs(x)), (b - a) / 4);
72
- const derivative = step > 0
72
+ const derivative = options?.derivative?.(x) ?? (step > 0
73
73
  ? numericalDerivative(f, x, a, b, step)
74
- : null;
74
+ : null);
75
75
  const newtonCandidate = derivative === null || !Number.isFinite(derivative) || derivative === 0
76
76
  ? NaN
77
77
  : x - fx / derivative;
@@ -81,4 +81,3 @@ export function bracketedNewton(f, lower, upper, initialGuess, options) {
81
81
  }
82
82
  throw new ConvergenceError('bracketedNewton', maxIterations);
83
83
  }
84
- //# sourceMappingURL=bracketed-newton.js.map
@@ -0,0 +1,12 @@
1
+ export type Vector2 = readonly [number, number];
2
+ export interface Newton2DEvaluation {
3
+ residual: Vector2;
4
+ jacobian: readonly [Vector2, Vector2];
5
+ }
6
+ export interface DampedNewton2DOptions {
7
+ tolerance?: number;
8
+ maxIterations?: number;
9
+ maxBacktracks?: number;
10
+ isValid?: (point: Vector2) => boolean;
11
+ }
12
+ export declare function dampedNewton2D(evaluate: (point: Vector2) => Newton2DEvaluation, initial: Vector2, options?: DampedNewton2DOptions): [number, number];