iapws-if97 2.1.5 → 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 +32 -1
- package/README.md +78 -215
- 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 +6 -6
- package/dist/backward/fixed-temperature-solver.js +56 -9
- 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.d.ts +1 -2
- package/dist/backward/hs.js +28 -232
- package/dist/backward/ph.d.ts +8 -3
- package/dist/backward/ph.js +102 -54
- 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 -1
- package/dist/backward/ps.js +103 -38
- package/dist/backward/solution-validation.d.ts +7 -2
- package/dist/backward/solution-validation.js +162 -37
- package/dist/backward/th.d.ts +3 -6
- package/dist/backward/th.js +10 -73
- package/dist/backward/tolerances.d.ts +3 -1
- package/dist/backward/tolerances.js +9 -4
- package/dist/backward/ts.d.ts +3 -6
- package/dist/backward/ts.js +10 -73
- package/dist/boundaries.d.ts +0 -1
- package/dist/boundaries.js +0 -1
- package/dist/constants.d.ts +11 -24
- package/dist/constants.js +17 -28
- 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 -1
- package/dist/core/input-validation.js +5 -1
- package/dist/core/region-detector.d.ts +3 -41
- package/dist/core/region-detector.js +39 -597
- package/dist/core/region3-pt.d.ts +12 -1
- package/dist/core/region3-pt.js +68 -13
- package/dist/core/solve-input-normalization.d.ts +0 -1
- package/dist/core/solve-input-normalization.js +4 -2
- package/dist/core/solver.d.ts +7 -11
- package/dist/core/solver.js +26 -18
- package/dist/core/thermodynamic-derivatives.d.ts +17 -0
- package/dist/core/thermodynamic-derivatives.js +27 -0
- package/dist/detect.d.ts +16 -3
- package/dist/detect.js +36 -3
- 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 -6
- package/dist/index.js +10 -22
- package/dist/regions/boundaries.d.ts +0 -1
- package/dist/regions/boundaries.js +0 -1
- package/dist/regions/region1.d.ts +2 -2
- package/dist/regions/region1.js +1 -2
- package/dist/regions/region2.d.ts +2 -2
- package/dist/regions/region2.js +1 -2
- package/dist/regions/region3-data.d.ts +0 -1
- package/dist/regions/region3-data.js +3 -4
- package/dist/regions/region3-eval.d.ts +0 -1
- package/dist/regions/region3-eval.js +0 -1
- package/dist/regions/region3-subregions.d.ts +0 -1
- package/dist/regions/region3-subregions.js +15 -11
- package/dist/regions/region3.d.ts +4 -2
- package/dist/regions/region3.js +32 -3
- package/dist/regions/region4.d.ts +2 -1
- package/dist/regions/region4.js +39 -13
- package/dist/regions/region5.d.ts +2 -2
- package/dist/regions/region5.js +2 -3
- package/dist/regions.d.ts +0 -1
- package/dist/regions.js +0 -1
- package/dist/saturation/common.d.ts +2 -9
- package/dist/saturation/common.js +66 -45
- package/dist/saturation/region4-boundaries.d.ts +0 -5
- package/dist/saturation/region4-boundaries.js +17 -18
- package/dist/saturation/region4-hs.d.ts +2 -1
- package/dist/saturation/region4-hs.js +499 -106
- package/dist/saturation/two-phase.d.ts +2 -3
- package/dist/saturation/two-phase.js +7 -8
- package/dist/saturation.d.ts +0 -1
- package/dist/saturation.js +0 -1
- package/dist/solvers/bracketed-newton.d.ts +10 -6
- package/dist/solvers/bracketed-newton.js +19 -13
- package/dist/solvers/damped-newton-2d.d.ts +12 -0
- package/dist/solvers/damped-newton-2d.js +68 -0
- package/dist/solvers/extended-arithmetic.d.ts +6 -0
- package/dist/solvers/extended-arithmetic.js +38 -0
- package/dist/solvers/newton-raphson.d.ts +0 -1
- package/dist/solvers/newton-raphson.js +0 -1
- package/dist/transport/properties.d.ts +4 -3
- package/dist/transport/properties.js +14 -5
- package/dist/transport.d.ts +0 -1
- package/dist/transport.js +0 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.js +14 -1
- package/package.json +18 -10
- package/dist/backward/objective-normalization.d.ts +0 -7
- package/dist/backward/objective-normalization.js +0 -8
- package/dist/core/public-normalization.d.ts +0 -4
- package/dist/core/public-normalization.js +0 -54
- package/dist/solvers/nelder-mead.d.ts +0 -17
- package/dist/solvers/nelder-mead.js +0 -116
package/dist/backward/ph.js
CHANGED
|
@@ -7,19 +7,20 @@
|
|
|
7
7
|
* Reference: IAPWS-IF97 Supplementary Release on Backward Equations
|
|
8
8
|
*/
|
|
9
9
|
import { Region, IF97Error, OutOfRangeError } from '../types.js';
|
|
10
|
+
import { SaturationEndpointError } from '../errors.js';
|
|
10
11
|
import * as C from '../constants.js';
|
|
11
12
|
import { region1 } from '../regions/region1.js';
|
|
12
13
|
import { region2 } from '../regions/region2.js';
|
|
13
|
-
import { region3ByRhoT } from '../regions/region3.js';
|
|
14
|
+
import { region3ByRhoT, region3PressureResidual } from '../regions/region3.js';
|
|
14
15
|
import { region5 } from '../regions/region5.js';
|
|
15
16
|
import { saturationTemperature } from '../regions/region4.js';
|
|
16
|
-
import { assertFiniteNumber } from '../core/input-validation.js';
|
|
17
|
-
import { detectRegionPH } from '
|
|
17
|
+
import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
|
|
18
|
+
import { assertPressurePropertyEnvelope, detectRegionPH, pressurePropertyBoundaryState, pressurePropertySeam } from './pressure-property-region.js';
|
|
18
19
|
import { newtonRaphson } from '../solvers/newton-raphson.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
20
|
+
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
21
|
+
import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
|
|
22
|
+
import { validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
|
|
23
|
+
import { helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
|
|
23
24
|
import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtPressure, } from '../saturation/common.js';
|
|
24
25
|
import { assertCriticalRegion4PHInput, assertRegion4StateAllowed, } from '../saturation/region4-boundaries.js';
|
|
25
26
|
// ─── Backward Polynomial Evaluator ─────────────────────────────────────────
|
|
@@ -39,7 +40,7 @@ const R1_PH = [
|
|
|
39
40
|
[3, 10, 6.6456186191635e-8], [3, 32, 8.0670734103027e-11], [4, 32, -9.3477771213947e-13],
|
|
40
41
|
[5, 32, 5.8265442020601e-15], [6, 32, -1.5020185953503e-17],
|
|
41
42
|
];
|
|
42
|
-
function r1BackwardT(p, h) {
|
|
43
|
+
export function r1BackwardT(p, h) {
|
|
43
44
|
return evalPoly(R1_PH, 0, -1, p, h / 2500);
|
|
44
45
|
}
|
|
45
46
|
// ─── Region 2 Backward T(P,H) ─────────────────────────────────────────────
|
|
@@ -47,9 +48,10 @@ function r1BackwardT(p, h) {
|
|
|
47
48
|
// For 4 < p <= 6.546699678 MPa the supplementary release selects subregion 2b
|
|
48
49
|
// directly; above this threshold use the B2bc(h) boundary to discriminate 2b/2c.
|
|
49
50
|
const R2_PH_B2BC_P_MIN = 6.546699678;
|
|
50
|
-
|
|
51
|
+
/** B2bc boundary: h → p, discriminating subregions 2b and 2c.
|
|
52
|
+
* Ref: IAPWS-IF97, Eq. 20. Check point: h = 3516.004323 → p = 100 MPa. */
|
|
51
53
|
export function b2bc_H_P(h) {
|
|
52
|
-
return 0.
|
|
54
|
+
return 0.90584278514723e3 - 0.67955786399241 * h + 0.12809002730136e-3 * h * h;
|
|
53
55
|
}
|
|
54
56
|
// Subregion 2a — Eq. 22
|
|
55
57
|
const R2A_PH = [
|
|
@@ -60,7 +62,7 @@ const R2A_PH = [
|
|
|
60
62
|
[2, 2, -0.31078046629583], [2, 7, 11.670873077107], [2, 36, 128127984.04046], [2, 38, -985549096.23276],
|
|
61
63
|
[2, 40, 2822454697.3002], [2, 42, -3594897141.0703], [2, 44, 1722734991.3197], [3, 24, -13551.334240775],
|
|
62
64
|
[3, 44, 12848734.66465], [4, 12, 1.3865724283226], [4, 32, 235988.32556514], [4, 44, -13105236.545054],
|
|
63
|
-
[5, 32, 7399.9835474766], [5, 36, -551966.9703006], [5, 42, 3715408.5996233], [6, 34, 19127.
|
|
65
|
+
[5, 32, 7399.9835474766], [5, 36, -551966.9703006], [5, 42, 3715408.5996233], [6, 34, 19127.729239660],
|
|
64
66
|
[6, 44, -415351.64835634], [7, 28, -62.459855192507],
|
|
65
67
|
];
|
|
66
68
|
function r2aBackwardT(p, h) {
|
|
@@ -96,7 +98,7 @@ const R2C_PH = [
|
|
|
96
98
|
function r2cBackwardT(p, h) {
|
|
97
99
|
return evalPoly(R2C_PH, -25, 1.8, p, h / 2000);
|
|
98
100
|
}
|
|
99
|
-
function r2BackwardT(p, h) {
|
|
101
|
+
export function r2BackwardT(p, h) {
|
|
100
102
|
let T;
|
|
101
103
|
if (p <= C.R2_P_CRT) {
|
|
102
104
|
T = r2aBackwardT(p, h);
|
|
@@ -173,22 +175,39 @@ const R3B_PH_V = [
|
|
|
173
175
|
[-1, 5, -11.8035753702231], [0, 0, 0.169490044091791], [1, 0, -0.0179967222507787],
|
|
174
176
|
[1, 1, 0.0371810116332674], [2, 2, -0.0536288335065096], [2, 6, 1.6069710109252],
|
|
175
177
|
];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
[20, 16, -3.33775713645296e22], [22, 3, 3.56499469636328e10], [24, 18, -1.48547544720641e26],
|
|
182
|
-
[28, 8, 3.30611514838798e18], [36, 24, 8.13641294467829e37],
|
|
183
|
-
];
|
|
184
|
-
/** R4: enthalpy → saturation pressure for PH boundary detection */
|
|
185
|
-
export function r4EnthalpyToPsat(h) {
|
|
186
|
-
const mu = h / 2600;
|
|
187
|
-
let p = 0;
|
|
188
|
-
for (const [I, J, N] of R4_H_PSAT) {
|
|
189
|
-
p += N * Math.pow(mu - 1.02, I) * Math.pow(mu - 0.608, J);
|
|
178
|
+
export function r3BackwardPHInitial(p, h) {
|
|
179
|
+
if (h < b3ab_P_to_H(p)) {
|
|
180
|
+
const T = 760 * evalPoly(R3A_PH_T, -0.240, 0.615, p / 100, h / 2300);
|
|
181
|
+
const v = 0.0028 * evalPoly(R3A_PH_V, -0.128, 0.727, p / 100, h / 2100);
|
|
182
|
+
return { rho: 1 / v, T };
|
|
190
183
|
}
|
|
191
|
-
|
|
184
|
+
const T = 860 * evalPoly(R3B_PH_T, -0.298, 0.720, p / 100, h / 2800);
|
|
185
|
+
const v = 0.0088 * evalPoly(R3B_PH_V, -0.0661, 0.720, p / 100, h / 2800);
|
|
186
|
+
return { rho: 1 / v, T };
|
|
187
|
+
}
|
|
188
|
+
function solveRegion3PH(p, h, rho0, T0, refinePressure = false) {
|
|
189
|
+
// Scaling the 1e-13 solver tolerance by 1e-4 resolves pressure below one ULP before PT reconstruction.
|
|
190
|
+
const pScale = Math.max(1, Math.abs(p)) * (refinePressure ? 1e-4 : 1);
|
|
191
|
+
const hScale = Math.max(1, Math.abs(h));
|
|
192
|
+
const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
|
|
193
|
+
const state = region3ByRhoT(candidateRho, candidateT);
|
|
194
|
+
const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
|
|
195
|
+
return {
|
|
196
|
+
residual: [
|
|
197
|
+
(refinePressure ? region3PressureResidual(candidateRho, candidateT, p) : state.pressure - p) / pScale,
|
|
198
|
+
(state.enthalpy - h) / hScale,
|
|
199
|
+
],
|
|
200
|
+
jacobian: [
|
|
201
|
+
[derivatives.dpDrho / pScale, derivatives.dpDT / pScale],
|
|
202
|
+
[derivatives.dhDrho / hScale, derivatives.dhDT / hScale],
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}, [rho0, T0], {
|
|
206
|
+
// Near critical pressure, the default residual can cross a stability boundary.
|
|
207
|
+
tolerance: 1e-13,
|
|
208
|
+
isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
|
|
209
|
+
});
|
|
210
|
+
return region3ByRhoT(rho, T);
|
|
192
211
|
}
|
|
193
212
|
// ─── Main PH Solver ────────────────────────────────────────────────────────
|
|
194
213
|
/**
|
|
@@ -202,52 +221,81 @@ export function solvePH(p, h) {
|
|
|
202
221
|
if (p < C.P_MIN || p > C.P_MAX) {
|
|
203
222
|
throw new OutOfRangeError('Pressure', p, C.P_MIN, C.P_MAX);
|
|
204
223
|
}
|
|
205
|
-
if (h
|
|
224
|
+
if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
|
|
206
225
|
throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
|
|
207
226
|
}
|
|
208
227
|
assertCriticalRegion4PHInput(p, h, 'solvePH');
|
|
209
|
-
|
|
228
|
+
assertPressurePropertyEnvelope(p, h, 'enthalpy');
|
|
229
|
+
const seam = pressurePropertySeam(p, h, 'enthalpy');
|
|
230
|
+
if (seam !== null && seam.kind !== 'overlap') {
|
|
231
|
+
return validateBackwardState(seam.state, [
|
|
232
|
+
{ label: 'pressure', expected: p },
|
|
233
|
+
{ label: 'enthalpy', expected: h },
|
|
234
|
+
], { solverName: 'solvePH', expectedRegion: seam.state.region, seam });
|
|
235
|
+
}
|
|
236
|
+
if (seam === null) {
|
|
237
|
+
const boundary = pressurePropertyBoundaryState(p, h, 'enthalpy');
|
|
238
|
+
if (boundary !== null)
|
|
239
|
+
return boundary;
|
|
240
|
+
}
|
|
241
|
+
let region;
|
|
242
|
+
let saturationFailure = null;
|
|
243
|
+
try {
|
|
244
|
+
region = seam?.state.region ?? detectRegionPH(p, h);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (!(error instanceof SaturationEndpointError)) {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
saturationFailure = error;
|
|
251
|
+
region = Region.Region3;
|
|
252
|
+
}
|
|
210
253
|
switch (region) {
|
|
211
254
|
case Region.Region1: {
|
|
212
255
|
const T0 = r1BackwardT(p, h);
|
|
213
|
-
const T = newtonRaphson((T_x) => region1(p, T_x).enthalpy - h, T0);
|
|
256
|
+
const T = newtonRaphson((T_x) => region1(p, T_x).enthalpy - h, T0, (T_x) => region1(p, T_x).cp ?? Number.NaN);
|
|
214
257
|
return validateBackwardState(region1(p, T), [
|
|
215
258
|
{ label: 'pressure', expected: p },
|
|
216
259
|
{ label: 'enthalpy', expected: h },
|
|
217
260
|
], { solverName: 'solvePH', expectedRegion: Region.Region1 });
|
|
218
261
|
}
|
|
219
262
|
case Region.Region2: {
|
|
263
|
+
const targetH = Math.min(h, region2(p, C.R2_T_MAX).enthalpy);
|
|
220
264
|
const T0 = r2BackwardT(p, h);
|
|
221
|
-
const T = newtonRaphson((T_x) => region2(p, T_x).enthalpy -
|
|
265
|
+
const T = newtonRaphson((T_x) => region2(p, T_x).enthalpy - targetH, T0, (T_x) => region2(p, T_x).cp ?? Number.NaN);
|
|
222
266
|
return validateBackwardState(region2(p, T), [
|
|
223
267
|
{ label: 'pressure', expected: p },
|
|
224
268
|
{ label: 'enthalpy', expected: h },
|
|
225
269
|
], { solverName: 'solvePH', expectedRegion: Region.Region2 });
|
|
226
270
|
}
|
|
227
271
|
case Region.Region3: {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
272
|
+
try {
|
|
273
|
+
const initial = r3BackwardPHInitial(p, h);
|
|
274
|
+
const validate = (state) => validateBackwardState(state, [
|
|
275
|
+
{ label: 'pressure', expected: p },
|
|
276
|
+
{ label: 'enthalpy', expected: h },
|
|
277
|
+
], { solverName: 'solvePH', expectedRegion: Region.Region3 });
|
|
278
|
+
const validated = validate(solveRegion3PH(p, h, initial.rho, initial.T));
|
|
279
|
+
// Compressibility can amplify an accepted pressure residual beyond PT's density accuracy.
|
|
280
|
+
const relativeDensityError = Math.abs(region3PressureResidual(1 / validated.specificVolume, validated.temperature, p)) * validated.isothermalCompressibility;
|
|
281
|
+
if (saturationFailure === null && relativeDensityError <= 1e-12)
|
|
282
|
+
return validated;
|
|
283
|
+
try {
|
|
284
|
+
const refined = validate(solveRegion3PH(p, h, 1 / validated.specificVolume, validated.temperature, true));
|
|
285
|
+
validateRegion3PTReconstruction(refined, 'solvePH');
|
|
286
|
+
return refined;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (!(error instanceof IF97Error))
|
|
290
|
+
throw error;
|
|
291
|
+
return saturationFailure === null ? validated : validateRegion3PTReconstruction(validated, 'solvePH');
|
|
292
|
+
}
|
|
235
293
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (saturationFailure !== null && error instanceof IF97Error)
|
|
296
|
+
throw saturationFailure;
|
|
297
|
+
throw error;
|
|
239
298
|
}
|
|
240
|
-
const sol = nelderMead((pair) => {
|
|
241
|
-
const state = region3ByRhoT(1 / pair[0], pair[1]);
|
|
242
|
-
return sumNormalizedResiduals([
|
|
243
|
-
{ actual: state.enthalpy, expected: h, tolerance: enthalpyTolerance },
|
|
244
|
-
{ actual: state.pressure, expected: p, tolerance: pressureTolerance },
|
|
245
|
-
]);
|
|
246
|
-
}, [v0, T0], { maxIterations: 1000, minErrorDelta: 1e-8, minTolerance: 1e-9 });
|
|
247
|
-
return validateBackwardState(region3ByRhoT(1 / sol.x[0], sol.x[1]), [
|
|
248
|
-
{ label: 'pressure', expected: p, tolerance: pressureTolerance },
|
|
249
|
-
{ label: 'enthalpy', expected: h },
|
|
250
|
-
], { solverName: 'solvePH', expectedRegion: Region.Region3 });
|
|
251
299
|
}
|
|
252
300
|
case Region.Region4: {
|
|
253
301
|
const endpoints = saturationEndpointsAtPressure(p);
|
|
@@ -259,7 +307,8 @@ export function solvePH(p, h) {
|
|
|
259
307
|
], { solverName: 'solvePH', expectedRegion: Region.Region4 });
|
|
260
308
|
}
|
|
261
309
|
case Region.Region5: {
|
|
262
|
-
const
|
|
310
|
+
const targetH = Math.min(h, region5(p, C.R5_T_MAX).enthalpy);
|
|
311
|
+
const T = bracketedNewton((T_x) => region5(p, T_x).enthalpy - targetH, C.R5_T_MIN + Number.EPSILON * C.R5_T_MIN, C.R5_T_MAX, 1500, { derivative: (T_x) => region5(p, T_x).cp ?? Number.NaN, tolerance: 1e-12 });
|
|
263
312
|
return validateBackwardState(region5(p, T), [
|
|
264
313
|
{ label: 'pressure', expected: p },
|
|
265
314
|
{ label: 'enthalpy', expected: h },
|
|
@@ -269,4 +318,3 @@ export function solvePH(p, h) {
|
|
|
269
318
|
throw new IF97Error(`Cannot determine region for P=${p} MPa, h=${h} kJ/kg`);
|
|
270
319
|
}
|
|
271
320
|
}
|
|
272
|
-
//# sourceMappingURL=ph.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Region } from '../types.js';
|
|
2
|
+
import type { BasicProperties } from '../types.js';
|
|
3
|
+
import { type FormulationSeamResolution } from '../core/formulation-seams.js';
|
|
4
|
+
/** Resolve the native saturation/B23 intersection before choosing one inverse region. */
|
|
5
|
+
export declare function pressurePropertyBoundaryState(p: number, target: number, property: 'enthalpy' | 'entropy'): BasicProperties | null;
|
|
6
|
+
/** Reject proven fixed-pressure envelope violations without classifying interior gaps. */
|
|
7
|
+
export declare function assertPressurePropertyEnvelope(p: number, target: number, property: 'enthalpy' | 'entropy'): void;
|
|
8
|
+
/** Share the fixed-pressure seam policy between detectors and backward solvers. */
|
|
9
|
+
export declare function pressurePropertySeam(p: number, target: number, property: 'enthalpy' | 'entropy'): FormulationSeamResolution | null;
|
|
10
|
+
/** Detect region for P-H inputs. */
|
|
11
|
+
export declare function detectRegionPH(p: number, h: number): Region | -1;
|
|
12
|
+
/** Detect region for P-S inputs. */
|
|
13
|
+
export declare function detectRegionPS(p: number, s: number): Region | -1;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as C from '../constants.js';
|
|
2
|
+
import { SaturationEndpointError } from '../errors.js';
|
|
3
|
+
import { region1 } from '../regions/region1.js';
|
|
4
|
+
import { region2 } from '../regions/region2.js';
|
|
5
|
+
import { region5 } from '../regions/region5.js';
|
|
6
|
+
import { boundary23_T_to_P } from '../regions/boundaries.js';
|
|
7
|
+
import { saturationPressure } from '../regions/region4.js';
|
|
8
|
+
import { saturationEndpointsAtPressure } from '../saturation/common.js';
|
|
9
|
+
import { OutOfRangeError, Region } from '../types.js';
|
|
10
|
+
import { detectRegionPT, nativeB23TemperatureBracket, nativeSaturationTemperatureBracket } from '../core/region-detector.js';
|
|
11
|
+
import { withinClosedLowerBound } from '../core/input-validation.js';
|
|
12
|
+
import { findPressureSeam } from '../core/formulation-seams.js';
|
|
13
|
+
import { nextPositiveFloat } from '../core/floating-point.js';
|
|
14
|
+
import { solveRegion3PTBasic } from '../core/region3-pt.js';
|
|
15
|
+
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
16
|
+
import { resolveBackwardCandidates } from './candidate-resolution.js';
|
|
17
|
+
import { BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE } from './tolerances.js';
|
|
18
|
+
const P13 = saturationPressure(C.R2_T_MIN);
|
|
19
|
+
const B23_P_AT_T13 = boundary23_T_to_P(C.R2_T_MIN);
|
|
20
|
+
/** Resolve the native saturation/B23 intersection before choosing one inverse region. */
|
|
21
|
+
export function pressurePropertyBoundaryState(p, target, property) {
|
|
22
|
+
if (p < C.B23_P_MIN || p > B23_P_AT_T13)
|
|
23
|
+
return null;
|
|
24
|
+
const references = [region1(p, C.R2_T_MIN), region2(p, C.R2_T_MIN)];
|
|
25
|
+
if (!references.some((state) => Math.abs(state[property] - target) <=
|
|
26
|
+
(property === 'enthalpy' ? 0.2 : 0.0002)))
|
|
27
|
+
return null;
|
|
28
|
+
const endpoints = saturationEndpointsAtPressure(p);
|
|
29
|
+
if (endpoints.liquid[property] <= target && target <= endpoints.vapor[property])
|
|
30
|
+
return null;
|
|
31
|
+
const tolerance = 128 * Number.EPSILON * Math.max(1, Math.abs(target));
|
|
32
|
+
const temperatures = new Set([
|
|
33
|
+
C.R2_T_MIN,
|
|
34
|
+
nextPositiveFloat(C.R2_T_MIN, -1),
|
|
35
|
+
nextPositiveFloat(C.R2_T_MIN, 1),
|
|
36
|
+
...nativeSaturationTemperatureBracket(p),
|
|
37
|
+
]);
|
|
38
|
+
const stateAt = (T) => {
|
|
39
|
+
const region = detectRegionPT(p, T);
|
|
40
|
+
return region === Region.Region1 ? region1(p, T)
|
|
41
|
+
: region === Region.Region2 ? region2(p, T)
|
|
42
|
+
: region === Region.Region3 ? solveRegion3PTBasic(p, T) : null;
|
|
43
|
+
};
|
|
44
|
+
return resolveBackwardCandidates([...temperatures].map((T) => () => {
|
|
45
|
+
const state = stateAt(T);
|
|
46
|
+
if (state === null)
|
|
47
|
+
return null;
|
|
48
|
+
if (Math.abs(state[property] - target) <= tolerance)
|
|
49
|
+
return state;
|
|
50
|
+
const slope = property === 'enthalpy' ? state.cp : state.cp / T;
|
|
51
|
+
const temperature = T + (target - state[property]) / slope;
|
|
52
|
+
if (detectRegionPT(p, temperature) !== state.region)
|
|
53
|
+
return null;
|
|
54
|
+
const corrected = stateAt(temperature);
|
|
55
|
+
return corrected !== null && Math.abs(corrected[property] - target) <= tolerance ? corrected : null;
|
|
56
|
+
}), [
|
|
57
|
+
{ label: 'pressure', expected: p, tolerance: 0 },
|
|
58
|
+
{ label: property, expected: target, tolerance },
|
|
59
|
+
], { solverName: property === 'enthalpy' ? 'solvePH' : 'solvePS' });
|
|
60
|
+
}
|
|
61
|
+
/** Reject proven fixed-pressure envelope violations without classifying interior gaps. */
|
|
62
|
+
export function assertPressurePropertyEnvelope(p, target, property) {
|
|
63
|
+
const minimum = region1(p, C.T_MIN)[property];
|
|
64
|
+
const maximum = (p <= C.R5_P_MAX ? region5(p, C.R5_T_MAX) : region2(p, C.R2_T_MAX))[property];
|
|
65
|
+
const aboveMinimum = property === 'enthalpy'
|
|
66
|
+
? target >= minimum || isClose(target, minimum)
|
|
67
|
+
: withinClosedLowerBound(target, minimum, 'entropy');
|
|
68
|
+
if (!aboveMinimum || !withinClosedUpperBound(target, maximum)) {
|
|
69
|
+
throw new OutOfRangeError(`${property === 'enthalpy' ? 'Enthalpy' : 'Entropy'} at p=${p} MPa`, target, minimum, maximum);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Share the fixed-pressure seam policy between detectors and backward solvers. */
|
|
73
|
+
export function pressurePropertySeam(p, target, property) {
|
|
74
|
+
const match = findPressureSeam(p, target, property);
|
|
75
|
+
if (match === null)
|
|
76
|
+
return null;
|
|
77
|
+
if (p >= C.Pt && p < C.Pc) {
|
|
78
|
+
try {
|
|
79
|
+
const endpoints = saturationEndpointsAtPressure(p);
|
|
80
|
+
if (target >= endpoints.liquid[property] && target <= endpoints.vapor[property])
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (!(error instanceof SaturationEndpointError))
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const solverName = property === 'enthalpy' ? 'solvePH' : 'solvePS';
|
|
89
|
+
const constraints = [{ label: 'pressure', expected: p }, { label: property, expected: target }];
|
|
90
|
+
const state = resolveBackwardCandidates([
|
|
91
|
+
() => match.state[property] === target ? match.state : null,
|
|
92
|
+
() => pressureSeamRoot(p, target, property, match, match.pair.lower.region),
|
|
93
|
+
() => pressureSeamRoot(p, target, property, match, match.pair.upper.region),
|
|
94
|
+
], constraints, { solverName, seam: match });
|
|
95
|
+
const resolved = state ?? (match.kind === 'canonical'
|
|
96
|
+
? resolveBackwardCandidates([() => match.state], constraints, { solverName }) : null);
|
|
97
|
+
return resolved === null ? null : resolved === match.state ? match : { ...match, kind: 'canonical', state: resolved };
|
|
98
|
+
}
|
|
99
|
+
function pressureSeamRoot(p, target, property, seam, region) {
|
|
100
|
+
let lower;
|
|
101
|
+
let upper;
|
|
102
|
+
let evaluate;
|
|
103
|
+
if (region === Region.Region1) {
|
|
104
|
+
lower = C.T_MIN;
|
|
105
|
+
upper = C.R2_T_MIN;
|
|
106
|
+
evaluate = (T) => region1(p, T);
|
|
107
|
+
}
|
|
108
|
+
else if (region === Region.Region2) {
|
|
109
|
+
lower = p > C.B23_P_MIN ? nativeB23TemperatureBracket(p)[1] : C.T_MIN;
|
|
110
|
+
if (p < C.Pc)
|
|
111
|
+
lower = Math.max(lower, nativeSaturationTemperatureBracket(p)[1]);
|
|
112
|
+
upper = C.R2_T_MAX;
|
|
113
|
+
evaluate = (T) => region2(p, T);
|
|
114
|
+
}
|
|
115
|
+
else if (region === Region.Region3) {
|
|
116
|
+
lower = nextPositiveFloat(C.R2_T_MIN, 1);
|
|
117
|
+
upper = nativeB23TemperatureBracket(p)[0];
|
|
118
|
+
if (p < C.Pc) {
|
|
119
|
+
const [liquidT, vaporT] = nativeSaturationTemperatureBracket(p);
|
|
120
|
+
if (seam.pair.seam === '13')
|
|
121
|
+
upper = Math.min(upper, liquidT);
|
|
122
|
+
else
|
|
123
|
+
lower = Math.max(lower, vaporT);
|
|
124
|
+
}
|
|
125
|
+
evaluate = (T) => solveRegion3PTBasic(p, T);
|
|
126
|
+
}
|
|
127
|
+
else if (region === Region.Region5) {
|
|
128
|
+
lower = nextPositiveFloat(C.R5_T_MIN, 1);
|
|
129
|
+
upper = C.R5_T_MAX;
|
|
130
|
+
evaluate = (T) => region5(p, T);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (lower > upper)
|
|
136
|
+
return null;
|
|
137
|
+
const direction = region === seam.pair.lower.region ? -1 : 1;
|
|
138
|
+
const boundary = direction < 0 ? upper : lower;
|
|
139
|
+
const first = evaluate(boundary);
|
|
140
|
+
if (direction * (target - first[property]) < 0)
|
|
141
|
+
return null;
|
|
142
|
+
if (target === first[property])
|
|
143
|
+
return first;
|
|
144
|
+
if (lower === upper)
|
|
145
|
+
return null;
|
|
146
|
+
const slope = property === 'enthalpy' ? first.cp : first.cp / boundary;
|
|
147
|
+
let distance = Math.abs(target - first[property]) / slope;
|
|
148
|
+
let bracket;
|
|
149
|
+
while (true) {
|
|
150
|
+
bracket = Math.max(lower, Math.min(upper, boundary + direction * distance));
|
|
151
|
+
if (bracket === boundary)
|
|
152
|
+
bracket = nextPositiveFloat(boundary, direction);
|
|
153
|
+
const last = evaluate(bracket);
|
|
154
|
+
if (direction * (last[property] - target) >= 0)
|
|
155
|
+
break;
|
|
156
|
+
if (bracket === lower || bracket === upper)
|
|
157
|
+
return null;
|
|
158
|
+
distance *= 2;
|
|
159
|
+
}
|
|
160
|
+
lower = Math.min(boundary, bracket);
|
|
161
|
+
upper = Math.max(boundary, bracket);
|
|
162
|
+
const temperature = bracketedNewton((T) => evaluate(T)[property] - target, lower, upper, undefined, {
|
|
163
|
+
derivative: (T) => {
|
|
164
|
+
const state = evaluate(T);
|
|
165
|
+
return property === 'enthalpy' ? state.cp : state.cp / T;
|
|
166
|
+
},
|
|
167
|
+
tolerance: BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE,
|
|
168
|
+
residualTolerance: 0,
|
|
169
|
+
});
|
|
170
|
+
return evaluate(temperature);
|
|
171
|
+
}
|
|
172
|
+
/** Detect region for P-H inputs. */
|
|
173
|
+
export function detectRegionPH(p, h) {
|
|
174
|
+
return detectRegionPressureProperty(p, h, 'enthalpy');
|
|
175
|
+
}
|
|
176
|
+
/** Detect region for P-S inputs. */
|
|
177
|
+
export function detectRegionPS(p, s) {
|
|
178
|
+
return detectRegionPressureProperty(p, s, 'entropy');
|
|
179
|
+
}
|
|
180
|
+
function detectRegionPressureProperty(p, target, property) {
|
|
181
|
+
if (!Number.isFinite(p) || !Number.isFinite(target))
|
|
182
|
+
return -1;
|
|
183
|
+
const seam = pressurePropertySeam(p, target, property);
|
|
184
|
+
if (seam !== null)
|
|
185
|
+
return seam.state.region;
|
|
186
|
+
const boundary = pressurePropertyBoundaryState(p, target, property);
|
|
187
|
+
if (boundary !== null)
|
|
188
|
+
return boundary.region;
|
|
189
|
+
// Preserve PH's existing lower-bound tolerance; PS absorbs EOS rounding noise.
|
|
190
|
+
const aboveMinimum = (minimum) => property === 'enthalpy'
|
|
191
|
+
? minimum <= target || isClose(target, minimum)
|
|
192
|
+
: withinClosedLowerBound(target, minimum, 'entropy');
|
|
193
|
+
if (p >= C.P_MIN && p <= P13) {
|
|
194
|
+
const endpoints = p >= C.Pt ? saturationEndpointsAtPressure(p) : null;
|
|
195
|
+
if (endpoints !== null && endpoints.liquid[property] <= target && target <= endpoints.vapor[property]) {
|
|
196
|
+
return Region.Region4;
|
|
197
|
+
}
|
|
198
|
+
const [liquidT, vaporT] = endpoints === null ? nativeSaturationTemperatureBracket(p)
|
|
199
|
+
: [endpoints.temperature, endpoints.temperature];
|
|
200
|
+
const lower = region1(p, C.T_MIN)[property];
|
|
201
|
+
const boundary14 = endpoints?.liquid[property] ?? region1(p, liquidT)[property];
|
|
202
|
+
const boundary24 = endpoints?.vapor[property] ?? region2(p, vaporT)[property];
|
|
203
|
+
const boundary25 = region2(p, C.R2_T_MAX)[property];
|
|
204
|
+
if (aboveMinimum(lower) && target <= boundary14)
|
|
205
|
+
return Region.Region1;
|
|
206
|
+
if (withinClosedLowerBound(target, boundary24, property) && withinClosedUpperBound(target, boundary25))
|
|
207
|
+
return Region.Region2;
|
|
208
|
+
if (p <= C.R5_P_MAX && region5(p, C.R5_T_MIN)[property] < target &&
|
|
209
|
+
withinClosedUpperBound(target, region5(p, C.R5_T_MAX)[property]))
|
|
210
|
+
return Region.Region5;
|
|
211
|
+
}
|
|
212
|
+
else if (p > P13 && p <= C.P_MAX) {
|
|
213
|
+
const lower = region1(p, C.T_MIN)[property];
|
|
214
|
+
const boundary13 = region1(p, C.R2_T_MIN)[property];
|
|
215
|
+
const [, region2T] = nativeB23TemperatureBracket(p);
|
|
216
|
+
const boundary32 = region2(p, region2T)[property];
|
|
217
|
+
const boundary25 = region2(p, C.R2_T_MAX)[property];
|
|
218
|
+
if (p < C.Pc) {
|
|
219
|
+
try {
|
|
220
|
+
const endpoints = saturationEndpointsAtPressure(p);
|
|
221
|
+
if (endpoints.liquid[property] <= target && target <= endpoints.vapor[property]) {
|
|
222
|
+
return Region.Region4;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (!(error instanceof SaturationEndpointError) ||
|
|
227
|
+
(target > boundary13 && target < boundary32))
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (aboveMinimum(lower) && target <= boundary13)
|
|
232
|
+
return Region.Region1;
|
|
233
|
+
if (withinClosedLowerBound(target, boundary32, property) && withinClosedUpperBound(target, boundary25))
|
|
234
|
+
return Region.Region2;
|
|
235
|
+
if (p <= C.R5_P_MAX && region5(p, C.R5_T_MIN)[property] < target &&
|
|
236
|
+
withinClosedUpperBound(target, region5(p, C.R5_T_MAX)[property]))
|
|
237
|
+
return Region.Region5;
|
|
238
|
+
if (p < C.Pc) {
|
|
239
|
+
const endpoints = saturationEndpointsAtPressure(p);
|
|
240
|
+
const liquid = endpoints.liquid[property];
|
|
241
|
+
const vapor = endpoints.vapor[property];
|
|
242
|
+
if (boundary13 < target && target < liquid)
|
|
243
|
+
return Region.Region3;
|
|
244
|
+
if (liquid < target && target < vapor)
|
|
245
|
+
return Region.Region4;
|
|
246
|
+
if (vapor < target && target < boundary32)
|
|
247
|
+
return Region.Region3;
|
|
248
|
+
}
|
|
249
|
+
else if (boundary13 < target && target < boundary32) {
|
|
250
|
+
return Region.Region3;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return -1;
|
|
254
|
+
}
|
|
255
|
+
// Accept floating-point residue only at closed upper bounds.
|
|
256
|
+
function withinClosedUpperBound(value, boundary) {
|
|
257
|
+
return value <= boundary + 8 * Number.EPSILON * Math.max(1, Math.abs(boundary));
|
|
258
|
+
}
|
|
259
|
+
/** Check approximate equality with relative tolerance. */
|
|
260
|
+
function isClose(a, b) {
|
|
261
|
+
return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
262
|
+
}
|
package/dist/backward/ps.d.ts
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* Reference: IAPWS-IF97 Supplementary Release on Backward Equations
|
|
5
5
|
*/
|
|
6
6
|
import type { BasicProperties } from '../types.js';
|
|
7
|
+
export declare function r1BackwardT(p: number, s: number): number;
|
|
8
|
+
export declare function r2BackwardT(p: number, s: number): number;
|
|
9
|
+
export declare function r3BackwardPSInitial(p: number, s: number): {
|
|
10
|
+
rho: number;
|
|
11
|
+
T: number;
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* Solve for thermodynamic state given P and S.
|
|
9
15
|
* @param p - Pressure [MPa]
|
|
10
16
|
* @param s - Specific entropy [kJ/(kg·K)]
|
|
11
17
|
*/
|
|
12
18
|
export declare function solvePS(p: number, s: number): BasicProperties;
|
|
13
|
-
//# sourceMappingURL=ps.d.ts.map
|