iapws-if97 2.1.2 → 2.2.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 (167) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +134 -94
  3. package/dist/backward/fixed-temperature-solver.d.ts +6 -6
  4. package/dist/backward/fixed-temperature-solver.js +34 -8
  5. package/dist/backward/hs.d.ts +1 -2
  6. package/dist/backward/hs.js +67 -46
  7. package/dist/backward/ph.d.ts +8 -5
  8. package/dist/backward/ph.js +43 -46
  9. package/dist/backward/ps.d.ts +0 -1
  10. package/dist/backward/ps.js +30 -18
  11. package/dist/backward/solution-validation.d.ts +0 -1
  12. package/dist/backward/solution-validation.js +33 -5
  13. package/dist/backward/th.d.ts +0 -1
  14. package/dist/backward/th.js +4 -6
  15. package/dist/backward/thermodynamic-derivatives.d.ts +17 -0
  16. package/dist/backward/thermodynamic-derivatives.js +27 -0
  17. package/dist/backward/tolerances.d.ts +0 -1
  18. package/dist/backward/tolerances.js +0 -1
  19. package/dist/backward/ts.d.ts +0 -1
  20. package/dist/backward/ts.js +4 -6
  21. package/dist/boundaries.d.ts +0 -1
  22. package/dist/boundaries.js +0 -1
  23. package/dist/constants.d.ts +0 -9
  24. package/dist/constants.js +3 -13
  25. package/dist/core/input-validation.d.ts +0 -1
  26. package/dist/core/input-validation.js +0 -1
  27. package/dist/core/public-normalization.d.ts +0 -1
  28. package/dist/core/public-normalization.js +0 -1
  29. package/dist/core/region-detector.d.ts +0 -1
  30. package/dist/core/region-detector.js +234 -67
  31. package/dist/core/region3-pt.d.ts +12 -1
  32. package/dist/core/region3-pt.js +38 -9
  33. package/dist/core/solve-input-normalization.d.ts +0 -1
  34. package/dist/core/solve-input-normalization.js +0 -1
  35. package/dist/core/solver.d.ts +0 -1
  36. package/dist/core/solver.js +0 -1
  37. package/dist/detect.d.ts +0 -1
  38. package/dist/detect.js +0 -1
  39. package/dist/index.d.ts +0 -1
  40. package/dist/index.js +0 -1
  41. package/dist/regions/boundaries.d.ts +0 -1
  42. package/dist/regions/boundaries.js +0 -1
  43. package/dist/regions/region1.d.ts +0 -1
  44. package/dist/regions/region1.js +0 -1
  45. package/dist/regions/region2.d.ts +0 -1
  46. package/dist/regions/region2.js +0 -1
  47. package/dist/regions/region3-data.d.ts +0 -1
  48. package/dist/regions/region3-data.js +0 -1
  49. package/dist/regions/region3-eval.d.ts +0 -1
  50. package/dist/regions/region3-eval.js +0 -1
  51. package/dist/regions/region3-subregions.d.ts +0 -1
  52. package/dist/regions/region3-subregions.js +0 -1
  53. package/dist/regions/region3.d.ts +0 -1
  54. package/dist/regions/region3.js +0 -1
  55. package/dist/regions/region4.d.ts +0 -1
  56. package/dist/regions/region4.js +0 -1
  57. package/dist/regions/region5.d.ts +0 -1
  58. package/dist/regions/region5.js +0 -1
  59. package/dist/regions.d.ts +0 -1
  60. package/dist/regions.js +0 -1
  61. package/dist/saturation/common.d.ts +0 -1
  62. package/dist/saturation/common.js +6 -13
  63. package/dist/saturation/region4-boundaries.d.ts +0 -5
  64. package/dist/saturation/region4-boundaries.js +4 -5
  65. package/dist/saturation/region4-hs.d.ts +2 -1
  66. package/dist/saturation/region4-hs.js +277 -108
  67. package/dist/saturation/two-phase.d.ts +0 -1
  68. package/dist/saturation/two-phase.js +0 -1
  69. package/dist/saturation.d.ts +0 -1
  70. package/dist/saturation.js +0 -1
  71. package/dist/solvers/bracketed-newton.d.ts +5 -4
  72. package/dist/solvers/bracketed-newton.js +5 -6
  73. package/dist/solvers/damped-newton-2d.d.ts +12 -0
  74. package/dist/solvers/damped-newton-2d.js +68 -0
  75. package/dist/solvers/newton-raphson.d.ts +0 -1
  76. package/dist/solvers/newton-raphson.js +0 -1
  77. package/dist/transport/properties.d.ts +0 -1
  78. package/dist/transport/properties.js +0 -1
  79. package/dist/transport.d.ts +0 -1
  80. package/dist/transport.js +0 -1
  81. package/dist/types.d.ts +0 -1
  82. package/dist/types.js +0 -1
  83. package/package.json +19 -10
  84. package/dist/backward/fixed-temperature-solver.d.ts.map +0 -1
  85. package/dist/backward/fixed-temperature-solver.js.map +0 -1
  86. package/dist/backward/hs.d.ts.map +0 -1
  87. package/dist/backward/hs.js.map +0 -1
  88. package/dist/backward/objective-normalization.d.ts +0 -7
  89. package/dist/backward/objective-normalization.d.ts.map +0 -1
  90. package/dist/backward/objective-normalization.js +0 -8
  91. package/dist/backward/objective-normalization.js.map +0 -1
  92. package/dist/backward/ph.d.ts.map +0 -1
  93. package/dist/backward/ph.js.map +0 -1
  94. package/dist/backward/ps.d.ts.map +0 -1
  95. package/dist/backward/ps.js.map +0 -1
  96. package/dist/backward/solution-validation.d.ts.map +0 -1
  97. package/dist/backward/solution-validation.js.map +0 -1
  98. package/dist/backward/th.d.ts.map +0 -1
  99. package/dist/backward/th.js.map +0 -1
  100. package/dist/backward/tolerances.d.ts.map +0 -1
  101. package/dist/backward/tolerances.js.map +0 -1
  102. package/dist/backward/ts.d.ts.map +0 -1
  103. package/dist/backward/ts.js.map +0 -1
  104. package/dist/boundaries.d.ts.map +0 -1
  105. package/dist/boundaries.js.map +0 -1
  106. package/dist/constants.d.ts.map +0 -1
  107. package/dist/constants.js.map +0 -1
  108. package/dist/core/input-validation.d.ts.map +0 -1
  109. package/dist/core/input-validation.js.map +0 -1
  110. package/dist/core/public-normalization.d.ts.map +0 -1
  111. package/dist/core/public-normalization.js.map +0 -1
  112. package/dist/core/region-detector.d.ts.map +0 -1
  113. package/dist/core/region-detector.js.map +0 -1
  114. package/dist/core/region3-pt.d.ts.map +0 -1
  115. package/dist/core/region3-pt.js.map +0 -1
  116. package/dist/core/solve-input-normalization.d.ts.map +0 -1
  117. package/dist/core/solve-input-normalization.js.map +0 -1
  118. package/dist/core/solver.d.ts.map +0 -1
  119. package/dist/core/solver.js.map +0 -1
  120. package/dist/detect.d.ts.map +0 -1
  121. package/dist/detect.js.map +0 -1
  122. package/dist/index.d.ts.map +0 -1
  123. package/dist/index.js.map +0 -1
  124. package/dist/regions/boundaries.d.ts.map +0 -1
  125. package/dist/regions/boundaries.js.map +0 -1
  126. package/dist/regions/region1.d.ts.map +0 -1
  127. package/dist/regions/region1.js.map +0 -1
  128. package/dist/regions/region2.d.ts.map +0 -1
  129. package/dist/regions/region2.js.map +0 -1
  130. package/dist/regions/region3-data.d.ts.map +0 -1
  131. package/dist/regions/region3-data.js.map +0 -1
  132. package/dist/regions/region3-eval.d.ts.map +0 -1
  133. package/dist/regions/region3-eval.js.map +0 -1
  134. package/dist/regions/region3-subregions.d.ts.map +0 -1
  135. package/dist/regions/region3-subregions.js.map +0 -1
  136. package/dist/regions/region3.d.ts.map +0 -1
  137. package/dist/regions/region3.js.map +0 -1
  138. package/dist/regions/region4.d.ts.map +0 -1
  139. package/dist/regions/region4.js.map +0 -1
  140. package/dist/regions/region5.d.ts.map +0 -1
  141. package/dist/regions/region5.js.map +0 -1
  142. package/dist/regions.d.ts.map +0 -1
  143. package/dist/regions.js.map +0 -1
  144. package/dist/saturation/common.d.ts.map +0 -1
  145. package/dist/saturation/common.js.map +0 -1
  146. package/dist/saturation/region4-boundaries.d.ts.map +0 -1
  147. package/dist/saturation/region4-boundaries.js.map +0 -1
  148. package/dist/saturation/region4-hs.d.ts.map +0 -1
  149. package/dist/saturation/region4-hs.js.map +0 -1
  150. package/dist/saturation/two-phase.d.ts.map +0 -1
  151. package/dist/saturation/two-phase.js.map +0 -1
  152. package/dist/saturation.d.ts.map +0 -1
  153. package/dist/saturation.js.map +0 -1
  154. package/dist/solvers/bracketed-newton.d.ts.map +0 -1
  155. package/dist/solvers/bracketed-newton.js.map +0 -1
  156. package/dist/solvers/nelder-mead.d.ts +0 -17
  157. package/dist/solvers/nelder-mead.d.ts.map +0 -1
  158. package/dist/solvers/nelder-mead.js +0 -116
  159. package/dist/solvers/nelder-mead.js.map +0 -1
  160. package/dist/solvers/newton-raphson.d.ts.map +0 -1
  161. package/dist/solvers/newton-raphson.js.map +0 -1
  162. package/dist/transport/properties.d.ts.map +0 -1
  163. package/dist/transport/properties.js.map +0 -1
  164. package/dist/transport.d.ts.map +0 -1
  165. package/dist/transport.js.map +0 -1
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 2.2.0
6
+
7
+ ### Fixed
8
+
9
+ - Improved backward-solver accuracy, robustness, and performance.
10
+ - Fixed saturation-region detection and package validation.
11
+ - Requires Node.js `>=20.19.0`.
12
+
13
+ ## 2.1.5
14
+
15
+ - fix some issues
16
+
5
17
  ## 2.1.2
6
18
 
7
19
  - fix some issues
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
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
+ [![CI](https://github.com/jltonghui/iapws-if97/actions/workflows/ci.yml/badge.svg)](https://github.com/jltonghui/iapws-if97/actions/workflows/ci.yml)
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
+ `iapws-if97` calculates industrial water and steam properties in Node.js and TypeScript using the [IAPWS-IF97](https://www.iapws.org/) standard published by the International Association for the Properties of Water and Steam.
6
+
7
+ It includes forward and backward state solvers, saturation solvers, and transport-property helpers.
6
8
 
7
9
  ## Features
8
10
 
@@ -11,21 +13,19 @@ This library provides forward and backward state solvers, saturation solvers, an
11
13
  - Transport properties: viscosity, thermal conductivity, surface tension, dielectric constant, and ionization constant
12
14
  - Verified against official IAPWS tables and published engineering references
13
15
 
14
- ## Project Origin
15
-
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.
17
-
18
- <p align="right">-- Retired Thermodynamic Engineer, Shuping</p>
19
-
20
16
  ## Installation
21
17
 
18
+ Requires Node.js `>=20.19.0`. The package publishes ESM, and all documented examples use `import`. CommonJS is not part of the tested API.
19
+
22
20
  ```bash
23
21
  npm install iapws-if97
24
22
  ```
25
23
 
26
- ## Quick Start
24
+ ## Quick start
27
25
 
28
- ```ts
26
+ Save the following as `example.mjs` and run it with `node example.mjs`:
27
+
28
+ ```js
29
29
  import { solve, solvePT, solvePx } from 'iapws-if97';
30
30
 
31
31
  const a = solvePT(3, 300);
@@ -39,15 +39,20 @@ console.log(c.quality); // 0.5
39
39
 
40
40
  ## Main API
41
41
 
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`
42
+ | Function | Input pair | Units |
43
+ | --- | --- | --- |
44
+ | `solvePT(p, T)` | pressure, temperature | `MPa`, `K` |
45
+ | `solvePH(p, h)` | pressure, enthalpy | `MPa`, `kJ/kg` |
46
+ | `solvePS(p, s)` | pressure, entropy | `MPa`, `kJ/(kg·K)` |
47
+ | `solveHS(h, s)` | enthalpy, entropy | `kJ/kg`, `kJ/(kg·K)` |
48
+ | `solveTH(T, h)` | temperature, enthalpy | `K`, `kJ/kg` |
49
+ | `solveTS(T, s)` | temperature, entropy | `K`, `kJ/(kg·K)` |
50
+ | `solvePx(p, x)` | pressure, vapor quality | `MPa`, dimensionless `[0, 1]` |
51
+ | `solveTx(T, x)` | temperature, vapor quality | `K`, dimensionless `[0, 1]` |
52
+
53
+ All numeric inputs must be finite numbers.
54
+
55
+ ### Unified `solve()`
51
56
 
52
57
  Use `solve({ mode, ... })` when the input pair is determined at runtime:
53
58
 
@@ -57,30 +62,33 @@ import { solve } from 'iapws-if97';
57
62
  const state = solve({ mode: 'PT', p: 16, T: 823.15 });
58
63
  ```
59
64
 
60
- `solve` accepts both short-form and long-form property names:
65
+ Each mode accepts canonical short fields or their long aliases:
66
+
67
+ | Mode | Short fields | Long aliases |
68
+ | --- | --- | --- |
69
+ | `PT` | `p`, `T` | `pressure`, `temperature` |
70
+ | `PH` | `p`, `h` | `pressure`, `enthalpy` |
71
+ | `PS` | `p`, `s` | `pressure`, `entropy` |
72
+ | `HS` | `h`, `s` | `enthalpy`, `entropy` |
73
+ | `Px` | `p`, `x` | `pressure`, `quality` |
74
+ | `Tx` | `T`, `x` | `temperature`, `quality` |
75
+ | `TH` | `T`, `h` | `temperature`, `enthalpy` |
76
+ | `TS` | `T`, `s` | `temperature`, `entropy` |
61
77
 
62
78
  ```ts
63
- const a = solve({ mode: 'PT', p: 16, T: 823.15 });
64
- const b = solve({ mode: 'PT', pressure: 16, temperature: 823.15 });
65
- ```
79
+ import type { SolveInput } from 'iapws-if97';
80
+ import { solve } from 'iapws-if97';
66
81
 
67
- Supported modes:
82
+ const shortForm: SolveInput = { mode: 'PT', p: 16, T: 823.15 };
83
+ const longForm: SolveInput = { mode: 'PT', pressure: 16, temperature: 823.15 };
68
84
 
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 };
85
+ const a = solve(shortForm);
86
+ const b = solve(longForm);
79
87
  ```
80
88
 
81
- You can use either form for each property. If you provide both, their values must match.
89
+ You can mix short and long names within one input. If both aliases for the same property are present, their values must match exactly. The exported `SolveInput` type is the source of truth for accepted combinations.
82
90
 
83
- ## Solver Return Value
91
+ ## Solver return value
84
92
 
85
93
  All solvers return a `SteamState` object containing both thermodynamic and transport properties:
86
94
 
@@ -108,16 +116,16 @@ type SteamState = {
108
116
  };
109
117
  ```
110
118
 
111
- `SteamState` always uses canonical property names.
119
+ `SteamState` always uses canonical property names.
112
120
 
113
121
  **Notes:**
114
122
 
115
- - `quality` is `null` for single-phase states — it is only defined on the saturation line.
123
+ - `quality` is `null` for single-phase states; it is only defined on the saturation line.
116
124
  - In two-phase mixtures (`0 < x < 1`), `cp`, `cv`, `speedOfSound`, `isobaricExpansion`, `isothermalCompressibility`, `viscosity`, `thermalConductivity`, `dielectricConstant`, and `ionizationConstant` are `null`.
117
125
  - Saturation endpoints (`x = 0` or `x = 1`) still expose single-phase transport properties even though they carry Region 4 metadata.
118
126
  - `solvePT(p, T)` is a single-phase solver. On the subcritical saturation boundary it resolves to the liquid side.
119
127
  - `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`.
128
+ - `density` is provided directly, so there is no need to invert `specificVolume`.
121
129
  - `ionizationConstant` is `null` outside the IAPWS validity range for that correlation.
122
130
 
123
131
  ## Units
@@ -140,19 +148,59 @@ type SteamState = {
140
148
 
141
149
  Any field typed as `number | null` returns `null` when the property is undefined for the given state.
142
150
 
143
- ## Saturation Endpoint
151
+ ## Advanced imports
152
+
153
+ The package root is limited to the main solvers, `SteamState`, `SolveInput`, `Region`, and the public error classes. Lower-level helpers are available from explicit subpaths:
154
+
155
+ | Subpath | Exports |
156
+ | --- | --- |
157
+ | `iapws-if97/transport` | `viscosity`, `thermalConductivity`, `surfaceTension`, `dielectricConstant`, `ionizationConstant` |
158
+ | `iapws-if97/regions` | `region1`, `region2`, `region3ByRhoT`, `region5` |
159
+ | `iapws-if97/saturation` | `saturationPressure`, `saturationTemperature` |
160
+ | `iapws-if97/boundaries` | `boundary23_T_to_P`, `boundary23_P_to_T`, `region3Volume`, `region3SatVolume` |
161
+ | `iapws-if97/detect` | `detectRegionPT`, `detectRegionPH`, `detectRegionPS`, `detectRegionHS`, `detectRegionTH`, `detectRegionTS` |
162
+
163
+ ```ts
164
+ import { viscosity } from 'iapws-if97/transport';
165
+ import { region1 } from 'iapws-if97/regions';
166
+ import { saturationTemperature } from 'iapws-if97/saturation';
167
+ import { detectRegionPT } from 'iapws-if97/detect';
168
+ ```
169
+
170
+ These are low-level mathematical interfaces:
171
+
172
+ - Region functions return core thermodynamic properties without `density` or transport-property enrichment.
173
+ - Region, boundary, and detection helpers expect callers to respect the corresponding equation domains. Detection helpers return a `Region` value or `-1` when no valid region is found.
174
+ - `thermalConductivity(T, rho)` calculates the base contribution. Pass `cp`, `cv`, `drhodP_T`, and `mu` to include the IAPWS 2011 critical-enhancement term.
175
+ - `surfaceTension(T)` is a saturation-line property. `saturationPressure(T)` and `saturationTemperature(p)` expose the mathematical Region 4 boundary and are more permissive at endpoints than `solveTx` and `solvePx`.
144
176
 
145
- - Low-level Region 4 helpers stay mathematically permissive and high-level saturation state solvers are stricter:
177
+ ## Transport correlation limits
178
+
179
+ Transport correlations have validity ranges independent of the IF97 thermodynamic envelope. The library may return extrapolated values where noted:
180
+
181
+ | Property | Implemented behavior |
182
+ | --- | --- |
183
+ | Viscosity | Uses the IAPWS 2008 dilute-gas and finite-density terms. The near-critical enhancement is omitted, as permitted for industrial use by the release. |
184
+ | Thermal conductivity | Full `SteamState` calculations include the IAPWS 2011 critical enhancement. The two-argument low-level call does not. Values above the release's temperature range are extrapolations. |
185
+ | Surface tension | Defined for saturation states below the critical point. The low-level helper permits `273.15 K ≤ T ≤ Tc`; values below the triple point `Tt = 273.16 K` are extrapolations. |
186
+ | Dielectric constant | The IAPWS 1997 release is valid through `873 K`. The low-level helper does not enforce that upper bound, so higher-temperature results are extrapolations. |
187
+ | Ionization constant (`pKw`) | Returns `null` outside `273.15 K ≤ T ≤ 1273.15 K`. The release's `1000 MPa` pressure limit describes the correlation, not the state-solver pressure range. |
188
+
189
+ ## Saturation endpoints
190
+
191
+ The triple and critical points are `Pt = 0.000611657 MPa`, `Tt = 273.16 K`, `Pc = 22.064 MPa`, and `Tc = 647.096 K`.
192
+
193
+ - Low-level Region 4 helpers accept a wider endpoint range. The saturation state solvers use these stricter bounds:
146
194
  - `solvePx(p, x)` accepts `Pt ≤ p < Pc`
147
195
  - `solveTx(T, x)` accepts `Tt = 273.16 K ≤ T < Tc`
148
- - The true triple point is supported as a saturation-state boundary:
196
+ - The triple point is supported as a saturation-state boundary:
149
197
  - `solvePx(Pt, x)` is valid
150
198
  - `solveTx(Tt, x)` is valid
151
199
  - `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.
200
+ - The solvers do not return the exact critical point as a Region 4 state.
201
+ - `solveTH(T, h)` and `solveTS(T, s)` reject inputs within `0.001 K` of the critical temperature.
154
202
 
155
- ## Errors and Limits
203
+ ## Errors and limits
156
204
 
157
205
  The library throws typed errors:
158
206
 
@@ -160,14 +208,18 @@ The library throws typed errors:
160
208
  | --- | --- |
161
209
  | `OutOfRangeError` | Input is outside the supported IF97 range |
162
210
  | `ConvergenceError` | An internal iterative solve failed to converge |
163
- | `IF97Error` | Base class for all library-specific errors |
211
+ | `IF97Error` | Base class; root solvers also use it directly for non-numeric or non-finite inputs, conflicting aliases, unsupported modes, and unsupported critical-point states |
164
212
 
165
- ## Validity Notes
213
+ Numeric inputs to root solvers and transport helpers must be finite. Low-level region, boundary, and detection helpers rely on callers to respect their equation domains. `solvePT(Pc, Tc)`, exact critical Region 4 states, and `solveTH`/`solveTS` inputs within `0.001 K` of `Tc` are rejected because the required derivative properties or inverse solution are singular or ill-conditioned.
166
214
 
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`.
215
+ ## Thermodynamic validity
216
+
217
+ The thermodynamic solvers follow the piecewise IF97 industrial range:
218
+
219
+ - `273.15 K ≤ T ≤ 1073.15 K`: pressure up to `100 MPa`.
220
+ - `1073.15 K < T ≤ 2273.15 K` (Region 5): pressure up to `50 MPa`.
221
+
222
+ Transport-property limits are separate; see [Transport correlation limits](#transport-correlation-limits).
171
223
 
172
224
  ## Verification
173
225
 
@@ -181,60 +233,39 @@ The test suite covers:
181
233
  - Coverage thresholds enforced locally
182
234
  - ASME and GB/T steam-table comparisons against published values
183
235
 
184
- To run locally:
236
+ For a clean local verification run:
185
237
 
186
238
  ```bash
187
- npm run build
239
+ npm ci
240
+ npm run test:package
188
241
  npm test
242
+ ```
243
+
244
+ `test:package` performs a clean build and checks that the dry-run npm tarball has no dangling source-map references. Coverage and published-table comparisons are available separately:
245
+
246
+ ```bash
189
247
  npm run test:coverage
190
248
  npm run test:standards
191
249
  ```
192
250
 
193
- ## Integration Validation
251
+ ## Integration validation
194
252
 
195
253
  ![Mollier h-s diagram generated with iapws-if97](https://raw.githubusercontent.com/jltonghui/iapws-if97/main/assets/pics/mollier-hs-diagram.svg)
196
254
 
197
- This Mollier `h-s` diagram was generated with `iapws-if97` as a practical end-to-end validation artifact.
198
-
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% |
255
+ This Mollier `h-s` diagram was generated with `iapws-if97` as an end-to-end check of the calculation pipeline.
214
256
 
215
- The current ASME regression threshold is `maxRelativeError < 5e-4` (less than `0.05%`).
257
+ The standards tests check the published tables below. Both suites enforce `maxRelativeError < 5e-4` (less than `0.05%`).
216
258
 
217
- ### GB/T Validation Snapshot
259
+ | Reference | Coverage | Points checked | Worst relative error |
260
+ | --- | --- | ---: | ---: |
261
+ | *ASME International Steam Tables for Industrial Use*, 3rd ed., Table S-2 | Saturation states at 6 pressures | 42 | `0.0315405%` |
262
+ | *GB/T 34060-2017*, Table A.3(续), page 55 | Superheated steam at `10 MPa` and 6 temperatures | 24 | `0.0007020%` |
218
263
 
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.
222
-
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% |
232
-
233
- The current GB/T regression threshold is `maxRelativeError < 5e-4` (less than `0.05%`).
264
+ See [`tests/standards-asme`](tests/standards-asme) and [`tests/standards-cn`](tests/standards-cn) for the source values and per-case comparisons.
234
265
 
235
266
  ## TypeScript
236
267
 
237
- The package ships with bundled TypeScript declarations and ESM exports.
268
+ The package ships with generated TypeScript declarations for the root and every public subpath.
238
269
 
239
270
  ```ts
240
271
  import type { SolveInput, SteamState } from 'iapws-if97';
@@ -251,11 +282,20 @@ if (state.region === Region.Region1) {
251
282
  ## References
252
283
 
253
284
  - [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
285
+ - [IAPWS R7-97(2012), Industrial Formulation 1997](https://www.iapws.org/relguide/IF97-Rev.html)
286
+ - [IAPWS supplementary releases](https://www.iapws.org/release.html) for `T(p,h)`, `T(p,s)`, `p(h,s)`, Region 3 boundary equations, `Tsat(h,s)`, and `v(p,T)`
287
+ - [IAPWS SR4-04(2014), Region 3 `p(h,s)` and Region 4 `Tsat(h,s)`](https://iapws.org/documents/release/Supp-phs3-2014)
288
+ - [IAPWS R12-08, Viscosity of Ordinary Water Substance](https://www.iapws.org/relguide/viscosity.html)
289
+ - [IAPWS R15-11, Thermal Conductivity of Ordinary Water Substance](https://www.iapws.org/relguide/ThCond.html)
290
+ - [IAPWS R8-97, Static Dielectric Constant of Ordinary Water Substance](https://www.iapws.org/relguide/dielec.pdf)
291
+ - [IAPWS R1-76(2014), Surface Tension of Ordinary Water Substance](https://www.iapws.org/relguide/Surf-H2O.html)
292
+ - [IAPWS R11-24, Ionization Constant of H2O](https://www.iapws.org/relguide/Ionization.html)
293
+
294
+ ## Project origin
295
+
296
+ > `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.
297
+
298
+ <p align="right">Shuping, retired thermodynamic engineer</p>
259
299
 
260
300
  ## License
261
301
 
@@ -5,21 +5,21 @@
5
5
  * (enthalpy or entropy) using a bracketed Newton method.
6
6
  */
7
7
  import type { BasicProperties } from '../types.js';
8
- type PropertyExtractor = (state: BasicProperties) => number;
8
+ /** Property being inverted against pressure at fixed temperature. */
9
+ export type FixedTemperatureProperty = 'enthalpy' | 'entropy';
9
10
  /**
10
11
  * Solve for pressure at fixed temperature by matching a target property value.
11
12
  *
12
13
  * Evaluates the region equation at the bracket endpoints first (short-circuit
13
- * if either already matches), then runs a bracketed Newton iteration.
14
+ * if either already matches), then runs a bracketed Newton iteration driven by
15
+ * the analytic pressure derivative.
14
16
  *
15
17
  * @param evaluator - Region-specific equation returning BasicProperties from (p, T)
16
18
  * @param T - Fixed temperature [K]
17
19
  * @param target - Target property value (enthalpy or entropy)
18
20
  * @param lower - Lower pressure bracket [MPa]
19
21
  * @param upper - Upper pressure bracket [MPa]
20
- * @param extract - Function to extract the target property from BasicProperties
22
+ * @param property - Which property `target` represents
21
23
  * @param solverName - Label used in error messages
22
24
  */
23
- export declare function solveFixedTemperaturePressure(evaluator: (p: number, T: number) => BasicProperties, T: number, target: number, lower: number, upper: number, extract: PropertyExtractor, solverName: string): BasicProperties;
24
- export {};
25
- //# sourceMappingURL=fixed-temperature-solver.d.ts.map
25
+ export declare function solveFixedTemperaturePressure(evaluator: (p: number, T: number) => BasicProperties, T: number, target: number, lower: number, upper: number, property: FixedTemperatureProperty, solverName: string): BasicProperties;
@@ -1,31 +1,57 @@
1
1
  import { IF97Error } from '../types.js';
2
2
  import { bracketedNewton } from '../solvers/bracketed-newton.js';
3
+ import { gibbsPropertyDerivatives } from './thermodynamic-derivatives.js';
4
+ function propertyValue(state, property) {
5
+ return property === 'enthalpy' ? state.enthalpy : state.entropy;
6
+ }
7
+ /**
8
+ * (∂h/∂p)_T = 1000·v(1 − Tα) or (∂s/∂p)_T = −1000·v·α, both per MPa.
9
+ *
10
+ * These are Maxwell-relation identities, so they hold for the Helmholtz-based
11
+ * Region 3 states as well as the Gibbs-based regions. Returns NaN when a state
12
+ * carries no cp/α, which makes the caller fall back to a bisection step.
13
+ */
14
+ function pressureDerivative(state, property) {
15
+ const derivatives = gibbsPropertyDerivatives(state);
16
+ return property === 'enthalpy' ? derivatives.dhDp : derivatives.dsDp;
17
+ }
3
18
  /**
4
19
  * Solve for pressure at fixed temperature by matching a target property value.
5
20
  *
6
21
  * Evaluates the region equation at the bracket endpoints first (short-circuit
7
- * if either already matches), then runs a bracketed Newton iteration.
22
+ * if either already matches), then runs a bracketed Newton iteration driven by
23
+ * the analytic pressure derivative.
8
24
  *
9
25
  * @param evaluator - Region-specific equation returning BasicProperties from (p, T)
10
26
  * @param T - Fixed temperature [K]
11
27
  * @param target - Target property value (enthalpy or entropy)
12
28
  * @param lower - Lower pressure bracket [MPa]
13
29
  * @param upper - Upper pressure bracket [MPa]
14
- * @param extract - Function to extract the target property from BasicProperties
30
+ * @param property - Which property `target` represents
15
31
  * @param solverName - Label used in error messages
16
32
  */
17
- export function solveFixedTemperaturePressure(evaluator, T, target, lower, upper, extract, solverName) {
33
+ export function solveFixedTemperaturePressure(evaluator, T, target, lower, upper, property, solverName) {
34
+ // Keyed by p, so the residual and its derivative at the same point share one
35
+ // equation evaluation regardless of the order the solver calls them in.
36
+ let cachedPressure = Number.NaN;
37
+ let cachedState = null;
38
+ const stateAt = (p) => {
39
+ if (p !== cachedPressure || cachedState === null) {
40
+ cachedPressure = p;
41
+ cachedState = evaluator(p, T);
42
+ }
43
+ return cachedState;
44
+ };
18
45
  const lowerState = evaluator(lower, T);
19
46
  const upperState = evaluator(upper, T);
20
- if (Math.abs(extract(lowerState) - target) <= 1e-9)
47
+ if (Math.abs(propertyValue(lowerState, property) - target) <= 1e-9)
21
48
  return lowerState;
22
- if (Math.abs(extract(upperState) - target) <= 1e-9)
49
+ if (Math.abs(propertyValue(upperState, property) - target) <= 1e-9)
23
50
  return upperState;
24
- const pressure = bracketedNewton((p) => extract(evaluator(p, T)) - target, lower, upper, (lower + upper) / 2);
25
- const state = evaluator(pressure, T);
51
+ const pressure = bracketedNewton((p) => propertyValue(stateAt(p), property) - target, lower, upper, (lower + upper) / 2, { derivative: (p) => pressureDerivative(stateAt(p), property) });
52
+ const state = stateAt(pressure);
26
53
  if (!Number.isFinite(state.pressure)) {
27
54
  throw new IF97Error(`${solverName} failed to recover a valid pressure`);
28
55
  }
29
56
  return state;
30
57
  }
31
- //# sourceMappingURL=fixed-temperature-solver.js.map
@@ -2,7 +2,7 @@
2
2
  * IAPWS-IF97 Backward Equations: H-S -> P, T
3
3
  *
4
4
  * Given specific enthalpy and entropy, compute pressure and temperature.
5
- * Uses backward P(h,s) equations for initial guess + Nelder-Mead refinement.
5
+ * Uses backward P(h,s) equations for initial guesses + damped Newton refinement.
6
6
  *
7
7
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations p(h,s)
8
8
  */
@@ -13,4 +13,3 @@ import type { BasicProperties } from '../types.js';
13
13
  * @param s - Specific entropy [kJ/(kg·K)]
14
14
  */
15
15
  export declare function solveHS(h: number, s: number): BasicProperties;
16
- //# sourceMappingURL=hs.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * IAPWS-IF97 Backward Equations: H-S -> P, T
3
3
  *
4
4
  * Given specific enthalpy and entropy, compute pressure and temperature.
5
- * Uses backward P(h,s) equations for initial guess + Nelder-Mead refinement.
5
+ * Uses backward P(h,s) equations for initial guesses + damped Newton refinement.
6
6
  *
7
7
  * Reference: IAPWS-IF97 Supplementary Release on Backward Equations p(h,s)
8
8
  */
@@ -14,12 +14,10 @@ import { region3ByRhoT } from '../regions/region3.js';
14
14
  import { region5 } from '../regions/region5.js';
15
15
  import { assertFiniteNumber } from '../core/input-validation.js';
16
16
  import { detectRegionHS } from '../core/region-detector.js';
17
- import { newtonRaphson } from '../solvers/newton-raphson.js';
18
- import { nelderMead } from '../solvers/nelder-mead.js';
19
- import { solvePH } from './ph.js';
17
+ import { dampedNewton2D } from '../solvers/damped-newton-2d.js';
18
+ import { r1BackwardT, r2BackwardT, r3BackwardPHInitial } from './ph.js';
20
19
  import { validateBackwardState } from './solution-validation.js';
21
- import { sumNormalizedResiduals } from './objective-normalization.js';
22
- import { backwardConstraintTolerance } from './tolerances.js';
20
+ import { gibbsPropertyDerivatives, helmholtzPropertyDerivatives, } from './thermodynamic-derivatives.js';
23
21
  import { tryRegion4HSState, } from '../saturation/region4-hs.js';
24
22
  import { assertCriticalRegion4HSInput, } from '../saturation/region4-boundaries.js';
25
23
  // ─── Backward Polynomial Evaluator ─────────────────────────────────────────
@@ -159,21 +157,52 @@ function r3Phs(h, s) {
159
157
  return r3aPhs(h, s);
160
158
  return r3bPhs(h, s);
161
159
  }
162
- const REGION5_HS_OUT_OF_DOMAIN_PENALTY = 1e18;
163
- function normalizedRegion5HsObjective(h, s) {
164
- const enthalpyTolerance = backwardConstraintTolerance('enthalpy', h);
165
- const entropyTolerance = backwardConstraintTolerance('entropy', s);
166
- return (pair) => {
167
- const [p, T] = pair;
168
- if (p <= 0 || p > C.R5_P_MAX || T <= C.R5_T_MIN || T > C.R5_T_MAX) {
169
- return REGION5_HS_OUT_OF_DOMAIN_PENALTY;
170
- }
171
- const state = region5(p, T);
172
- return sumNormalizedResiduals([
173
- { actual: state.enthalpy, expected: h, tolerance: enthalpyTolerance },
174
- { actual: state.entropy, expected: s, tolerance: entropyTolerance },
175
- ]);
176
- };
160
+ function solveGibbsHS(h, s, initial, stateAt) {
161
+ const hScale = Math.max(1, Math.abs(h));
162
+ const sScale = Math.max(1, Math.abs(s));
163
+ const [p, T] = dampedNewton2D(([candidateP, candidateT]) => {
164
+ const state = stateAt(candidateP, candidateT);
165
+ const derivatives = gibbsPropertyDerivatives(state);
166
+ return {
167
+ residual: [
168
+ (state.enthalpy - h) / hScale,
169
+ (state.entropy - s) / sScale,
170
+ ],
171
+ jacobian: [
172
+ [derivatives.dhDp / hScale, derivatives.dhDT / hScale],
173
+ [derivatives.dsDp / sScale, derivatives.dsDT / sScale],
174
+ ],
175
+ };
176
+ }, initial, {
177
+ isValid: ([candidateP, candidateT]) => candidateP > 0 && candidateT > 0,
178
+ });
179
+ return stateAt(p, T);
180
+ }
181
+ function solveRegion3HS(h, s, rho0, T0) {
182
+ const hScale = Math.max(1, Math.abs(h));
183
+ const sScale = Math.max(1, Math.abs(s));
184
+ const [rho, T] = dampedNewton2D(([candidateRho, candidateT]) => {
185
+ const state = region3ByRhoT(candidateRho, candidateT);
186
+ const derivatives = helmholtzPropertyDerivatives(state, candidateRho);
187
+ return {
188
+ residual: [
189
+ (state.enthalpy - h) / hScale,
190
+ (state.entropy - s) / sScale,
191
+ ],
192
+ jacobian: [
193
+ [derivatives.dhDrho / hScale, derivatives.dhDT / hScale],
194
+ [derivatives.dsDrho / sScale, derivatives.dsDT / sScale],
195
+ ],
196
+ };
197
+ }, [rho0, T0], {
198
+ // Tighter than the 1e-10 default and load-bearing: near the 100 MPa ceiling
199
+ // Region 3 is nearly incompressible, so a residual the default would accept
200
+ // still lands p far enough outside [P_MIN, P_MAX] to fail envelope
201
+ // validation. Covered by the p=100 MPa cases in newton-regressions.test.ts.
202
+ tolerance: 1e-12,
203
+ isValid: ([candidateRho, candidateT]) => candidateRho > 0 && candidateT > 0,
204
+ });
205
+ return region3ByRhoT(rho, T);
177
206
  }
178
207
  // ─── Main HS Solver ─────────────────────────────────────────────────────────
179
208
  /**
@@ -191,53 +220,46 @@ export function solveHS(h, s) {
191
220
  throw new OutOfRangeError('Entropy', s, C.S_MIN, C.S_MAX);
192
221
  }
193
222
  assertCriticalRegion4HSInput(h, s, 'solveHS');
194
- const region4State = tryRegion4HSState(h, s);
195
- if (region4State !== null) {
196
- return validateBackwardState(region4State, [
197
- { label: 'enthalpy', expected: h },
198
- { label: 'entropy', expected: s },
199
- ], { solverName: 'solveHS', expectedRegion: Region.Region4 });
200
- }
201
223
  const region = detectRegionHS(h, s);
202
224
  switch (region) {
203
225
  case Region.Region1: {
204
226
  const P0 = r1Phs(h, s);
205
- const sol = nelderMead((pair) => Math.abs(region1(pair[0], pair[1]).enthalpy - h) +
206
- Math.abs(region1(pair[0], pair[1]).entropy - s), [P0, 400]);
207
- return validateBackwardState(region1(sol.x[0], sol.x[1]), [
227
+ const state = solveGibbsHS(h, s, [P0, r1BackwardT(P0, h)], region1);
228
+ return validateBackwardState(state, [
208
229
  { label: 'enthalpy', expected: h },
209
230
  { label: 'entropy', expected: s },
210
231
  ], { solverName: 'solveHS', expectedRegion: Region.Region1 });
211
232
  }
212
233
  case Region.Region2: {
213
234
  const P0 = r2Phs(h, s);
214
- const T0 = newtonRaphson((T) => region2(P0, T).enthalpy - h, 500);
215
- const sol = nelderMead((pair) => Math.abs(region2(pair[0], pair[1]).enthalpy - h) +
216
- Math.abs(region2(pair[0], pair[1]).entropy - s), [P0, T0]);
217
- return validateBackwardState(region2(sol.x[0], sol.x[1]), [
235
+ const state = solveGibbsHS(h, s, [P0, r2BackwardT(P0, h)], region2);
236
+ return validateBackwardState(state, [
218
237
  { label: 'enthalpy', expected: h },
219
238
  { label: 'entropy', expected: s },
220
239
  ], { solverName: 'solveHS', expectedRegion: Region.Region2 });
221
240
  }
222
241
  case Region.Region3: {
223
242
  const P0 = r3Phs(h, s);
224
- const init = solvePH(P0, h);
225
- const rho0 = 1 / init.specificVolume;
226
- const T0 = init.temperature;
227
- const sol = nelderMead((pair) => Math.abs(region3ByRhoT(pair[0], pair[1]).enthalpy - h) +
228
- Math.abs(region3ByRhoT(pair[0], pair[1]).entropy - s), [rho0, T0]);
229
- return validateBackwardState(region3ByRhoT(sol.x[0], sol.x[1]), [
243
+ const initial = r3BackwardPHInitial(P0, h);
244
+ const state = solveRegion3HS(h, s, initial.rho, initial.T);
245
+ return validateBackwardState(state, [
230
246
  { label: 'enthalpy', expected: h },
231
247
  { label: 'entropy', expected: s },
232
248
  ], { solverName: 'solveHS', expectedRegion: Region.Region3 });
233
249
  }
234
250
  case Region.Region4: {
235
- throw new IF97Error('solveHS identified Region 4 but failed to construct a valid saturation state');
251
+ const state = tryRegion4HSState(h, s);
252
+ if (state === null) {
253
+ throw new IF97Error('solveHS identified Region 4 but failed to construct a valid saturation state');
254
+ }
255
+ return validateBackwardState(state, [
256
+ { label: 'enthalpy', expected: h },
257
+ { label: 'entropy', expected: s },
258
+ ], { solverName: 'solveHS', expectedRegion: Region.Region4 });
236
259
  }
237
260
  case Region.Region5: {
238
- const objective = normalizedRegion5HsObjective(h, s);
239
- const sol = nelderMead(objective, [1, 1400], { maxIterations: 1000, minErrorDelta: 1e-8, minTolerance: 1e-9 });
240
- return validateBackwardState(region5(sol.x[0], sol.x[1]), [
261
+ const state = solveGibbsHS(h, s, [1, 1400], region5);
262
+ return validateBackwardState(state, [
241
263
  { label: 'enthalpy', expected: h },
242
264
  { label: 'entropy', expected: s },
243
265
  ], { solverName: 'solveHS', expectedRegion: Region.Region5 });
@@ -246,4 +268,3 @@ export function solveHS(h, s) {
246
268
  throw new IF97Error(`Cannot determine region for h=${h} kJ/kg, s=${s} kJ/(kg·K)`);
247
269
  }
248
270
  }
249
- //# sourceMappingURL=hs.js.map