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.
Files changed (109) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +78 -215
  3. package/dist/backward/candidate-resolution.d.ts +8 -0
  4. package/dist/backward/candidate-resolution.js +40 -0
  5. package/dist/backward/fixed-temperature-candidates.d.ts +4 -0
  6. package/dist/backward/fixed-temperature-candidates.js +188 -0
  7. package/dist/backward/fixed-temperature-solver.d.ts +6 -6
  8. package/dist/backward/fixed-temperature-solver.js +56 -9
  9. package/dist/backward/fixed-temperature-stationary.d.ts +4 -0
  10. package/dist/backward/fixed-temperature-stationary.js +146 -0
  11. package/dist/backward/hs-region.d.ts +50 -0
  12. package/dist/backward/hs-region.js +695 -0
  13. package/dist/backward/hs-state.d.ts +15 -0
  14. package/dist/backward/hs-state.js +316 -0
  15. package/dist/backward/hs.d.ts +1 -2
  16. package/dist/backward/hs.js +28 -232
  17. package/dist/backward/ph.d.ts +8 -3
  18. package/dist/backward/ph.js +102 -54
  19. package/dist/backward/pressure-property-region.d.ts +13 -0
  20. package/dist/backward/pressure-property-region.js +262 -0
  21. package/dist/backward/ps.d.ts +6 -1
  22. package/dist/backward/ps.js +103 -38
  23. package/dist/backward/solution-validation.d.ts +7 -2
  24. package/dist/backward/solution-validation.js +162 -37
  25. package/dist/backward/th.d.ts +3 -6
  26. package/dist/backward/th.js +10 -73
  27. package/dist/backward/tolerances.d.ts +3 -1
  28. package/dist/backward/tolerances.js +9 -4
  29. package/dist/backward/ts.d.ts +3 -6
  30. package/dist/backward/ts.js +10 -73
  31. package/dist/boundaries.d.ts +0 -1
  32. package/dist/boundaries.js +0 -1
  33. package/dist/constants.d.ts +11 -24
  34. package/dist/constants.js +17 -28
  35. package/dist/core/floating-point.d.ts +2 -0
  36. package/dist/core/floating-point.js +7 -0
  37. package/dist/core/formulation-seams.d.ts +27 -0
  38. package/dist/core/formulation-seams.js +289 -0
  39. package/dist/core/input-validation.d.ts +1 -1
  40. package/dist/core/input-validation.js +5 -1
  41. package/dist/core/region-detector.d.ts +3 -41
  42. package/dist/core/region-detector.js +39 -597
  43. package/dist/core/region3-pt.d.ts +12 -1
  44. package/dist/core/region3-pt.js +68 -13
  45. package/dist/core/solve-input-normalization.d.ts +0 -1
  46. package/dist/core/solve-input-normalization.js +4 -2
  47. package/dist/core/solver.d.ts +7 -11
  48. package/dist/core/solver.js +26 -18
  49. package/dist/core/thermodynamic-derivatives.d.ts +17 -0
  50. package/dist/core/thermodynamic-derivatives.js +27 -0
  51. package/dist/detect.d.ts +16 -3
  52. package/dist/detect.js +36 -3
  53. package/dist/errors.d.ts +11 -0
  54. package/dist/errors.js +16 -0
  55. package/dist/gas-constant.d.ts +5 -0
  56. package/dist/gas-constant.js +5 -0
  57. package/dist/index.d.ts +4 -6
  58. package/dist/index.js +10 -22
  59. package/dist/regions/boundaries.d.ts +0 -1
  60. package/dist/regions/boundaries.js +0 -1
  61. package/dist/regions/region1.d.ts +2 -2
  62. package/dist/regions/region1.js +1 -2
  63. package/dist/regions/region2.d.ts +2 -2
  64. package/dist/regions/region2.js +1 -2
  65. package/dist/regions/region3-data.d.ts +0 -1
  66. package/dist/regions/region3-data.js +3 -4
  67. package/dist/regions/region3-eval.d.ts +0 -1
  68. package/dist/regions/region3-eval.js +0 -1
  69. package/dist/regions/region3-subregions.d.ts +0 -1
  70. package/dist/regions/region3-subregions.js +15 -11
  71. package/dist/regions/region3.d.ts +4 -2
  72. package/dist/regions/region3.js +32 -3
  73. package/dist/regions/region4.d.ts +2 -1
  74. package/dist/regions/region4.js +39 -13
  75. package/dist/regions/region5.d.ts +2 -2
  76. package/dist/regions/region5.js +2 -3
  77. package/dist/regions.d.ts +0 -1
  78. package/dist/regions.js +0 -1
  79. package/dist/saturation/common.d.ts +2 -9
  80. package/dist/saturation/common.js +66 -45
  81. package/dist/saturation/region4-boundaries.d.ts +0 -5
  82. package/dist/saturation/region4-boundaries.js +17 -18
  83. package/dist/saturation/region4-hs.d.ts +2 -1
  84. package/dist/saturation/region4-hs.js +499 -106
  85. package/dist/saturation/two-phase.d.ts +2 -3
  86. package/dist/saturation/two-phase.js +7 -8
  87. package/dist/saturation.d.ts +0 -1
  88. package/dist/saturation.js +0 -1
  89. package/dist/solvers/bracketed-newton.d.ts +10 -6
  90. package/dist/solvers/bracketed-newton.js +19 -13
  91. package/dist/solvers/damped-newton-2d.d.ts +12 -0
  92. package/dist/solvers/damped-newton-2d.js +68 -0
  93. package/dist/solvers/extended-arithmetic.d.ts +6 -0
  94. package/dist/solvers/extended-arithmetic.js +38 -0
  95. package/dist/solvers/newton-raphson.d.ts +0 -1
  96. package/dist/solvers/newton-raphson.js +0 -1
  97. package/dist/transport/properties.d.ts +4 -3
  98. package/dist/transport/properties.js +14 -5
  99. package/dist/transport.d.ts +0 -1
  100. package/dist/transport.js +0 -1
  101. package/dist/types.d.ts +18 -1
  102. package/dist/types.js +14 -1
  103. package/package.json +18 -10
  104. package/dist/backward/objective-normalization.d.ts +0 -7
  105. package/dist/backward/objective-normalization.js +0 -8
  106. package/dist/core/public-normalization.d.ts +0 -4
  107. package/dist/core/public-normalization.js +0 -54
  108. package/dist/solvers/nelder-mead.d.ts +0 -17
  109. package/dist/solvers/nelder-mead.js +0 -116
package/CHANGELOG.md CHANGED
@@ -1,6 +1,37 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
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
- # `iapws-if97`
1
+ # iapws-if97
2
2
 
3
- Calculate industrial water and steam properties in Node.js and TypeScript, based on the [IAPWS-IF97](https://www.iapws.org/) standard (International Association for the Properties of Water and Steam — Industrial Formulation 1997).
3
+ **Industrial water and steam properties for Node.js and TypeScript.**
4
4
 
5
- This library provides forward and backward state solvers, saturation solvers, and transport-property helpers for engineering calculations involving water and steam.
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
- ## Features
7
+ [![CI](https://github.com/jltonghui/iapws-if97/actions/workflows/ci.yml/badge.svg)](https://github.com/jltonghui/iapws-if97/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/iapws-if97.svg)](https://www.npmjs.com/package/iapws-if97)
8
9
 
9
- - Covers IF97 Regions 1–5, including high-temperature Region 5
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
- ## Project Origin
12
+ ![IAPWS-IF97 pressure–temperature map showing Regions 1–5, the saturation curve, and the 100 MPa and 50 MPa pressure limits.](https://raw.githubusercontent.com/jltonghui/iapws-if97/main/assets/pics/if97-regions.svg)
15
13
 
16
- > `iapws-if97` is the core calculation engine behind the WeChat Mini Program "汽水计算器" (`wxid: wx7201fd1713b524e5`). Since its launch in 2019, it has served nearly 20,000 users. With the help of AI, this edition has been updated to incorporate `R11-24 (2024)`. I am open-sourcing it in the hope that it will help others with similar engineering needs.
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
- <p align="right">-- Retired Thermodynamic Engineer, Shuping</p>
19
+ ## Start calculating
19
20
 
20
- ## Installation
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
- ## Quick Start
27
+ Save as `example.mjs` and run `node example.mjs`:
27
28
 
28
- ```ts
29
- import { solve, solvePT, solvePx } from 'iapws-if97';
29
+ ```js
30
+ import { solvePT } from 'iapws-if97';
30
31
 
31
- const a = solvePT(3, 300);
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(a.enthalpy); // kJ/kg
36
- console.log(b.temperature); // K
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
- ## Main API
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
- - `solvePT(p, T)` // `MPa`, `K`
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
- Use `solve({ mode, ... })` when the input pair is determined at runtime:
53
-
54
- ```ts
55
- import { solve } from 'iapws-if97';
56
-
57
- const state = solve({ mode: 'PT', p: 16, T: 823.15 });
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
- `solve` accepts both short-form and long-form property names:
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
- ```ts
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
- Supported modes:
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
- You can use either form for each property. If you provide both, their values must match.
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
- `SteamState` always uses canonical property names.
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
- - Thermodynamic properties follow IF97 validity ranges: up to `100 MPa` and `2273.15 K` (including Region 5).
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
- ## Verification
67
+ Specify quality to calculate saturated liquid, saturated vapor, or wet steam:
173
68
 
174
- The test suite covers:
69
+ ```js
70
+ import { solvePx } from 'iapws-if97';
175
71
 
176
- - Official IF97 verification tables (Regions 1–5)
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
- To run locally:
74
+ console.log(wet.temperature.toFixed(2)); // 453.04 K
185
75
 
186
- ```bash
187
- npm run build
188
- npm test
189
- npm run test:coverage
190
- npm run test:standards
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
- ## Integration Validation
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
- ![Mollier h-s diagram generated with iapws-if97](https://raw.githubusercontent.com/jltonghui/iapws-if97/main/assets/pics/mollier-hs-diagram.svg)
85
+ ## Before integrating
196
86
 
197
- This Mollier `h-s` diagram was generated with `iapws-if97` as a practical end-to-end validation artifact.
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
- ### ASME Validation Snapshot
200
-
201
- Compares saturation states against *ASME International Steam Tables for Industrial Use*, 3rd ed., Table S-2 ("Properties of Saturated Water and Steam — Pressure").
202
- Each case checks 7 values: saturation temperature, liquid/vapor specific volume, enthalpy, and entropy.
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
- ### GB/T Validation Snapshot
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
- Compares superheated-steam states against *GB/T 34060-2017*, Table A.3(续), page 55.
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
- | GB/T case | Pressure (MPa) | Temperature (°C) | Points checked | Max relative error |
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
- The current GB/T regression threshold is `maxRelativeError < 5e-4` (less than `0.05%`).
103
+ ```bash
104
+ npm ci
105
+ npm run typecheck
106
+ npm test
107
+ npm run test:package
108
+ ```
234
109
 
235
- ## TypeScript
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
- The package ships with bundled TypeScript declarations and ESM exports.
112
+ ## References
238
113
 
239
- ```ts
240
- import type { SolveInput, SteamState } from 'iapws-if97';
241
- import { Region, solve } from 'iapws-if97';
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
- const input: SolveInput = { mode: 'PT', p: 3, T: 300 };
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
- if (state.region === Region.Region1) {
247
- console.log('compressed liquid');
248
- }
249
- ```
121
+ ## Project origin
250
122
 
251
- ## References
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
- - [IAPWS official website](https://www.iapws.org/)
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
+ }