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.
- package/CHANGELOG.md +25 -2
- package/README.md +69 -246
- package/dist/backward/candidate-resolution.d.ts +8 -0
- package/dist/backward/candidate-resolution.js +40 -0
- package/dist/backward/fixed-temperature-candidates.d.ts +4 -0
- package/dist/backward/fixed-temperature-candidates.js +188 -0
- package/dist/backward/fixed-temperature-solver.d.ts +3 -3
- package/dist/backward/fixed-temperature-solver.js +38 -17
- package/dist/backward/fixed-temperature-stationary.d.ts +4 -0
- package/dist/backward/fixed-temperature-stationary.js +146 -0
- package/dist/backward/hs-region.d.ts +50 -0
- package/dist/backward/hs-region.js +695 -0
- package/dist/backward/hs-state.d.ts +15 -0
- package/dist/backward/hs-state.js +316 -0
- package/dist/backward/hs.js +28 -242
- package/dist/backward/ph.d.ts +2 -0
- package/dist/backward/ph.js +72 -21
- package/dist/backward/pressure-property-region.d.ts +13 -0
- package/dist/backward/pressure-property-region.js +262 -0
- package/dist/backward/ps.d.ts +6 -0
- package/dist/backward/ps.js +81 -28
- package/dist/backward/solution-validation.d.ts +7 -1
- package/dist/backward/solution-validation.js +138 -41
- package/dist/backward/th.d.ts +3 -5
- package/dist/backward/th.js +10 -71
- package/dist/backward/tolerances.d.ts +3 -0
- package/dist/backward/tolerances.js +9 -3
- package/dist/backward/ts.d.ts +3 -5
- package/dist/backward/ts.js +10 -71
- package/dist/constants.d.ts +11 -15
- package/dist/constants.js +14 -15
- package/dist/core/floating-point.d.ts +2 -0
- package/dist/core/floating-point.js +7 -0
- package/dist/core/formulation-seams.d.ts +27 -0
- package/dist/core/formulation-seams.js +289 -0
- package/dist/core/input-validation.d.ts +1 -0
- package/dist/core/input-validation.js +5 -0
- package/dist/core/region-detector.d.ts +3 -40
- package/dist/core/region-detector.js +37 -762
- package/dist/core/region3-pt.d.ts +1 -1
- package/dist/core/region3-pt.js +42 -16
- package/dist/core/solve-input-normalization.js +4 -1
- package/dist/core/solver.d.ts +7 -10
- package/dist/core/solver.js +26 -17
- package/dist/detect.d.ts +16 -2
- package/dist/detect.js +36 -2
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +16 -0
- package/dist/gas-constant.d.ts +5 -0
- package/dist/gas-constant.js +5 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +10 -21
- package/dist/regions/region1.d.ts +2 -1
- package/dist/regions/region1.js +1 -1
- package/dist/regions/region2.d.ts +2 -1
- package/dist/regions/region2.js +1 -1
- package/dist/regions/region3-data.js +3 -3
- package/dist/regions/region3-subregions.js +15 -10
- package/dist/regions/region3.d.ts +4 -1
- package/dist/regions/region3.js +32 -2
- package/dist/regions/region4.d.ts +2 -0
- package/dist/regions/region4.js +39 -12
- package/dist/regions/region5.d.ts +2 -1
- package/dist/regions/region5.js +2 -2
- package/dist/saturation/common.d.ts +2 -8
- package/dist/saturation/common.js +64 -36
- package/dist/saturation/region4-boundaries.js +13 -13
- package/dist/saturation/region4-hs.js +282 -58
- package/dist/saturation/two-phase.d.ts +2 -2
- package/dist/saturation/two-phase.js +7 -7
- package/dist/solvers/bracketed-newton.d.ts +6 -3
- package/dist/solvers/bracketed-newton.js +15 -8
- package/dist/solvers/extended-arithmetic.d.ts +6 -0
- package/dist/solvers/extended-arithmetic.js +38 -0
- package/dist/transport/properties.d.ts +4 -2
- package/dist/transport/properties.js +14 -4
- package/dist/types.d.ts +18 -0
- package/dist/types.js +14 -0
- package/package.json +4 -4
- package/dist/core/public-normalization.d.ts +0 -3
- package/dist/core/public-normalization.js +0 -53
- /package/dist/{backward → core}/thermodynamic-derivatives.d.ts +0 -0
- /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
|
|
12
|
-
*
|
|
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 =
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
160
|
+
function tryRegion4HSEntropyState(h, s) {
|
|
137
161
|
let cachedTemperature = Number.NaN;
|
|
138
162
|
let cachedEvaluation = null;
|
|
139
163
|
const evaluate = (temperature) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
182
|
+
if (!(candidate >= C.Tt && candidate <= C.R2_T_MIN))
|
|
160
183
|
break;
|
|
161
|
-
temperature =
|
|
184
|
+
temperature = candidate;
|
|
162
185
|
}
|
|
163
186
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
248
|
-
if (!isAdmissibleQuality(normalizedQualityFromEnthalpy) || !isAdmissibleQuality(normalizedQualityFromEntropy)) {
|
|
374
|
+
if (!isAdmissibleQuality(qualityFromEnthalpy) || !isAdmissibleQuality(qualityFromEntropy)) {
|
|
249
375
|
return null;
|
|
250
376
|
}
|
|
251
|
-
if (Math.abs(
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
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],
|
|
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 {
|
|
12
|
-
import {
|
|
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
|
|
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],
|
|
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
|
|
42
|
-
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
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
|
|
7
|
-
*
|
|
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,
|
|
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
|
|
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) <=
|
|
45
|
+
if (Math.abs(fa) <= residualTolerance)
|
|
44
46
|
return a;
|
|
45
|
-
if (Math.abs(fb) <=
|
|
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) <=
|
|
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
|
-
|
|
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
|
|
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
|
/**
|