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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as C from '../constants.js';
|
|
2
|
+
import { solveRegion3PTBasic } from '../core/region3-pt.js';
|
|
3
|
+
import { nextPositiveFloat } from '../core/floating-point.js';
|
|
4
|
+
import { NumericalStateError } from '../errors.js';
|
|
5
|
+
import { formulationSeamAtTemperature, matchFixedSeam } from '../core/formulation-seams.js';
|
|
6
|
+
import { boundary23_T_to_P } from '../regions/boundaries.js';
|
|
7
|
+
import { region1 } from '../regions/region1.js';
|
|
8
|
+
import { region2 } from '../regions/region2.js';
|
|
9
|
+
import { region5 } from '../regions/region5.js';
|
|
10
|
+
import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtTemperature, } from '../saturation/common.js';
|
|
11
|
+
import { AmbiguousStateError, OutOfRangeError } from '../types.js';
|
|
12
|
+
import { createFixedTemperatureStationaryPressureFinder } from './fixed-temperature-stationary.js';
|
|
13
|
+
import { solveFixedTemperaturePressure } from './fixed-temperature-solver.js';
|
|
14
|
+
import { backwardCandidateError, validateBackwardState } from './solution-validation.js';
|
|
15
|
+
import { fixedTemperatureRootTolerance } from './tolerances.js';
|
|
16
|
+
/** Return the only physical TH/TS state, or null if unresolved; proven envelope violations throw. */
|
|
17
|
+
export function uniqueFixedTemperatureState(T, target, property) {
|
|
18
|
+
if (!Number.isFinite(T) || !Number.isFinite(target) || T < C.T_MIN || T > C.T_MAX) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const solverName = property === 'enthalpy' ? 'solveTH' : 'solveTS';
|
|
22
|
+
const rootTolerance = fixedTemperatureRootTolerance(property);
|
|
23
|
+
const constraints = [
|
|
24
|
+
{ label: 'temperature', expected: T },
|
|
25
|
+
{ label: property, expected: target },
|
|
26
|
+
];
|
|
27
|
+
const candidates = [];
|
|
28
|
+
let propertyMin = Infinity;
|
|
29
|
+
let propertyMax = -Infinity;
|
|
30
|
+
let finiteEnvelope = true;
|
|
31
|
+
const seamPair = formulationSeamAtTemperature(T);
|
|
32
|
+
const seam = seamPair === null ? null : matchFixedSeam(seamPair, property, target, 'temperature');
|
|
33
|
+
const incidentCandidates = new Set();
|
|
34
|
+
const stationaryPressures = createFixedTemperatureStationaryPressureFinder(T, property);
|
|
35
|
+
const sameState = (first, second) => Math.abs(first.pressure - second.pressure) <= 1e-9 * Math.max(1, Math.abs(second.pressure)) &&
|
|
36
|
+
Math.abs(first.specificVolume - second.specificVolume) <=
|
|
37
|
+
1e-9 * Math.max(first.specificVolume, second.specificVolume);
|
|
38
|
+
const add = (state, incident = false) => {
|
|
39
|
+
if (state !== seam?.state) {
|
|
40
|
+
const error = backwardCandidateError(state, constraints, { solverName });
|
|
41
|
+
if (error instanceof NumericalStateError)
|
|
42
|
+
throw error;
|
|
43
|
+
if (error !== null)
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// The same saturation endpoint or stationary point can bound two intervals.
|
|
47
|
+
let existing = candidates.find((other) => sameState(other, state));
|
|
48
|
+
if (existing !== undefined && incident && seam?.kind === 'overlap' &&
|
|
49
|
+
state.region === seam.state.region && existing.region !== state.region) {
|
|
50
|
+
candidates[candidates.indexOf(existing)] = state;
|
|
51
|
+
incidentCandidates.delete(existing);
|
|
52
|
+
existing = state;
|
|
53
|
+
}
|
|
54
|
+
if (existing === undefined) {
|
|
55
|
+
candidates.push(state);
|
|
56
|
+
}
|
|
57
|
+
if (incident)
|
|
58
|
+
incidentCandidates.add(existing ?? state);
|
|
59
|
+
};
|
|
60
|
+
const addInterval = (evaluator, lower, upper, includeLower = true, includeUpper = true) => {
|
|
61
|
+
if (upper.pressure < lower.pressure ||
|
|
62
|
+
((!includeLower || !includeUpper) && upper.pressure === lower.pressure))
|
|
63
|
+
return;
|
|
64
|
+
const stateAt = (p, temperature) => {
|
|
65
|
+
if (p === lower.pressure)
|
|
66
|
+
return lower;
|
|
67
|
+
if (p === upper.pressure)
|
|
68
|
+
return upper;
|
|
69
|
+
return evaluator(p, temperature);
|
|
70
|
+
};
|
|
71
|
+
const points = [
|
|
72
|
+
lower,
|
|
73
|
+
...stationaryPressures(lower, upper)
|
|
74
|
+
.map((p) => stateAt(p, T)),
|
|
75
|
+
upper,
|
|
76
|
+
];
|
|
77
|
+
const lowerIncident = seamPair !== null && lower.pressure === seamPair.pressure;
|
|
78
|
+
const upperIncident = seamPair !== null && upper.pressure === seamPair.pressure;
|
|
79
|
+
for (let i = 0; i < points.length; i++) {
|
|
80
|
+
const current = points[i];
|
|
81
|
+
if (Number.isFinite(current[property])) {
|
|
82
|
+
propertyMin = Math.min(propertyMin, current[property]);
|
|
83
|
+
propertyMax = Math.max(propertyMax, current[property]);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
finiteEnvelope = false;
|
|
87
|
+
}
|
|
88
|
+
const residual = current[property] - target;
|
|
89
|
+
const currentIncluded = (i > 0 || includeLower) && (i < points.length - 1 || includeUpper);
|
|
90
|
+
const currentMatches = Math.abs(residual) <= rootTolerance &&
|
|
91
|
+
(currentIncluded || candidates.some((other) => sameState(other, current)));
|
|
92
|
+
if (currentMatches && currentIncluded) {
|
|
93
|
+
add(current, (i === 0 && lowerIncident) || (i === points.length - 1 && upperIncident));
|
|
94
|
+
}
|
|
95
|
+
if (!currentIncluded && Math.abs(residual) <= rootTolerance && !currentMatches) {
|
|
96
|
+
// The closest admissible float is the endpoint of the supported pressure domain.
|
|
97
|
+
const pressure = nextPositiveFloat(current.pressure, i === 0 ? 1 : -1);
|
|
98
|
+
if (pressure > lower.pressure && pressure < upper.pressure) {
|
|
99
|
+
const interior = stateAt(pressure, T);
|
|
100
|
+
if (Math.abs(interior[property] - target) <= rootTolerance) {
|
|
101
|
+
add(interior, (i === 0 && lowerIncident) || (i === points.length - 1 && upperIncident));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (i === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const previous = points[i - 1];
|
|
108
|
+
const previousResidual = previous[property] - target;
|
|
109
|
+
const previousIncluded = i > 1 || includeLower;
|
|
110
|
+
const previousMatches = Math.abs(previousResidual) <= rootTolerance &&
|
|
111
|
+
(previousIncluded || candidates.some((other) => sameState(other, previous)));
|
|
112
|
+
// An excluded endpoint cannot hide a root just inside its open interval.
|
|
113
|
+
if (currentMatches || previousMatches ||
|
|
114
|
+
previousResidual * residual >= 0)
|
|
115
|
+
continue;
|
|
116
|
+
add(solveFixedTemperaturePressure(stateAt, T, target, previous.pressure, current.pressure, property, solverName, previousIncluded, currentIncluded), (i === 1 && lowerIncident) || (i === points.length - 1 && upperIncident));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (T > C.R5_T_MIN) {
|
|
120
|
+
addInterval(region5, region5(C.P_MIN, T), region5(C.R5_P_MAX, T));
|
|
121
|
+
}
|
|
122
|
+
else if (T >= C.B23_T_MAX) {
|
|
123
|
+
addInterval(region2, region2(C.P_MIN, T), region2(C.P_MAX, T));
|
|
124
|
+
}
|
|
125
|
+
else if (T >= C.Tc) {
|
|
126
|
+
const p23 = Math.min(C.P_MAX, boundary23_T_to_P(T));
|
|
127
|
+
addInterval(region2, region2(C.P_MIN, T), region2(p23, T), true, false);
|
|
128
|
+
addInterval(solveRegion3PTBasic, solveRegion3PTBasic(p23, T), solveRegion3PTBasic(C.P_MAX, T));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const endpoints = saturationEndpointsAtTemperature(T);
|
|
132
|
+
if (T >= C.Tt &&
|
|
133
|
+
target >= endpoints.liquid[property] - rootTolerance &&
|
|
134
|
+
target <= endpoints.vapor[property] + rootTolerance) {
|
|
135
|
+
add(mixSaturationState(endpoints, qualityFromSaturationProperty(endpoints.liquid[property], endpoints.vapor[property], target)));
|
|
136
|
+
}
|
|
137
|
+
if (T <= C.R2_T_MIN) {
|
|
138
|
+
addInterval(region1, region1(endpoints.pressure, T), region1(C.P_MAX, T));
|
|
139
|
+
addInterval(region2, region2(C.P_MIN, T), region2(endpoints.pressure, T), true, false);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const p23 = boundary23_T_to_P(T);
|
|
143
|
+
addInterval(region2, region2(C.P_MIN, T), region2(Math.min(p23, endpoints.pressure), T), true, false);
|
|
144
|
+
// Keep the vapor and liquid Region 3 branches separate at saturation.
|
|
145
|
+
addInterval(solveRegion3PTBasic, solveRegion3PTBasic(p23, T), endpoints.vapor);
|
|
146
|
+
addInterval(solveRegion3PTBasic, endpoints.liquid, solveRegion3PTBasic(C.P_MAX, T));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (seam !== null) {
|
|
150
|
+
if (seam.kind === 'canonical') {
|
|
151
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
152
|
+
if (incidentCandidates.has(candidates[i]))
|
|
153
|
+
candidates.splice(i, 1);
|
|
154
|
+
}
|
|
155
|
+
add(seam.state);
|
|
156
|
+
}
|
|
157
|
+
else if (seam.kind === 'overlap') {
|
|
158
|
+
const canonical = candidates.find((state) => incidentCandidates.has(state) &&
|
|
159
|
+
state.region === seam.pair.canonical.region);
|
|
160
|
+
if (canonical !== undefined) {
|
|
161
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
162
|
+
if (candidates[i] !== canonical && incidentCandidates.has(candidates[i]))
|
|
163
|
+
candidates.splice(i, 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (candidates.length === 0) {
|
|
168
|
+
add(seam.state);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Only the hull exterior proves a range error; an interior gap can be numerical.
|
|
172
|
+
if (candidates.length === 0 && finiteEnvelope &&
|
|
173
|
+
Number.isFinite(propertyMin) && Number.isFinite(propertyMax) &&
|
|
174
|
+
(target < propertyMin - rootTolerance || target > propertyMax + rootTolerance)) {
|
|
175
|
+
throw new OutOfRangeError(`${property === 'enthalpy' ? 'Enthalpy' : 'Entropy'} at T=${T} K`, target, propertyMin, propertyMax);
|
|
176
|
+
}
|
|
177
|
+
const validate = (state) => validateBackwardState(state, constraints, {
|
|
178
|
+
solverName,
|
|
179
|
+
expectedRegion: state.region,
|
|
180
|
+
...(seam !== null && state === seam.state ? { seam } : {}),
|
|
181
|
+
});
|
|
182
|
+
if (candidates.length > 1) {
|
|
183
|
+
const validated = candidates.map(validate);
|
|
184
|
+
validated.sort((first, second) => first.pressure - second.pressure);
|
|
185
|
+
throw new AmbiguousStateError(T, target, property, validated);
|
|
186
|
+
}
|
|
187
|
+
return candidates.length === 0 ? null : validate(candidates[0]);
|
|
188
|
+
}
|
|
@@ -11,8 +11,8 @@ export type FixedTemperatureProperty = 'enthalpy' | 'entropy';
|
|
|
11
11
|
* Solve for pressure at fixed temperature by matching a target property value.
|
|
12
12
|
*
|
|
13
13
|
* Evaluates the region equation at the bracket endpoints first (short-circuit
|
|
14
|
-
* if
|
|
15
|
-
* the analytic pressure derivative.
|
|
14
|
+
* if an included endpoint matches), then runs a bracketed Newton iteration driven by
|
|
15
|
+
* the analytic pressure derivative (density derivative for Region 3).
|
|
16
16
|
*
|
|
17
17
|
* @param evaluator - Region-specific equation returning BasicProperties from (p, T)
|
|
18
18
|
* @param T - Fixed temperature [K]
|
|
@@ -22,4 +22,4 @@ export type FixedTemperatureProperty = 'enthalpy' | 'entropy';
|
|
|
22
22
|
* @param property - Which property `target` represents
|
|
23
23
|
* @param solverName - Label used in error messages
|
|
24
24
|
*/
|
|
25
|
-
export declare function solveFixedTemperaturePressure(evaluator: (p: number, T: number) => BasicProperties, T: number, target: number, lower: number, upper: number, property: FixedTemperatureProperty, solverName: string): BasicProperties;
|
|
25
|
+
export declare function solveFixedTemperaturePressure(evaluator: (p: number, T: number) => BasicProperties, T: number, target: number, lower: number, upper: number, property: FixedTemperatureProperty, solverName: string, includeLower?: boolean, includeUpper?: boolean): BasicProperties;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { IF97Error } from '../types.js';
|
|
1
|
+
import { IF97Error, Region } from '../types.js';
|
|
2
|
+
import { region3ByRhoT } from '../regions/region3.js';
|
|
2
3
|
import { bracketedNewton } from '../solvers/bracketed-newton.js';
|
|
3
|
-
import { gibbsPropertyDerivatives } from '
|
|
4
|
+
import { gibbsPropertyDerivatives, helmholtzPropertyDerivatives } from '../core/thermodynamic-derivatives.js';
|
|
5
|
+
import { fixedTemperatureRootTolerance } from './tolerances.js';
|
|
4
6
|
function propertyValue(state, property) {
|
|
5
7
|
return property === 'enthalpy' ? state.enthalpy : state.entropy;
|
|
6
8
|
}
|
|
@@ -19,8 +21,8 @@ function pressureDerivative(state, property) {
|
|
|
19
21
|
* Solve for pressure at fixed temperature by matching a target property value.
|
|
20
22
|
*
|
|
21
23
|
* Evaluates the region equation at the bracket endpoints first (short-circuit
|
|
22
|
-
* if
|
|
23
|
-
* the analytic pressure derivative.
|
|
24
|
+
* if an included endpoint matches), then runs a bracketed Newton iteration driven by
|
|
25
|
+
* the analytic pressure derivative (density derivative for Region 3).
|
|
24
26
|
*
|
|
25
27
|
* @param evaluator - Region-specific equation returning BasicProperties from (p, T)
|
|
26
28
|
* @param T - Fixed temperature [K]
|
|
@@ -30,26 +32,45 @@ function pressureDerivative(state, property) {
|
|
|
30
32
|
* @param property - Which property `target` represents
|
|
31
33
|
* @param solverName - Label used in error messages
|
|
32
34
|
*/
|
|
33
|
-
export function solveFixedTemperaturePressure(evaluator, T, target, lower, upper, property, solverName) {
|
|
34
|
-
|
|
35
|
+
export function solveFixedTemperaturePressure(evaluator, T, target, lower, upper, property, solverName, includeLower = true, includeUpper = true) {
|
|
36
|
+
const lowerState = evaluator(lower, T);
|
|
37
|
+
const upperState = evaluator(upper, T);
|
|
38
|
+
const useDensity = lowerState.region === Region.Region3;
|
|
39
|
+
// Density bounds keep Region 3 iteration on the same saturation branch.
|
|
40
|
+
const lowerBound = useDensity ? 1 / lowerState.specificVolume : lower;
|
|
41
|
+
const upperBound = useDensity ? 1 / upperState.specificVolume : upper;
|
|
42
|
+
// Keyed by p or ρ, so the residual and its derivative at the same point share one
|
|
35
43
|
// equation evaluation regardless of the order the solver calls them in.
|
|
36
|
-
let
|
|
44
|
+
let cachedInput = Number.NaN;
|
|
37
45
|
let cachedState = null;
|
|
38
|
-
const stateAt = (
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
cachedState = evaluator(
|
|
46
|
+
const stateAt = (input) => {
|
|
47
|
+
if (input !== cachedInput || cachedState === null) {
|
|
48
|
+
cachedInput = input;
|
|
49
|
+
cachedState = useDensity ? region3ByRhoT(input, T) : evaluator(input, T);
|
|
42
50
|
}
|
|
43
51
|
return cachedState;
|
|
44
52
|
};
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
if (Math.abs(propertyValue(lowerState, property) - target) <= 1e-9)
|
|
53
|
+
const rootTolerance = fixedTemperatureRootTolerance(property);
|
|
54
|
+
if (includeLower && Math.abs(propertyValue(lowerState, property) - target) <= rootTolerance)
|
|
48
55
|
return lowerState;
|
|
49
|
-
if (Math.abs(propertyValue(upperState, property) - target) <=
|
|
56
|
+
if (includeUpper && Math.abs(propertyValue(upperState, property) - target) <= rootTolerance)
|
|
50
57
|
return upperState;
|
|
51
|
-
|
|
52
|
-
const
|
|
58
|
+
// Resolve open endpoints below their matching tolerance instead of snapping to them.
|
|
59
|
+
const residualScale = includeLower && includeUpper ? 1 : rootTolerance;
|
|
60
|
+
const root = bracketedNewton((input) => (propertyValue(stateAt(input), property) - target) / residualScale, lowerBound, upperBound, (lowerBound + upperBound) / 2, {
|
|
61
|
+
derivative: (input) => {
|
|
62
|
+
if (useDensity) {
|
|
63
|
+
const derivatives = helmholtzPropertyDerivatives(stateAt(input), input);
|
|
64
|
+
return (property === 'enthalpy' ? derivatives.dhDrho : derivatives.dsDrho) / residualScale;
|
|
65
|
+
}
|
|
66
|
+
return pressureDerivative(stateAt(input), property) / residualScale;
|
|
67
|
+
},
|
|
68
|
+
// Near-critical properties need a tighter bracket before termination.
|
|
69
|
+
tolerance: residualScale === 1 ? 1e-12 : Number.EPSILON,
|
|
70
|
+
// Flat isotherms require convergence in pressure or density.
|
|
71
|
+
residualTolerance: 0,
|
|
72
|
+
});
|
|
73
|
+
const state = stateAt(root);
|
|
53
74
|
if (!Number.isFinite(state.pressure)) {
|
|
54
75
|
throw new IF97Error(`${solverName} failed to recover a valid pressure`);
|
|
55
76
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type BasicProperties } from '../types.js';
|
|
2
|
+
import type { FixedTemperatureProperty } from './fixed-temperature-solver.js';
|
|
3
|
+
/** Share one isotherm's coefficients while keeping each pressure interval separate. */
|
|
4
|
+
export declare function createFixedTemperatureStationaryPressureFinder(T: number, property: FixedTemperatureProperty): (lowerState: BasicProperties, upperState: BasicProperties) => number[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { RHOc, Tc } from '../constants.js';
|
|
2
|
+
import { COEFFICIENTS as R1_COEFFICIENTS } from '../regions/region1.js';
|
|
3
|
+
import { RESIDUAL_COEFFICIENTS as R2_COEFFICIENTS } from '../regions/region2.js';
|
|
4
|
+
import { COEFFICIENTS as R3_COEFFICIENTS, region3ByRhoT } from '../regions/region3.js';
|
|
5
|
+
import { RESIDUAL_COEFFICIENTS as R5_COEFFICIENTS } from '../regions/region5.js';
|
|
6
|
+
import { Region } from '../types.js';
|
|
7
|
+
function evaluate(coefficients, x) {
|
|
8
|
+
let value = 0;
|
|
9
|
+
for (let i = coefficients.length - 1; i >= 0; i--)
|
|
10
|
+
value = value * x + coefficients[i];
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
// Derivative roots partition a polynomial into monotone intervals (Rolle's theorem).
|
|
14
|
+
function polynomialRoots(coefficients, lower, upper) {
|
|
15
|
+
while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0)
|
|
16
|
+
coefficients.pop();
|
|
17
|
+
if (coefficients.length < 2)
|
|
18
|
+
return [];
|
|
19
|
+
const scale = Math.max(...coefficients.map(Math.abs));
|
|
20
|
+
const normalized = coefficients.map((value) => value / scale);
|
|
21
|
+
// Interval Horner bounds can exclude zero before constructing derivative roots.
|
|
22
|
+
let minimum = 0;
|
|
23
|
+
let maximum = 0;
|
|
24
|
+
let magnitude = 0;
|
|
25
|
+
for (let i = normalized.length - 1; i >= 0; i--) {
|
|
26
|
+
minimum = normalized[i] + minimum * (minimum < 0 ? upper : lower);
|
|
27
|
+
maximum = normalized[i] + maximum * (maximum < 0 ? lower : upper);
|
|
28
|
+
magnitude += Math.abs(normalized[i]);
|
|
29
|
+
}
|
|
30
|
+
// With x in [0, 1], allow roundoff before excluding endpoint or tangent zeros.
|
|
31
|
+
const roundingMargin = 4 * Number.EPSILON * normalized.length * magnitude;
|
|
32
|
+
if (minimum > roundingMargin || maximum < -roundingMargin)
|
|
33
|
+
return [];
|
|
34
|
+
const derivative = normalized.slice(1).map((value, i) => value * (i + 1));
|
|
35
|
+
const points = [lower, ...polynomialRoots(derivative, lower, upper), upper];
|
|
36
|
+
const roots = [];
|
|
37
|
+
for (let i = 0; i < points.length; i++) {
|
|
38
|
+
let a = points[i];
|
|
39
|
+
let fa = evaluate(normalized, a);
|
|
40
|
+
if (fa === 0)
|
|
41
|
+
roots.push(a);
|
|
42
|
+
if (i === points.length - 1)
|
|
43
|
+
break;
|
|
44
|
+
let b = points[i + 1];
|
|
45
|
+
const fb = evaluate(normalized, b);
|
|
46
|
+
if (fa === 0 || fb === 0 || Math.sign(fa) === Math.sign(fb))
|
|
47
|
+
continue;
|
|
48
|
+
for (let step = 0; step < 64; step++) {
|
|
49
|
+
const midpoint = (a + b) / 2;
|
|
50
|
+
if (midpoint === a || midpoint === b)
|
|
51
|
+
break;
|
|
52
|
+
const fm = evaluate(normalized, midpoint);
|
|
53
|
+
if (fm === 0) {
|
|
54
|
+
a = midpoint;
|
|
55
|
+
b = midpoint;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (Math.sign(fa) === Math.sign(fm)) {
|
|
59
|
+
a = midpoint;
|
|
60
|
+
fa = fm;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
b = midpoint;
|
|
64
|
+
}
|
|
65
|
+
roots.push((a + b) / 2);
|
|
66
|
+
}
|
|
67
|
+
return roots.filter((root, i) => i === 0 || root !== roots[i - 1]);
|
|
68
|
+
}
|
|
69
|
+
function stationaryCoefficients(T, region, property) {
|
|
70
|
+
let table;
|
|
71
|
+
let tau;
|
|
72
|
+
switch (region) {
|
|
73
|
+
case Region.Region1:
|
|
74
|
+
table = R1_COEFFICIENTS;
|
|
75
|
+
tau = 1386 / T - 1.222;
|
|
76
|
+
break;
|
|
77
|
+
case Region.Region2:
|
|
78
|
+
table = R2_COEFFICIENTS;
|
|
79
|
+
tau = 540 / T - 0.5;
|
|
80
|
+
break;
|
|
81
|
+
case Region.Region3:
|
|
82
|
+
table = R3_COEFFICIENTS;
|
|
83
|
+
tau = Tc / T;
|
|
84
|
+
break;
|
|
85
|
+
case Region.Region5:
|
|
86
|
+
table = R5_COEFFICIENTS;
|
|
87
|
+
tau = 1000 / T;
|
|
88
|
+
break;
|
|
89
|
+
case Region.Region4: return [];
|
|
90
|
+
}
|
|
91
|
+
const logarithmicEntropy = property === 'entropy' && region !== Region.Region1;
|
|
92
|
+
const coefficients = Array(Math.max(...table.map(([I]) => I)) +
|
|
93
|
+
(logarithmicEntropy ? 1 : 0)).fill(0);
|
|
94
|
+
if (logarithmicEntropy) {
|
|
95
|
+
// Multiplying ds/dp (or ds/dδ) by its positive native variable clears 1/p (1/δ).
|
|
96
|
+
coefficients[0] = region === Region.Region3 ? -R3_COEFFICIENTS[0][2] : -1;
|
|
97
|
+
}
|
|
98
|
+
for (const [I, J, N] of table) {
|
|
99
|
+
if (I === 0)
|
|
100
|
+
continue;
|
|
101
|
+
// Constant nonzero factors do not affect the derivative's zeros.
|
|
102
|
+
let coefficient;
|
|
103
|
+
if (region === Region.Region3) {
|
|
104
|
+
coefficient = N * I * (property === 'enthalpy' ? I + J : J - 1) * Math.pow(tau, J);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const tauDerivative = J === 0 ? 0 : J * Math.pow(tau, J - 1);
|
|
108
|
+
const physicalTau = tau + (region === Region.Region1 ? 1.222 : region === Region.Region2 ? 0.5 : 0);
|
|
109
|
+
coefficient = property === 'enthalpy'
|
|
110
|
+
? N * I * tauDerivative
|
|
111
|
+
: N * I * (physicalTau * tauDerivative - Math.pow(tau, J));
|
|
112
|
+
}
|
|
113
|
+
coefficients[I - (logarithmicEntropy ? 0 : 1)] += coefficient;
|
|
114
|
+
}
|
|
115
|
+
return coefficients;
|
|
116
|
+
}
|
|
117
|
+
/** Share one isotherm's coefficients while keeping each pressure interval separate. */
|
|
118
|
+
export function createFixedTemperatureStationaryPressureFinder(T, property) {
|
|
119
|
+
const coefficientsByRegion = new Map();
|
|
120
|
+
return (lowerState, upperState) => {
|
|
121
|
+
const region = lowerState.region;
|
|
122
|
+
let coefficients = coefficientsByRegion.get(region);
|
|
123
|
+
if (coefficients === undefined) {
|
|
124
|
+
coefficients = stationaryCoefficients(T, region, property);
|
|
125
|
+
coefficientsByRegion.set(region, coefficients);
|
|
126
|
+
}
|
|
127
|
+
if (coefficients.length === 0)
|
|
128
|
+
return [];
|
|
129
|
+
const nativeValue = (state) => region === Region.Region1
|
|
130
|
+
? 7.1 - state.pressure / 16.53
|
|
131
|
+
: region === Region.Region3 ? 1 / (state.specificVolume * RHOc) : state.pressure;
|
|
132
|
+
const lower = Math.min(nativeValue(lowerState), nativeValue(upperState));
|
|
133
|
+
const upper = Math.max(nativeValue(lowerState), nativeValue(upperState));
|
|
134
|
+
// Scaling the native variable to [0, 1] avoids powers of pressures up to 100 MPa.
|
|
135
|
+
const scaled = coefficients.map((value, i) => value * Math.pow(upper, i));
|
|
136
|
+
return polynomialRoots(scaled, lower / upper, 1).map((root) => {
|
|
137
|
+
const native = root * upper;
|
|
138
|
+
if (region === Region.Region1)
|
|
139
|
+
return (7.1 - native) * 16.53;
|
|
140
|
+
if (region === Region.Region3)
|
|
141
|
+
return region3ByRhoT(native * RHOc, T).pressure;
|
|
142
|
+
return native;
|
|
143
|
+
}).filter((pressure) => pressure > lowerState.pressure && pressure < upperState.pressure)
|
|
144
|
+
.sort((a, b) => a - b);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BasicProperties } from '../types.js';
|
|
2
|
+
import { Region } from '../types.js';
|
|
3
|
+
export interface HSClassificationCandidate {
|
|
4
|
+
state?: BasicProperties;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Saturated liquid enthalpy boundary for Region 1 (Eq. 3).
|
|
8
|
+
* h'_1(s)
|
|
9
|
+
*/
|
|
10
|
+
export declare function h1Sat(s: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Region 1 / Region 3 boundary enthalpy (Eq. 1, B13).
|
|
13
|
+
* h_13(s)
|
|
14
|
+
*/
|
|
15
|
+
export declare function h13Boundary(s: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Region 3a / Region 4 saturation boundary (Eq. 4).
|
|
18
|
+
* h'_3a(s)
|
|
19
|
+
*/
|
|
20
|
+
export declare function h3aSat(s: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Region 2ab / Region 4 saturation boundary (Eq. 5).
|
|
23
|
+
* h"_2ab(s)
|
|
24
|
+
*/
|
|
25
|
+
export declare function h2abSat(s: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Region 2c / Region 3b saturation boundary (Eq. 6).
|
|
28
|
+
* h"_2c3b(s)
|
|
29
|
+
*/
|
|
30
|
+
export declare function h2c3bSat(s: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Region 2/3 boundary temperature from h,s (Eq. 8).
|
|
33
|
+
* T_B23(h,s)
|
|
34
|
+
*/
|
|
35
|
+
export declare function tB23hs(h: number, s: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* Backward equation P(h,s) for Region 2c (used for R2/R3 discrimination).
|
|
38
|
+
*/
|
|
39
|
+
export declare function backward2cPhs(h: number, s: number): number;
|
|
40
|
+
/** Share HS classification, throwing range errors only for proven envelope violations. */
|
|
41
|
+
export declare function classifyRegionHS(h: number, s: number, solved?: HSClassificationCandidate): Region | -1;
|
|
42
|
+
/**
|
|
43
|
+
* Detect the IAPWS-IF97 region for given H and S.
|
|
44
|
+
* Implements the zone-based detection from the IAPWS supplementary release.
|
|
45
|
+
*
|
|
46
|
+
* @param h - Specific enthalpy [kJ/kg]
|
|
47
|
+
* @param s - Specific entropy [kJ/(kg·K)]
|
|
48
|
+
* @returns Region number (1–5) or -1 if out of range
|
|
49
|
+
*/
|
|
50
|
+
export declare function detectRegionHS(h: number, s: number): Region | -1;
|