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/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 2.8.0 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `solveBasic(input)` and exported `BasicProperties` for all eight modes, without density or transport fields.
|
|
8
|
+
- Include liquid and vapor endpoints in Region 4 `saturation` (`SaturationEndpoints`), including at `x = 0` and `x = 1`.
|
|
9
|
+
- Test backward and boundary equations against official IAPWS values at their printed precision, including all 26 Region 3 `v(p,T)` subregions.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Remove decimal snapping and backward property input overwrites. Region 3 PT preserves input pressure after checking equation-of-state (EOS) convergence.
|
|
14
|
+
- Throw `AmbiguousStateError`, an `IF97Error` subclass, when `TH`/`TS` inputs match multiple states. This applies to direct solvers, `solve`, `solveBasic`, and region detectors. Its `candidates` field contains validated basic states in ascending pressure order. Callers that previously received one branch must handle ambiguity.
|
|
15
|
+
- Tighten ordinary backward enthalpy/entropy acceptance to `1e-9 × max(1, |input|)` and Region 3 PT pressure convergence to relative `1e-12`, with separate endpoint and fit-gap tolerances.
|
|
16
|
+
- Shorten the README, move API details to `docs/reference.md`, and redraw the P–T region map.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Correct 11 coefficient transcription errors in Region 1/3 `p(h,s)`, Region 3 `v(p,T)`, HS boundaries, and Region 2a `T(p,h)`/`T(p,s)`. Public `region3Volume` results change; refined solver results can differ in their last floating-point bits.
|
|
21
|
+
- Search all valid `TH`/`TS` branches, including non-monotonic isotherms, subcritical Region 3 vapor, and roots near open endpoints. Preserve multiple candidates near the triple point; low-temperature `TS` can match two liquid states and wet steam.
|
|
22
|
+
- Recover PH/PS and HS boundary roots and resolve region overlaps using `solvePT` boundary ownership. Recompute EOS properties after boundary normalization and revalidate input constraints before accepting candidates or fit-gap states.
|
|
23
|
+
- Preserve saturation input pressure or temperature, endpoint quality, and phase selection at `623.15 K` and near critical conditions. Use extended precision for saturation equations and Region 3 pressure inversion. Throw `IF97Error` when a stable phase or distinct saturation endpoints cannot be constructed.
|
|
24
|
+
- Correct global and pressure/entropy-dependent input limits. Reject out-of-range backward inputs while retaining valid boundary states; explain the triple-point lower bound in saturation errors.
|
|
25
|
+
- Force bisection when two iterations fail to halve the Newton bracket, preserving roots with quantized residuals near the triple point.
|
|
26
|
+
- Compute density as `1 / specificVolume`. Reject non-finite or non-positive viscosity, non-positive thermal-conductivity `cp`, and dielectric-constant inputs at or below `228 K`.
|
|
27
|
+
|
|
28
|
+
## 2.2.0
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Improved backward-solver accuracy and performance.
|
|
33
|
+
- Fixed saturation-region detection and package validation.
|
|
34
|
+
- Requires Node.js `>=20.19.0`.
|
|
4
35
|
|
|
5
36
|
## 2.1.5
|
|
6
37
|
|
package/README.md
CHANGED
|
@@ -1,266 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# iapws-if97
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Industrial water and steam properties for Node.js and TypeScript.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Turn two known properties into a water or steam state using [IAPWS-IF97](https://iapws.org/technical-guidance/release/IF97-Rev). For steam tables, thermal-cycle calculations, and engineering tools.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://github.com/jltonghui/iapws-if97/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/iapws-if97)
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
-
- Forward, backward, and saturation solvers behind a consistent API
|
|
11
|
-
- Transport properties: viscosity, thermal conductivity, surface tension, dielectric constant, and ionization constant
|
|
12
|
-
- Verified against official IAPWS tables and published engineering references
|
|
10
|
+
[API reference](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md) · [References](#references) · [Changelog](CHANGELOG.md)
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+

|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
- Forward, backward, and saturation solvers for eight input pairs across IF97 Regions 1–5.
|
|
15
|
+
- Saturation results include liquid and vapor endpoints.
|
|
16
|
+
- Viscosity, thermal conductivity, surface tension, dielectric constant, and ionization constant where applicable.
|
|
17
|
+
- ESM with TypeScript declarations and no runtime dependencies.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
## Start calculating
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Requires Node.js ≥20.19.0. Use ESM imports.
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
npm install iapws-if97
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Save as `example.mjs` and run `node example.mjs`:
|
|
27
28
|
|
|
28
|
-
```
|
|
29
|
-
import {
|
|
29
|
+
```js
|
|
30
|
+
import { solvePT } from 'iapws-if97';
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
const b = solve({ mode: 'PH', p: 3, h: a.enthalpy });
|
|
33
|
-
const c = solvePx(1, 0.5);
|
|
32
|
+
const steam = solvePT(3, 573.15); // 3 MPa, 300 °C
|
|
34
33
|
|
|
35
|
-
console.log(
|
|
36
|
-
console.log(
|
|
37
|
-
console.log(c.quality); // 0.5
|
|
34
|
+
console.log(steam.enthalpy.toFixed(2)); // 2994.35 kJ/kg
|
|
35
|
+
console.log(steam.density.toFixed(3)); // 12.319 kg/m³
|
|
38
36
|
```
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
The result includes pressure, temperature, enthalpy, entropy, density, heat capacities, speed of sound, and applicable transport properties. [All fields and units →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#solver-return-value)
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
- `solvePH(p, h)` // `MPa`, `kJ/kg`
|
|
44
|
-
- `solvePS(p, s)` // `MPa`, `kJ/(kg·K)`
|
|
45
|
-
- `solveHS(h, s)` // `kJ/kg`, `kJ/(kg·K)`
|
|
46
|
-
- `solveTH(T, h)` // `K`, `kJ/kg`
|
|
47
|
-
- `solveTS(T, s)` // `K`, `kJ/(kg·K)`
|
|
48
|
-
- `solvePx(p, x)` // `MPa`, quality `[0, 1]`
|
|
49
|
-
- `solveTx(T, x)` // `K`, quality `[0, 1]`
|
|
50
|
-
- `solve(input)` // same units as the selected `mode`
|
|
40
|
+
## Use the properties you have
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
| Known properties | Solver |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Pressure + temperature | `solvePT(p, T)` |
|
|
45
|
+
| Pressure + enthalpy | `solvePH(p, h)` |
|
|
46
|
+
| Pressure + entropy | `solvePS(p, s)` |
|
|
47
|
+
| Enthalpy + entropy | `solveHS(h, s)` |
|
|
48
|
+
| Temperature + enthalpy | `solveTH(T, h)` |
|
|
49
|
+
| Temperature + entropy | `solveTS(T, s)` |
|
|
50
|
+
| Pressure + vapor quality | `solvePx(p, x)` |
|
|
51
|
+
| Temperature + vapor quality | `solveTx(T, x)` |
|
|
59
52
|
|
|
60
|
-
`
|
|
53
|
+
Use MPa, K, kJ/kg, and kJ/(kg·K) for pressure, temperature, enthalpy, and entropy. Vapor quality `x` is the vapor mass fraction, from `0` (saturated liquid) to `1` (saturated vapor).
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
const a = solve({ mode: 'PT', p: 16, T: 823.15 });
|
|
64
|
-
const b = solve({ mode: 'PT', pressure: 16, temperature: 823.15 });
|
|
65
|
-
```
|
|
55
|
+
Use `solve()` when the input pair is chosen at runtime:
|
|
66
56
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
type SolveInput =
|
|
71
|
-
| { mode: 'PT'; p: number; T: number }
|
|
72
|
-
| { mode: 'PH'; p: number; h: number }
|
|
73
|
-
| { mode: 'PS'; p: number; s: number }
|
|
74
|
-
| { mode: 'HS'; h: number; s: number }
|
|
75
|
-
| { mode: 'Px'; p: number; x: number }
|
|
76
|
-
| { mode: 'Tx'; T: number; x: number }
|
|
77
|
-
| { mode: 'TH'; T: number; h: number }
|
|
78
|
-
| { mode: 'TS'; T: number; s: number };
|
|
79
|
-
```
|
|
57
|
+
```js
|
|
58
|
+
import { solve } from 'iapws-if97';
|
|
80
59
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Solver Return Value
|
|
84
|
-
|
|
85
|
-
All solvers return a `SteamState` object containing both thermodynamic and transport properties:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
type SteamState = {
|
|
89
|
-
region: Region;
|
|
90
|
-
pressure: number;
|
|
91
|
-
temperature: number;
|
|
92
|
-
specificVolume: number;
|
|
93
|
-
density: number;
|
|
94
|
-
internalEnergy: number;
|
|
95
|
-
entropy: number;
|
|
96
|
-
enthalpy: number;
|
|
97
|
-
cp: number | null;
|
|
98
|
-
cv: number | null;
|
|
99
|
-
speedOfSound: number | null;
|
|
100
|
-
quality: number | null;
|
|
101
|
-
viscosity: number | null;
|
|
102
|
-
thermalConductivity: number | null;
|
|
103
|
-
surfaceTension: number | null;
|
|
104
|
-
dielectricConstant: number | null;
|
|
105
|
-
ionizationConstant: number | null;
|
|
106
|
-
isobaricExpansion: number | null;
|
|
107
|
-
isothermalCompressibility: number | null;
|
|
108
|
-
};
|
|
60
|
+
const state = solve({ mode: 'PH', p: 3, h: 3000 });
|
|
109
61
|
```
|
|
110
62
|
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
**Notes:**
|
|
114
|
-
|
|
115
|
-
- `quality` is `null` for single-phase states — it is only defined on the saturation line.
|
|
116
|
-
- In two-phase mixtures (`0 < x < 1`), `cp`, `cv`, `speedOfSound`, `isobaricExpansion`, `isothermalCompressibility`, `viscosity`, `thermalConductivity`, `dielectricConstant`, and `ionizationConstant` are `null`.
|
|
117
|
-
- Saturation endpoints (`x = 0` or `x = 1`) still expose single-phase transport properties even though they carry Region 4 metadata.
|
|
118
|
-
- `solvePT(p, T)` is a single-phase solver. On the subcritical saturation boundary it resolves to the liquid side.
|
|
119
|
-
- `surfaceTension` is only available for Region 4 saturation states below the critical point; otherwise `null`.
|
|
120
|
-
- `density` is provided directly — no need to invert `specificVolume`.
|
|
121
|
-
- `ionizationConstant` is `null` outside the IAPWS validity range for that correlation.
|
|
122
|
-
|
|
123
|
-
## Units
|
|
124
|
-
|
|
125
|
-
- Pressure: `MPa`
|
|
126
|
-
- Temperature: `K`
|
|
127
|
-
- Specific volume: `m^3/kg`
|
|
128
|
-
- Density: `kg/m^3`
|
|
129
|
-
- Enthalpy, internal energy: `kJ/kg`
|
|
130
|
-
- Entropy, heat capacities: `kJ/(kg·K)`
|
|
131
|
-
- Speed of sound: `m/s`
|
|
132
|
-
- Quality: dimensionless, `0`–`1` (saturation line only)
|
|
133
|
-
- Viscosity: `Pa·s`
|
|
134
|
-
- Thermal conductivity: `W/(m·K)`
|
|
135
|
-
- Surface tension: `N/m`
|
|
136
|
-
- Dielectric constant: dimensionless
|
|
137
|
-
- Ionization constant (`pKw`): dimensionless
|
|
138
|
-
- Isobaric expansion coefficient: `1/K`
|
|
139
|
-
- Isothermal compressibility: `1/MPa`
|
|
140
|
-
|
|
141
|
-
Any field typed as `number | null` returns `null` when the property is undefined for the given state.
|
|
142
|
-
|
|
143
|
-
## Saturation Endpoint
|
|
144
|
-
|
|
145
|
-
- Low-level Region 4 helpers stay mathematically permissive and high-level saturation state solvers are stricter:
|
|
146
|
-
- `solvePx(p, x)` accepts `Pt ≤ p < Pc`
|
|
147
|
-
- `solveTx(T, x)` accepts `Tt = 273.16 K ≤ T < Tc`
|
|
148
|
-
- The true triple point is supported as a saturation-state boundary:
|
|
149
|
-
- `solvePx(Pt, x)` is valid
|
|
150
|
-
- `solveTx(Tt, x)` is valid
|
|
151
|
-
- `273.15 K` is treated as a low-level extrapolation boundary only.
|
|
152
|
-
- The exact critical point is never returned as a Region 4 state.
|
|
153
|
-
- `solveTH(T, h)` and `solveTS(T, s)` keep a small exclusion band around `Tc` and reject inputs within `0.001 K` of the critical temperature.
|
|
154
|
-
|
|
155
|
-
## Errors and Limits
|
|
156
|
-
|
|
157
|
-
The library throws typed errors:
|
|
158
|
-
|
|
159
|
-
| Error | Meaning |
|
|
160
|
-
| --- | --- |
|
|
161
|
-
| `OutOfRangeError` | Input is outside the supported IF97 range |
|
|
162
|
-
| `ConvergenceError` | An internal iterative solve failed to converge |
|
|
163
|
-
| `IF97Error` | Base class for all library-specific errors |
|
|
164
|
-
|
|
165
|
-
## Validity Notes
|
|
63
|
+
Long field names such as `pressure` and `enthalpy` are also accepted. Use [`solveBasic()`](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#thermodynamics-without-transport) when you only need thermodynamics; it skips transport calculations and omits `density`.
|
|
166
64
|
|
|
167
|
-
|
|
168
|
-
- Surface tension applies only along the saturation line below the critical point.
|
|
169
|
-
- Thermal conductivity follows the IAPWS 2011 release.
|
|
170
|
-
- Ionization constant follows the IAPWS 2024 revised release, valid for stable fluid states from `273.15 K` to `1273.15 K` and up to `1000 MPa`.
|
|
65
|
+
## Work with saturation
|
|
171
66
|
|
|
172
|
-
|
|
67
|
+
Specify quality to calculate saturated liquid, saturated vapor, or wet steam:
|
|
173
68
|
|
|
174
|
-
|
|
69
|
+
```js
|
|
70
|
+
import { solvePx } from 'iapws-if97';
|
|
175
71
|
|
|
176
|
-
|
|
177
|
-
- Backward-equation round-trip accuracy
|
|
178
|
-
- Temperature-led backward round trips (`TH` and `TS`)
|
|
179
|
-
- High-pressure Region 4 regressions
|
|
180
|
-
- IAPWS R11-24 ionization-constant verification values
|
|
181
|
-
- Coverage thresholds enforced locally
|
|
182
|
-
- ASME and GB/T steam-table comparisons against published values
|
|
72
|
+
const wet = solvePx(1, 0.9); // 1 MPa, 90% vapor by mass
|
|
183
73
|
|
|
184
|
-
|
|
74
|
+
console.log(wet.temperature.toFixed(2)); // 453.04 K
|
|
185
75
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
76
|
+
if (wet.saturation) {
|
|
77
|
+
const { liquid, vapor } = wet.saturation;
|
|
78
|
+
const latentHeat = vapor.enthalpy - liquid.enthalpy;
|
|
79
|
+
console.log(latentHeat.toFixed(2)); // 2014.44 kJ/kg
|
|
80
|
+
}
|
|
191
81
|
```
|
|
192
82
|
|
|
193
|
-
|
|
83
|
+
Every saturation result includes `saturation.liquid` and `saturation.vapor` for phase properties and latent-heat calculations. Use `solveTx(T, x)` when temperature is known instead of pressure.
|
|
194
84
|
|
|
195
|
-
|
|
85
|
+
## Before integrating
|
|
196
86
|
|
|
197
|
-
|
|
87
|
+
- PT selects a single phase. Pressure and temperature alone do not determine wet-steam quality; use `Px` or `Tx` for a mixture.
|
|
88
|
+
- Single-phase `quality` is `null`. Mixtures return `null` for heat capacities, speed of sound, and most transport properties. [Return-value details →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#solver-return-value)
|
|
89
|
+
- TH and TS throw `AmbiguousStateError` with candidate states when multiple solutions exist. Use pressure or quality to resolve the state. [Errors →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#errors-and-limits)
|
|
90
|
+
- The exact critical point is unsupported, and near-critical solves can fail. Transport correlations have separate validity ranges. [Numerical limits →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#saturation-endpoints) · [Transport limits →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#transport-correlation-limits)
|
|
198
91
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
The snapshot below reflects the current `tests/standards-asme` regression run.
|
|
204
|
-
|
|
205
|
-
| ASME case | Pressure (MPa) | Points checked | Max relative error |
|
|
206
|
-
| --- | ---: | ---: | ---: |
|
|
207
|
-
| Table S-2 @ 0.01 MPa | 0.01 | 7 | 0.0257993% |
|
|
208
|
-
| Table S-2 @ 0.10 MPa | 0.10 | 7 | 0.0141744% |
|
|
209
|
-
| Table S-2 @ 1.00 MPa | 1.00 | 7 | 0.0207405% |
|
|
210
|
-
| Table S-2 @ 5.0 MPa | 5.0 | 7 | 0.0315405% |
|
|
211
|
-
| Table S-2 @ 10.0 MPa | 10.0 | 7 | 0.0261599% |
|
|
212
|
-
| Table S-2 @ 20.0 MPa | 20.0 | 7 | 0.0173004% |
|
|
213
|
-
| Overall worst case | - | 42 | 0.0315405% |
|
|
214
|
-
|
|
215
|
-
The current ASME regression threshold is `maxRelativeError < 5e-4` (less than `0.05%`).
|
|
92
|
+
| Thermodynamic temperature range | Maximum pressure |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 273.15–1073.15 K | 100 MPa |
|
|
95
|
+
| Above 1073.15 K, up to 2273.15 K | 50 MPa |
|
|
216
96
|
|
|
217
|
-
|
|
97
|
+
Saturation solvers accept 273.16 ≤ T < 647.096 K or 0.000611657 ≤ p < 22.064 MPa. All numeric inputs must be finite; invalid or unresolved states throw [typed errors](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#errors-and-limits).
|
|
218
98
|
|
|
219
|
-
|
|
220
|
-
Each case checks 4 values at `10 MPa`: specific volume, enthalpy, entropy, and speed of sound.
|
|
221
|
-
The snapshot below reflects the current `tests/standards-cn` regression run.
|
|
99
|
+
## Verification and development
|
|
222
100
|
|
|
223
|
-
|
|
224
|
-
| --- | ---: | ---: | ---: | ---: |
|
|
225
|
-
| Table A.3 @ 10 MPa, 350 C | 10 | 350 | 4 | 0.0007020% |
|
|
226
|
-
| Table A.3 @ 10 MPa, 400 C | 10 | 400 | 4 | 0.0004650% |
|
|
227
|
-
| Table A.3 @ 10 MPa, 500 C | 10 | 500 | 4 | 0.0006808% |
|
|
228
|
-
| Table A.3 @ 10 MPa, 600 C | 10 | 600 | 4 | 0.0005630% |
|
|
229
|
-
| Table A.3 @ 10 MPa, 700 C | 10 | 700 | 4 | 0.0004072% |
|
|
230
|
-
| Table A.3 @ 10 MPa, 800 C | 10 | 800 | 4 | 0.0004146% |
|
|
231
|
-
| Overall worst case | - | - | 24 | 0.0007020% |
|
|
101
|
+
Tests cover official IAPWS verification values, backward-solver round trips, region boundaries, and published ASME and GB/T steam tables. CI also checks TypeScript types, package exports, and coverage thresholds. [Verification details and references →](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#verification)
|
|
232
102
|
|
|
233
|
-
|
|
103
|
+
```bash
|
|
104
|
+
npm ci
|
|
105
|
+
npm run typecheck
|
|
106
|
+
npm test
|
|
107
|
+
npm run test:package
|
|
108
|
+
```
|
|
234
109
|
|
|
235
|
-
|
|
110
|
+
For low-level region equations, saturation curves, transport correlations, or region detection, see [advanced imports](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#advanced-imports).
|
|
236
111
|
|
|
237
|
-
|
|
112
|
+
## References
|
|
238
113
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
114
|
+
- [IAPWS R7-97(2012), Industrial Formulation 1997](https://iapws.org/technical-guidance/release/IF97-Rev).
|
|
115
|
+
- [IAPWS supplementary releases](https://iapws.org/technical-guidance/release) for alternative input pairs, region boundaries, and Region 3 specific volume.
|
|
116
|
+
- [R12-08, viscosity](https://iapws.org/technical-guidance/release/viscosity) and [R15-11, thermal conductivity](https://iapws.org/technical-guidance/release/ThCond).
|
|
117
|
+
- [R1-76(2014), surface tension](https://iapws.org/technical-guidance/release/Surf-H2O), [R8-97, dielectric constant](https://iapws.org/technical-guidance/release/Dielec), and [R11-24, ionization constant](https://iapws.org/technical-guidance/release/Ionization).
|
|
242
118
|
|
|
243
|
-
|
|
244
|
-
const state: SteamState = solve(input);
|
|
119
|
+
See the [full reference list](https://github.com/jltonghui/iapws-if97/blob/main/docs/reference.md#references) for release details.
|
|
245
120
|
|
|
246
|
-
|
|
247
|
-
console.log('compressed liquid');
|
|
248
|
-
}
|
|
249
|
-
```
|
|
121
|
+
## Project origin
|
|
250
122
|
|
|
251
|
-
|
|
123
|
+
> `iapws-if97` began as the calculation engine behind the WeChat Mini Program "汽水计算器" (`wxid: wx7201fd1713b524e5`). Since its 2019 launch, it has served more than 20,000 users. AI-assisted review helped bring this open-source edition up to date with IAPWS `R11-24 (2024)`. I am sharing the engine so it can reach engineers far beyond the original program.
|
|
252
124
|
|
|
253
|
-
|
|
254
|
-
- IAPWS R7-97(2012), Revised Release on the IAPWS Industrial Formulation 1997 for the Thermodynamic Properties of Water and Steam
|
|
255
|
-
- IAPWS supplementary releases for backward equations `T(p,h)`, `T(p,s)`, and `p(h,s)`
|
|
256
|
-
- IAPWS R15-11, Thermal Conductivity of Ordinary Water Substance
|
|
257
|
-
- IAPWS R1-76(2014), Surface Tension of Ordinary Water Substance
|
|
258
|
-
- IAPWS R11-24, Revised Release on the Ionization Constant of H2O
|
|
125
|
+
<p align="right">Shuping, retired thermodynamic engineer</p>
|
|
259
126
|
|
|
260
127
|
## License
|
|
261
128
|
|
|
262
|
-
MIT
|
|
263
|
-
|
|
264
|
-
## Disclaimer
|
|
265
|
-
|
|
266
|
-
This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from the use of this software.
|
|
129
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FormulationSeamResolution } from '../core/formulation-seams.js';
|
|
2
|
+
import { type BasicProperties } from '../types.js';
|
|
3
|
+
import { type BackwardConstraint } from './solution-validation.js';
|
|
4
|
+
/** Exhaust nearby roots before applying inverse seam and saturation ownership. */
|
|
5
|
+
export declare function resolveBackwardCandidates(attempts: readonly (() => BasicProperties | null)[], constraints: readonly BackwardConstraint[], options: {
|
|
6
|
+
solverName: string;
|
|
7
|
+
seam?: FormulationSeamResolution;
|
|
8
|
+
}): BasicProperties | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NumericalStateError, RegionMismatchError } from '../errors.js';
|
|
2
|
+
import { IF97Error, Region } from '../types.js';
|
|
3
|
+
import { backwardCandidateError, normalizeBackwardState, validateBackwardState, } from './solution-validation.js';
|
|
4
|
+
/** Exhaust nearby roots before applying inverse seam and saturation ownership. */
|
|
5
|
+
export function resolveBackwardCandidates(attempts, constraints, options) {
|
|
6
|
+
const candidates = [];
|
|
7
|
+
let failure = null;
|
|
8
|
+
for (const attempt of attempts) {
|
|
9
|
+
try {
|
|
10
|
+
let state = attempt();
|
|
11
|
+
if (state !== null) {
|
|
12
|
+
let error = backwardCandidateError(state, constraints, options);
|
|
13
|
+
if (error instanceof RegionMismatchError) {
|
|
14
|
+
state = normalizeBackwardState(state, constraints, options.solverName);
|
|
15
|
+
error = backwardCandidateError(state, constraints, options);
|
|
16
|
+
}
|
|
17
|
+
if (error === null)
|
|
18
|
+
candidates.push(state);
|
|
19
|
+
else if (error instanceof NumericalStateError)
|
|
20
|
+
failure ??= error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!(error instanceof IF97Error))
|
|
25
|
+
throw error;
|
|
26
|
+
if (!(error instanceof RegionMismatchError))
|
|
27
|
+
failure ??= error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const saturated = candidates.find(({ region }) => region === Region.Region4);
|
|
31
|
+
const owned = candidates.find(({ region }) => region === options.seam?.pair.canonical.region);
|
|
32
|
+
if (candidates.length > 0)
|
|
33
|
+
return saturated ?? owned ?? candidates[0];
|
|
34
|
+
if (failure !== null)
|
|
35
|
+
throw failure;
|
|
36
|
+
if (options.seam !== undefined && options.seam.kind === 'gap') {
|
|
37
|
+
return validateBackwardState(options.seam.state, constraints, options);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BasicProperties } from '../types.js';
|
|
2
|
+
import { type FixedTemperatureProperty } from './fixed-temperature-solver.js';
|
|
3
|
+
/** Return the only physical TH/TS state, or null if unresolved; proven envelope violations throw. */
|
|
4
|
+
export declare function uniqueFixedTemperatureState(T: number, target: number, property: FixedTemperatureProperty): BasicProperties | null;
|
|
@@ -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
|
+
}
|