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
package/dist/backward/ph.js
CHANGED
|
@@ -7,18 +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';
|
|
20
|
+
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
19
21
|
import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
|
|
20
|
-
import { validateBackwardState } from './solution-validation.js';
|
|
21
|
-
import { helmholtzPropertyDerivatives } from '
|
|
22
|
+
import { validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
|
|
23
|
+
import { helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
|
|
22
24
|
import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtPressure, } from '../saturation/common.js';
|
|
23
25
|
import { assertCriticalRegion4PHInput, assertRegion4StateAllowed, } from '../saturation/region4-boundaries.js';
|
|
24
26
|
// ─── Backward Polynomial Evaluator ─────────────────────────────────────────
|
|
@@ -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) {
|
|
@@ -119,7 +121,7 @@ export function r2BackwardT(p, h) {
|
|
|
119
121
|
}
|
|
120
122
|
// ─── Region 3 Backward T(P,H) and v(P,H) ──────────────────────────────────
|
|
121
123
|
/** B3ab boundary: P → H */
|
|
122
|
-
function b3ab_P_to_H(p) {
|
|
124
|
+
export function b3ab_P_to_H(p) {
|
|
123
125
|
return 2014.64004206875 + 3.74696550136983 * p - 0.0219921901054187 * p * p + 0.000087513168600995 * p * p * p;
|
|
124
126
|
}
|
|
125
127
|
const R3A_PH_T = [
|
|
@@ -183,15 +185,16 @@ export function r3BackwardPHInitial(p, h) {
|
|
|
183
185
|
const v = 0.0088 * evalPoly(R3B_PH_V, -0.0661, 0.720, p / 100, h / 2800);
|
|
184
186
|
return { rho: 1 / v, T };
|
|
185
187
|
}
|
|
186
|
-
function solveRegion3PH(p, h, rho0, T0) {
|
|
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);
|
|
188
191
|
const hScale = Math.max(1, Math.abs(h));
|
|
189
192
|
const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
|
|
190
193
|
const state = region3ByRhoT(candidateRho, candidateT);
|
|
191
194
|
const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
|
|
192
195
|
return {
|
|
193
196
|
residual: [
|
|
194
|
-
(state.pressure - p) / pScale,
|
|
197
|
+
(refinePressure ? region3PressureResidual(candidateRho, candidateT, p) : state.pressure - p) / pScale,
|
|
195
198
|
(state.enthalpy - h) / hScale,
|
|
196
199
|
],
|
|
197
200
|
jacobian: [
|
|
@@ -200,6 +203,8 @@ function solveRegion3PH(p, h, rho0, T0) {
|
|
|
200
203
|
],
|
|
201
204
|
};
|
|
202
205
|
}, [rho0, T0], {
|
|
206
|
+
// Near critical pressure, the default residual can cross a stability boundary.
|
|
207
|
+
tolerance: 1e-13,
|
|
203
208
|
isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
|
|
204
209
|
});
|
|
205
210
|
return region3ByRhoT(rho, T);
|
|
@@ -216,11 +221,35 @@ export function solvePH(p, h) {
|
|
|
216
221
|
if (p < C.P_MIN || p > C.P_MAX) {
|
|
217
222
|
throw new OutOfRangeError('Pressure', p, C.P_MIN, C.P_MAX);
|
|
218
223
|
}
|
|
219
|
-
if (h
|
|
224
|
+
if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
|
|
220
225
|
throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
|
|
221
226
|
}
|
|
222
227
|
assertCriticalRegion4PHInput(p, h, 'solvePH');
|
|
223
|
-
|
|
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
|
+
}
|
|
224
253
|
switch (region) {
|
|
225
254
|
case Region.Region1: {
|
|
226
255
|
const T0 = r1BackwardT(p, h);
|
|
@@ -231,21 +260,42 @@ export function solvePH(p, h) {
|
|
|
231
260
|
], { solverName: 'solvePH', expectedRegion: Region.Region1 });
|
|
232
261
|
}
|
|
233
262
|
case Region.Region2: {
|
|
263
|
+
const targetH = Math.min(h, region2(p, C.R2_T_MAX).enthalpy);
|
|
234
264
|
const T0 = r2BackwardT(p, h);
|
|
235
|
-
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);
|
|
236
266
|
return validateBackwardState(region2(p, T), [
|
|
237
267
|
{ label: 'pressure', expected: p },
|
|
238
268
|
{ label: 'enthalpy', expected: h },
|
|
239
269
|
], { solverName: 'solvePH', expectedRegion: Region.Region2 });
|
|
240
270
|
}
|
|
241
271
|
case Region.Region3: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
|
|
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
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (saturationFailure !== null && error instanceof IF97Error)
|
|
296
|
+
throw saturationFailure;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
249
299
|
}
|
|
250
300
|
case Region.Region4: {
|
|
251
301
|
const endpoints = saturationEndpointsAtPressure(p);
|
|
@@ -257,7 +307,8 @@ export function solvePH(p, h) {
|
|
|
257
307
|
], { solverName: 'solvePH', expectedRegion: Region.Region4 });
|
|
258
308
|
}
|
|
259
309
|
case Region.Region5: {
|
|
260
|
-
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 });
|
|
261
312
|
return validateBackwardState(region5(p, T), [
|
|
262
313
|
{ label: 'pressure', expected: p },
|
|
263
314
|
{ label: 'enthalpy', expected: h },
|
|
@@ -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,6 +4,12 @@
|
|
|
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]
|
package/dist/backward/ps.js
CHANGED
|
@@ -4,17 +4,19 @@
|
|
|
4
4
|
* Reference: IAPWS-IF97 Supplementary Release on Backward Equations
|
|
5
5
|
*/
|
|
6
6
|
import { Region, IF97Error, OutOfRangeError } from '../types.js';
|
|
7
|
+
import { SaturationEndpointError } from '../errors.js';
|
|
7
8
|
import * as C from '../constants.js';
|
|
8
9
|
import { region1 } from '../regions/region1.js';
|
|
9
10
|
import { region2 } from '../regions/region2.js';
|
|
10
|
-
import { region3ByRhoT } from '../regions/region3.js';
|
|
11
|
+
import { region3ByRhoT, region3PressureResidual } from '../regions/region3.js';
|
|
11
12
|
import { region5 } from '../regions/region5.js';
|
|
12
|
-
import { assertFiniteNumber } from '../core/input-validation.js';
|
|
13
|
-
import { detectRegionPS } from '
|
|
13
|
+
import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
|
|
14
|
+
import { assertPressurePropertyEnvelope, detectRegionPS, pressurePropertyBoundaryState, pressurePropertySeam } from './pressure-property-region.js';
|
|
14
15
|
import { newtonRaphson } from '../solvers/newton-raphson.js';
|
|
16
|
+
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
15
17
|
import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
|
|
16
|
-
import { validateBackwardState } from './solution-validation.js';
|
|
17
|
-
import { helmholtzPropertyDerivatives } from '
|
|
18
|
+
import { validateBackwardState, validateRegion3PTReconstruction } from './solution-validation.js';
|
|
19
|
+
import { helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
|
|
18
20
|
import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtPressure, } from '../saturation/common.js';
|
|
19
21
|
import { assertCriticalRegion4PSInput, assertRegion4StateAllowed, } from '../saturation/region4-boundaries.js';
|
|
20
22
|
function evalPoly(table, piShift, sigShift, pi, sig) {
|
|
@@ -33,7 +35,7 @@ const R1_PS = [
|
|
|
33
35
|
[2, 31, -4.2522657042207e-26], [3, 10, 2.6400441360689e-13], [3, 32, 7.8124600459723e-29],
|
|
34
36
|
[4, 32, -3.0732199903668e-31],
|
|
35
37
|
];
|
|
36
|
-
function r1BackwardT(p, s) {
|
|
38
|
+
export function r1BackwardT(p, s) {
|
|
37
39
|
return evalPoly(R1_PS, 0, -2, p, s);
|
|
38
40
|
}
|
|
39
41
|
// ─── Region 2 Backward T(P,S) — Subregions 2a/2b/2c ───────────────────────
|
|
@@ -41,7 +43,7 @@ const R2A_PS = [
|
|
|
41
43
|
[-1.5, -24, -392359.83861984], [-1.5, -23, 515265.7382727], [-1.5, -19, 40482.443161048],
|
|
42
44
|
[-1.5, -13, -321.93790923902], [-1.5, -11, 96.961424218694], [-1.5, -10, -22.867846371773],
|
|
43
45
|
[-1.25, -19, -449429.14124357], [-1.25, -15, -5011.8336020166], [-1.25, -6, 0.35684463560015],
|
|
44
|
-
[-1, -26, 44235.33584819], [-1, -21, -13673.388811708], [-1, -17, 421632.
|
|
46
|
+
[-1, -26, 44235.33584819], [-1, -21, -13673.388811708], [-1, -17, 421632.60207864],
|
|
45
47
|
[-1, -16, 22516.925837475], [-1, -9, 474.42144865646], [-1, -8, -149.31130797647],
|
|
46
48
|
[-0.75, -15, -197811.26320452], [-0.75, -14, -23554.39947076], [-0.5, -26, -19070.616302076],
|
|
47
49
|
[-0.5, -13, 55375.669883164], [-0.5, -9, 3829.3691437363], [-0.5, -7, -603.91860580567],
|
|
@@ -84,7 +86,7 @@ const R2C_PS = [
|
|
|
84
86
|
[6, 1, 1.0136618529763e-8], [7, 0, 5.9925719692351e-12], [7, 1, -2.0677870105164e-11],
|
|
85
87
|
[7, 3, -2.0874278181886e-11], [7, 4, 1.0162166825089e-10], [7, 5, -1.6429828281347e-10],
|
|
86
88
|
];
|
|
87
|
-
function r2BackwardT(p, s) {
|
|
89
|
+
export function r2BackwardT(p, s) {
|
|
88
90
|
if (p <= C.R2_P_CRT) {
|
|
89
91
|
return evalPoly(R2A_PS, 0, 2, p, s / 2);
|
|
90
92
|
}
|
|
@@ -144,15 +146,26 @@ const R3B_PS_V_TABLE = [
|
|
|
144
146
|
[0, 2, -19.1449143716586], [1, 0, -0.0150448002905284], [1, 2, 14.6407900162154],
|
|
145
147
|
[2, 2, -3.2747778718823],
|
|
146
148
|
];
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
+
export function r3BackwardPSInitial(p, s) {
|
|
150
|
+
if (s <= C.R3_S_CRT) {
|
|
151
|
+
const T = 760 * evalPoly(R3A_PS_T_TABLE, -0.240, 0.703, p / 100, s / 4.4);
|
|
152
|
+
const v = 0.0028 * evalPoly(R3A_PS_V_TABLE, -0.187, 0.755, p / 100, s / 4.4);
|
|
153
|
+
return { rho: 1 / v, T };
|
|
154
|
+
}
|
|
155
|
+
const T = 860 * evalPoly(R3B_PS_T_TABLE, -0.760, 0.818, p / 100, s / 5.3);
|
|
156
|
+
const v = 0.0088 * evalPoly(R3B_PS_V_TABLE, -0.298, 0.816, p / 100, s / 5.3);
|
|
157
|
+
return { rho: 1 / v, T };
|
|
158
|
+
}
|
|
159
|
+
function solveRegion3PS(p, s, rho0, T0, refinePressure = false) {
|
|
160
|
+
// Scaling the 1e-13 solver tolerance by 1e-4 resolves pressure below one ULP before PT reconstruction.
|
|
161
|
+
const pScale = Math.max(1, Math.abs(p)) * (refinePressure ? 1e-4 : 1);
|
|
149
162
|
const sScale = Math.max(1, Math.abs(s));
|
|
150
163
|
const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
|
|
151
164
|
const state = region3ByRhoT(candidateRho, candidateT);
|
|
152
165
|
const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
|
|
153
166
|
return {
|
|
154
167
|
residual: [
|
|
155
|
-
(state.pressure - p) / pScale,
|
|
168
|
+
(refinePressure ? region3PressureResidual(candidateRho, candidateT, p) : state.pressure - p) / pScale,
|
|
156
169
|
(state.entropy - s) / sScale,
|
|
157
170
|
],
|
|
158
171
|
jacobian: [
|
|
@@ -161,6 +174,8 @@ function solveRegion3PS(p, s, rho0, T0) {
|
|
|
161
174
|
],
|
|
162
175
|
};
|
|
163
176
|
}, [rho0, T0], {
|
|
177
|
+
// Near saturation, the default residual can put the liquid root across Tsat.
|
|
178
|
+
tolerance: 1e-13,
|
|
164
179
|
isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
|
|
165
180
|
});
|
|
166
181
|
return region3ByRhoT(rho, T);
|
|
@@ -177,11 +192,35 @@ export function solvePS(p, s) {
|
|
|
177
192
|
if (p < C.P_MIN || p > C.P_MAX) {
|
|
178
193
|
throw new OutOfRangeError('Pressure', p, C.P_MIN, C.P_MAX);
|
|
179
194
|
}
|
|
180
|
-
if (s
|
|
195
|
+
if (!withinClosedLowerBound(s, C.S_MIN, 'entropy') || s > C.S_MAX) {
|
|
181
196
|
throw new OutOfRangeError('Entropy', s, C.S_MIN, C.S_MAX);
|
|
182
197
|
}
|
|
183
198
|
assertCriticalRegion4PSInput(p, s, 'solvePS');
|
|
184
|
-
|
|
199
|
+
assertPressurePropertyEnvelope(p, s, 'entropy');
|
|
200
|
+
const seam = pressurePropertySeam(p, s, 'entropy');
|
|
201
|
+
if (seam !== null && seam.kind !== 'overlap') {
|
|
202
|
+
return validateBackwardState(seam.state, [
|
|
203
|
+
{ label: 'pressure', expected: p },
|
|
204
|
+
{ label: 'entropy', expected: s },
|
|
205
|
+
], { solverName: 'solvePS', expectedRegion: seam.state.region, seam });
|
|
206
|
+
}
|
|
207
|
+
if (seam === null) {
|
|
208
|
+
const boundary = pressurePropertyBoundaryState(p, s, 'entropy');
|
|
209
|
+
if (boundary !== null)
|
|
210
|
+
return boundary;
|
|
211
|
+
}
|
|
212
|
+
let region;
|
|
213
|
+
let saturationFailure = null;
|
|
214
|
+
try {
|
|
215
|
+
region = seam?.state.region ?? detectRegionPS(p, s);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (!(error instanceof SaturationEndpointError)) {
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
saturationFailure = error;
|
|
222
|
+
region = Region.Region3;
|
|
223
|
+
}
|
|
185
224
|
switch (region) {
|
|
186
225
|
case Region.Region1: {
|
|
187
226
|
const T0 = r1BackwardT(p, s);
|
|
@@ -192,29 +231,42 @@ export function solvePS(p, s) {
|
|
|
192
231
|
], { solverName: 'solvePS', expectedRegion: Region.Region1 });
|
|
193
232
|
}
|
|
194
233
|
case Region.Region2: {
|
|
234
|
+
const targetS = Math.min(s, region2(p, C.R2_T_MAX).entropy);
|
|
195
235
|
const T0 = r2BackwardT(p, s);
|
|
196
|
-
const T = newtonRaphson((T_x) => region2(p, T_x).entropy -
|
|
236
|
+
const T = newtonRaphson((T_x) => region2(p, T_x).entropy - targetS, T0, (T_x) => (region2(p, T_x).cp ?? Number.NaN) / T_x);
|
|
197
237
|
return validateBackwardState(region2(p, T), [
|
|
198
238
|
{ label: 'pressure', expected: p },
|
|
199
239
|
{ label: 'entropy', expected: s },
|
|
200
240
|
], { solverName: 'solvePS', expectedRegion: Region.Region2 });
|
|
201
241
|
}
|
|
202
242
|
case Region.Region3: {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
243
|
+
try {
|
|
244
|
+
const initial = r3BackwardPSInitial(p, s);
|
|
245
|
+
const validate = (state) => validateBackwardState(state, [
|
|
246
|
+
{ label: 'pressure', expected: p },
|
|
247
|
+
{ label: 'entropy', expected: s },
|
|
248
|
+
], { solverName: 'solvePS', expectedRegion: Region.Region3 });
|
|
249
|
+
const validated = validate(solveRegion3PS(p, s, initial.rho, initial.T));
|
|
250
|
+
// Compressibility can amplify an accepted pressure residual beyond PT's density accuracy.
|
|
251
|
+
const relativeDensityError = Math.abs(region3PressureResidual(1 / validated.specificVolume, validated.temperature, p)) * validated.isothermalCompressibility;
|
|
252
|
+
if (saturationFailure === null && relativeDensityError <= 1e-12)
|
|
253
|
+
return validated;
|
|
254
|
+
try {
|
|
255
|
+
const refined = validate(solveRegion3PS(p, s, 1 / validated.specificVolume, validated.temperature, true));
|
|
256
|
+
validateRegion3PTReconstruction(refined, 'solvePS');
|
|
257
|
+
return refined;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (!(error instanceof IF97Error))
|
|
261
|
+
throw error;
|
|
262
|
+
return saturationFailure === null ? validated : validateRegion3PTReconstruction(validated, 'solvePS');
|
|
263
|
+
}
|
|
208
264
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (saturationFailure !== null && error instanceof IF97Error)
|
|
267
|
+
throw saturationFailure;
|
|
268
|
+
throw error;
|
|
212
269
|
}
|
|
213
|
-
const state = solveRegion3PS(p, s, 1 / v0, T0);
|
|
214
|
-
return validateBackwardState(state, [
|
|
215
|
-
{ label: 'pressure', expected: p, tolerance: pressureTolerance },
|
|
216
|
-
{ label: 'entropy', expected: s },
|
|
217
|
-
], { solverName: 'solvePS', expectedRegion: Region.Region3 });
|
|
218
270
|
}
|
|
219
271
|
case Region.Region4: {
|
|
220
272
|
const endpoints = saturationEndpointsAtPressure(p);
|
|
@@ -226,7 +278,8 @@ export function solvePS(p, s) {
|
|
|
226
278
|
], { solverName: 'solvePS', expectedRegion: Region.Region4 });
|
|
227
279
|
}
|
|
228
280
|
case Region.Region5: {
|
|
229
|
-
const
|
|
281
|
+
const targetS = Math.min(s, region5(p, C.R5_T_MAX).entropy);
|
|
282
|
+
const T = bracketedNewton((T_x) => region5(p, T_x).entropy - targetS, 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) / T_x, tolerance: 1e-12 });
|
|
230
283
|
return validateBackwardState(region5(p, T), [
|
|
231
284
|
{ label: 'pressure', expected: p },
|
|
232
285
|
{ label: 'entropy', expected: s },
|