iapws-if97 2.2.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/README.md +69 -246
  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 +3 -3
  8. package/dist/backward/fixed-temperature-solver.js +38 -17
  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.js +28 -242
  16. package/dist/backward/ph.d.ts +2 -0
  17. package/dist/backward/ph.js +72 -21
  18. package/dist/backward/pressure-property-region.d.ts +13 -0
  19. package/dist/backward/pressure-property-region.js +262 -0
  20. package/dist/backward/ps.d.ts +6 -0
  21. package/dist/backward/ps.js +81 -28
  22. package/dist/backward/solution-validation.d.ts +7 -1
  23. package/dist/backward/solution-validation.js +138 -41
  24. package/dist/backward/th.d.ts +3 -5
  25. package/dist/backward/th.js +10 -71
  26. package/dist/backward/tolerances.d.ts +3 -0
  27. package/dist/backward/tolerances.js +9 -3
  28. package/dist/backward/ts.d.ts +3 -5
  29. package/dist/backward/ts.js +10 -71
  30. package/dist/constants.d.ts +11 -15
  31. package/dist/constants.js +14 -15
  32. package/dist/core/floating-point.d.ts +2 -0
  33. package/dist/core/floating-point.js +7 -0
  34. package/dist/core/formulation-seams.d.ts +27 -0
  35. package/dist/core/formulation-seams.js +289 -0
  36. package/dist/core/input-validation.d.ts +1 -0
  37. package/dist/core/input-validation.js +5 -0
  38. package/dist/core/region-detector.d.ts +3 -40
  39. package/dist/core/region-detector.js +37 -762
  40. package/dist/core/region3-pt.d.ts +1 -1
  41. package/dist/core/region3-pt.js +42 -16
  42. package/dist/core/solve-input-normalization.js +4 -1
  43. package/dist/core/solver.d.ts +7 -10
  44. package/dist/core/solver.js +26 -17
  45. package/dist/detect.d.ts +16 -2
  46. package/dist/detect.js +36 -2
  47. package/dist/errors.d.ts +11 -0
  48. package/dist/errors.js +16 -0
  49. package/dist/gas-constant.d.ts +5 -0
  50. package/dist/gas-constant.js +5 -0
  51. package/dist/index.d.ts +4 -5
  52. package/dist/index.js +10 -21
  53. package/dist/regions/region1.d.ts +2 -1
  54. package/dist/regions/region1.js +1 -1
  55. package/dist/regions/region2.d.ts +2 -1
  56. package/dist/regions/region2.js +1 -1
  57. package/dist/regions/region3-data.js +3 -3
  58. package/dist/regions/region3-subregions.js +15 -10
  59. package/dist/regions/region3.d.ts +4 -1
  60. package/dist/regions/region3.js +32 -2
  61. package/dist/regions/region4.d.ts +2 -0
  62. package/dist/regions/region4.js +39 -12
  63. package/dist/regions/region5.d.ts +2 -1
  64. package/dist/regions/region5.js +2 -2
  65. package/dist/saturation/common.d.ts +2 -8
  66. package/dist/saturation/common.js +64 -36
  67. package/dist/saturation/region4-boundaries.js +13 -13
  68. package/dist/saturation/region4-hs.js +282 -58
  69. package/dist/saturation/two-phase.d.ts +2 -2
  70. package/dist/saturation/two-phase.js +7 -7
  71. package/dist/solvers/bracketed-newton.d.ts +6 -3
  72. package/dist/solvers/bracketed-newton.js +15 -8
  73. package/dist/solvers/extended-arithmetic.d.ts +6 -0
  74. package/dist/solvers/extended-arithmetic.js +38 -0
  75. package/dist/transport/properties.d.ts +4 -2
  76. package/dist/transport/properties.js +14 -4
  77. package/dist/types.d.ts +18 -0
  78. package/dist/types.js +14 -0
  79. package/package.json +4 -4
  80. package/dist/core/public-normalization.d.ts +0 -3
  81. package/dist/core/public-normalization.js +0 -53
  82. /package/dist/{backward → core}/thermodynamic-derivatives.d.ts +0 -0
  83. /package/dist/{backward → core}/thermodynamic-derivatives.js +0 -0
@@ -1,5 +1,6 @@
1
+ import type { FormulationSeamResolution } from '../core/formulation-seams.js';
1
2
  import type { BasicProperties } from '../types.js';
2
- import { Region } from '../types.js';
3
+ import { IF97Error, Region } from '../types.js';
3
4
  export interface BackwardConstraint {
4
5
  label: 'pressure' | 'temperature' | 'enthalpy' | 'entropy';
5
6
  expected: number;
@@ -8,5 +9,10 @@ export interface BackwardConstraint {
8
9
  export interface BackwardValidationOptions {
9
10
  solverName: string;
10
11
  expectedRegion?: Region;
12
+ seam?: FormulationSeamResolution;
11
13
  }
14
+ /** Inspect a candidate without moving it across a formulation or domain boundary. */
15
+ export declare function backwardCandidateError(state: BasicProperties, constraints: readonly BackwardConstraint[], options: BackwardValidationOptions): IF97Error | null;
16
+ export declare function normalizeBackwardState(state: BasicProperties, constraints: readonly BackwardConstraint[], solverName: string): BasicProperties;
12
17
  export declare function validateBackwardState(state: BasicProperties, constraints: readonly BackwardConstraint[], options: BackwardValidationOptions): BasicProperties;
18
+ export declare function validateRegion3PTReconstruction(state: BasicProperties, solverName: string): BasicProperties;
@@ -1,7 +1,13 @@
1
1
  import * as C from '../constants.js';
2
- import { detectRegionPT } from '../core/region-detector.js';
2
+ import { detectRegionPT, nativeB23TemperatureBracket, nativeSaturationTemperatureBracket } from '../core/region-detector.js';
3
+ import { solveRegion3PTBasic } from '../core/region3-pt.js';
4
+ import { nextPositiveFloat } from '../core/floating-point.js';
5
+ import { region2 } from '../regions/region2.js';
6
+ import { region1 } from '../regions/region1.js';
3
7
  import { saturationPressure } from '../regions/region4.js';
8
+ import { region5 } from '../regions/region5.js';
4
9
  import { IF97Error, Region } from '../types.js';
10
+ import { NumericalStateError, RegionMismatchError } from '../errors.js';
5
11
  import { backwardConstraintTolerance, region4SaturationPressureTolerance, } from './tolerances.js';
6
12
  function propertyValue(state, label) {
7
13
  switch (label) {
@@ -31,7 +37,12 @@ function snapDownFromAbove(value, boundary) {
31
37
  : value;
32
38
  }
33
39
  function normalizeComputedPT(state) {
34
- let pressure = snapUpFromBelow(state.pressure, C.P_MIN);
40
+ if (state.region === Region.Region4)
41
+ return state;
42
+ let pressure = state.pressure < C.P_MIN &&
43
+ C.P_MIN - state.pressure <= backwardConstraintTolerance('pressure', C.P_MIN)
44
+ ? C.P_MIN
45
+ : state.pressure;
35
46
  pressure = snapDownFromAbove(pressure, state.region === Region.Region5 ? C.R5_P_MAX : C.P_MAX);
36
47
  let temperature = snapUpFromBelow(state.temperature, C.T_MIN);
37
48
  if (state.region === Region.Region1) {
@@ -41,73 +52,159 @@ function normalizeComputedPT(state) {
41
52
  temperature = snapDownFromAbove(temperature, C.R2_T_MAX);
42
53
  }
43
54
  temperature = snapDownFromAbove(temperature, C.T_MAX);
44
- return pressure === state.pressure && temperature === state.temperature
45
- ? state
46
- : { ...state, pressure, temperature };
55
+ if (pressure === state.pressure && temperature === state.temperature)
56
+ return state;
57
+ switch (state.region) {
58
+ case Region.Region1: return region1(pressure, temperature);
59
+ case Region.Region2: return region2(pressure, temperature);
60
+ case Region.Region3: return solveRegion3PTBasic(pressure, temperature);
61
+ case Region.Region5: return region5(pressure, temperature);
62
+ }
63
+ }
64
+ function normalizeB23State(state) {
65
+ if ((state.region !== Region.Region2 && state.region !== Region.Region3) ||
66
+ state.pressure <= C.B23_P_MIN || state.pressure > C.P_MAX ||
67
+ (state.temperature <= C.R2_T_MIN &&
68
+ (state.region !== Region.Region2 || state.pressure <= saturationPressure(C.R2_T_MIN))) ||
69
+ state.temperature >= C.B23_T_MAX ||
70
+ detectRegionPT(state.pressure, state.temperature) === state.region) {
71
+ return state;
72
+ }
73
+ const [lower, upper] = nativeB23TemperatureBracket(state.pressure);
74
+ const vaporT = state.region === Region.Region2 && state.pressure < C.Pc
75
+ ? nativeSaturationTemperatureBracket(state.pressure)[1] : upper;
76
+ const temperature = state.region === Region.Region3 ? lower : Math.max(upper, vaporT);
77
+ if (Math.abs(temperature - state.temperature) > boundaryTolerance(temperature, state.temperature)) {
78
+ return state;
79
+ }
80
+ return state.region === Region.Region3
81
+ ? solveRegion3PTBasic(state.pressure, temperature)
82
+ : region2(state.pressure, temperature);
83
+ }
84
+ function normalizeRegion3LowerState(state) {
85
+ if (state.region !== Region.Region3 || !(state.temperature <= C.R2_T_MIN) ||
86
+ C.R2_T_MIN - state.temperature > boundaryTolerance(state.temperature, C.R2_T_MIN))
87
+ return state;
88
+ return solveRegion3PTBasic(state.pressure, nextPositiveFloat(C.R2_T_MIN, 1));
89
+ }
90
+ function normalizeSaturationState(state, constraints, solverName) {
91
+ if ((state.region !== Region.Region1 && state.region !== Region.Region2) ||
92
+ state.pressure > saturationPressure(C.R2_T_MIN) ||
93
+ state.temperature < C.T_MIN || state.temperature > C.R2_T_MIN ||
94
+ detectRegionPT(state.pressure, state.temperature) === state.region) {
95
+ return state;
96
+ }
97
+ if (!constraints.some(({ label }) => label === 'pressure')) {
98
+ const psat = saturationPressure(state.temperature);
99
+ const pressure = state.region === Region.Region1 ? psat : nextPositiveFloat(psat, -1);
100
+ const projected = state.region === Region.Region1
101
+ ? region1(pressure, state.temperature)
102
+ : region2(pressure, state.temperature);
103
+ if (Math.abs(pressure - state.pressure) <= backwardConstraintTolerance('pressure', psat) &&
104
+ backwardCandidateError(projected, constraints, { solverName }) === null)
105
+ return projected;
106
+ }
107
+ const [liquidT, vaporT] = nativeSaturationTemperatureBracket(state.pressure);
108
+ const temperature = state.region === Region.Region1 ? liquidT : vaporT;
109
+ if (Math.abs(temperature - state.temperature) > boundaryTolerance(temperature, state.temperature)) {
110
+ return state;
111
+ }
112
+ return state.region === Region.Region1
113
+ ? region1(state.pressure, temperature)
114
+ : region2(state.pressure, temperature);
47
115
  }
48
- function validateBasicEnvelope(state, solverName) {
116
+ function basicEnvelopeError(state, solverName) {
49
117
  if (!Number.isFinite(state.pressure) || !Number.isFinite(state.temperature) ||
50
- !Number.isFinite(state.specificVolume) || state.specificVolume <= 0) {
51
- throw new IF97Error(`${solverName} produced an invalid thermodynamic state`);
118
+ !Number.isFinite(state.specificVolume)) {
119
+ return new NumericalStateError(`${solverName} produced an invalid thermodynamic state`);
52
120
  }
121
+ if (state.specificVolume <= 0)
122
+ return new IF97Error(`${solverName} produced an invalid thermodynamic state`);
53
123
  if (state.pressure < C.P_MIN || state.pressure > C.P_MAX ||
54
124
  state.temperature < C.T_MIN || state.temperature > C.T_MAX) {
55
- throw new IF97Error(`${solverName} produced a state outside the IF97 PT envelope`);
125
+ return new IF97Error(`${solverName} produced a state outside the IF97 PT envelope`);
56
126
  }
127
+ if (state.region === Region.Region3) {
128
+ const stability = [state.cp, state.cv, state.isothermalCompressibility, state.speedOfSound];
129
+ if (!stability.every((value) => value !== null && Number.isFinite(value) && value > 0)) {
130
+ const ErrorType = stability.some((value) => value === null || !Number.isFinite(value))
131
+ ? NumericalStateError : IF97Error;
132
+ return new ErrorType(`${solverName}: Region 3 numerical inconsistency: computed EOS state at p=${state.pressure} MPa, T=${state.temperature} K failed stability checks`);
133
+ }
134
+ }
135
+ return null;
57
136
  }
58
- function validateRegionConsistency(state, solverName, expectedRegion) {
137
+ function regionConsistencyError(state, solverName, expectedRegion) {
59
138
  if (expectedRegion !== undefined && state.region !== expectedRegion) {
60
- throw new IF97Error(`${solverName} returned Region ${state.region}, expected Region ${expectedRegion}`);
139
+ return new IF97Error(`${solverName} returned Region ${state.region}, expected Region ${expectedRegion}`);
61
140
  }
62
141
  if (state.region === Region.Region4) {
63
142
  if (state.quality === null || !Number.isFinite(state.quality) || state.quality < 0 || state.quality > 1) {
64
- throw new IF97Error(`${solverName} produced an invalid Region 4 quality`);
143
+ return new IF97Error(`${solverName} produced an invalid Region 4 quality`);
65
144
  }
66
145
  const psat = saturationPressure(state.temperature);
67
146
  const pTolerance = region4SaturationPressureTolerance(state.pressure);
68
147
  if (Math.abs(psat - state.pressure) > pTolerance) {
69
- throw new IF97Error(`${solverName} produced a Region 4 state off the saturation curve`);
148
+ return new IF97Error(`${solverName} produced a Region 4 state off the saturation curve`);
70
149
  }
71
- return;
150
+ return null;
72
151
  }
73
152
  const detectedRegion = detectRegionPT(state.pressure, state.temperature);
74
153
  if (detectedRegion !== state.region) {
75
- throw new IF97Error(`${solverName} produced a PT state in Region ${detectedRegion}, not Region ${state.region}`);
154
+ return new RegionMismatchError(solverName, detectedRegion, state.region);
76
155
  }
156
+ return null;
77
157
  }
78
- function validateConstraints(state, constraints, solverName) {
158
+ function constraintError(state, constraints, solverName) {
79
159
  for (const constraint of constraints) {
80
160
  const actual = propertyValue(state, constraint.label);
81
161
  const tolerance = constraint.tolerance ?? backwardConstraintTolerance(constraint.label, constraint.expected);
82
- if (Math.abs(actual - constraint.expected) > tolerance) {
83
- throw new IF97Error(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
162
+ if (!Number.isFinite(actual)) {
163
+ return new NumericalStateError(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
164
+ }
165
+ if (!Number.isFinite(constraint.expected) || !Number.isFinite(tolerance) ||
166
+ tolerance < 0 || Math.abs(actual - constraint.expected) > tolerance) {
167
+ return new IF97Error(`${solverName} failed to match ${constraint.label}: expected ${constraint.expected}, got ${actual}`);
84
168
  }
85
169
  }
170
+ return null;
86
171
  }
87
- function withExactConstraints(state, constraints) {
88
- const normalized = { ...state };
89
- for (const constraint of constraints) {
90
- switch (constraint.label) {
91
- case 'pressure':
92
- normalized.pressure = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
93
- break;
94
- case 'temperature':
95
- normalized.temperature = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
96
- break;
97
- case 'enthalpy':
98
- normalized.enthalpy = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
99
- break;
100
- case 'entropy':
101
- normalized.entropy = Object.is(constraint.expected, -0) ? 0 : constraint.expected;
102
- break;
103
- }
172
+ /** Inspect a candidate without moving it across a formulation or domain boundary. */
173
+ export function backwardCandidateError(state, constraints, options) {
174
+ return basicEnvelopeError(state, options.solverName) ??
175
+ constraintError(state, constraints, options.solverName) ??
176
+ regionConsistencyError(state, options.solverName, options.expectedRegion);
177
+ }
178
+ export function normalizeBackwardState(state, constraints, solverName) {
179
+ const computed = normalizeComputedPT(state);
180
+ return constraints.some(({ label }) => label === 'temperature')
181
+ ? computed
182
+ : normalizeSaturationState(normalizeB23State(normalizeRegion3LowerState(computed)), constraints, solverName);
183
+ }
184
+ export function validateBackwardState(state, constraints, options) {
185
+ const seam = options.seam;
186
+ if (seam !== undefined && state !== seam.state) {
187
+ throw new IF97Error(`${options.solverName} supplied an unrelated formulation seam state`);
104
188
  }
189
+ const checkedConstraints = seam?.kind === 'gap'
190
+ ? constraints.map((constraint) => constraint.label === 'enthalpy' || constraint.label === 'entropy'
191
+ ? { ...constraint, tolerance: seam.budgets[constraint.label] }
192
+ : constraint)
193
+ : constraints;
194
+ const initialError = constraintError(state, checkedConstraints, options.solverName);
195
+ if (initialError !== null)
196
+ throw initialError;
197
+ const normalized = normalizeBackwardState(state, checkedConstraints, options.solverName);
198
+ const error = backwardCandidateError(normalized, checkedConstraints, options);
199
+ if (error !== null)
200
+ throw error;
105
201
  return normalized;
106
202
  }
107
- export function validateBackwardState(state, constraints, options) {
108
- const normalized = normalizeComputedPT(state);
109
- validateBasicEnvelope(normalized, options.solverName);
110
- validateRegionConsistency(normalized, options.solverName, options.expectedRegion);
111
- validateConstraints(normalized, constraints, options.solverName);
112
- return withExactConstraints(normalized, constraints);
203
+ export function validateRegion3PTReconstruction(state, solverName) {
204
+ return validateBackwardState(solveRegion3PTBasic(state.pressure, state.temperature), [
205
+ { label: 'pressure', expected: state.pressure },
206
+ { label: 'temperature', expected: state.temperature },
207
+ { label: 'enthalpy', expected: state.enthalpy },
208
+ { label: 'entropy', expected: state.entropy },
209
+ ], { solverName, expectedRegion: Region.Region3 });
113
210
  }
@@ -1,16 +1,13 @@
1
1
  /**
2
2
  * Backward equation T,H → state.
3
3
  *
4
- * Given temperature and specific enthalpy, determines the IAPWS-IF97 region
5
- * and solves for pressure using a bracketed Newton method, then returns
6
- * the full set of basic thermodynamic properties.
4
+ * Returns a state only when temperature and enthalpy determine it uniquely.
7
5
  */
8
6
  import type { BasicProperties } from '../types.js';
9
7
  /**
10
8
  * Solve for full basic thermodynamic state from temperature and enthalpy.
11
9
  *
12
- * Detects the IAPWS-IF97 region, then inverts h(p, T) to recover pressure
13
- * within the appropriate bracket for that region.
10
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
14
11
  *
15
12
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
16
13
  * the enthalpy surface is nearly flat there, making pressure recovery unreliable.
@@ -20,5 +17,6 @@ import type { BasicProperties } from '../types.js';
20
17
  * @returns Basic thermodynamic properties (without transport)
21
18
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
22
19
  * @throws {IF97Error} if T is within the critical exclusion band
20
+ * @throws {AmbiguousStateError} if multiple physical states match T and h
23
21
  */
24
22
  export declare function solveTH(T: number, h: number): BasicProperties;
@@ -1,22 +1,11 @@
1
1
  import * as C from '../constants.js';
2
- import { IF97Error, OutOfRangeError, Region } from '../types.js';
3
- import { assertFiniteNumber } from '../core/input-validation.js';
4
- import { detectRegionTH } from '../core/region-detector.js';
5
- import { solveRegion3PTBasic } from '../core/region3-pt.js';
6
- import { region1 } from '../regions/region1.js';
7
- import { region2 } from '../regions/region2.js';
8
- import { region5 } from '../regions/region5.js';
9
- import { boundary23_T_to_P } from '../regions/boundaries.js';
10
- import { saturationPressure } from '../regions/region4.js';
11
- import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtTemperature, } from '../saturation/common.js';
12
- import { assertRegion4StateAllowed } from '../saturation/region4-boundaries.js';
13
- import { solveFixedTemperaturePressure } from './fixed-temperature-solver.js';
14
- import { validateBackwardState } from './solution-validation.js';
2
+ import { IF97Error, OutOfRangeError } from '../types.js';
3
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
4
+ import { uniqueFixedTemperatureState } from './fixed-temperature-candidates.js';
15
5
  /**
16
6
  * Solve for full basic thermodynamic state from temperature and enthalpy.
17
7
  *
18
- * Detects the IAPWS-IF97 region, then inverts h(p, T) to recover pressure
19
- * within the appropriate bracket for that region.
8
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
20
9
  *
21
10
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
22
11
  * the enthalpy surface is nearly flat there, making pressure recovery unreliable.
@@ -26,6 +15,7 @@ import { validateBackwardState } from './solution-validation.js';
26
15
  * @returns Basic thermodynamic properties (without transport)
27
16
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
28
17
  * @throws {IF97Error} if T is within the critical exclusion band
18
+ * @throws {AmbiguousStateError} if multiple physical states match T and h
29
19
  */
30
20
  export function solveTH(T, h) {
31
21
  assertFiniteNumber('Temperature', T);
@@ -33,66 +23,15 @@ export function solveTH(T, h) {
33
23
  if (T < C.T_MIN || T > C.T_MAX) {
34
24
  throw new OutOfRangeError('Temperature', T, C.T_MIN, C.T_MAX);
35
25
  }
36
- if (h < C.H_MIN || h > C.H_MAX) {
26
+ if (!withinClosedLowerBound(h, C.H_MIN, 'enthalpy') || h > C.H_MAX) {
37
27
  throw new OutOfRangeError('Enthalpy', h, C.H_MIN, C.H_MAX);
38
28
  }
39
29
  if (Math.abs(T - C.Tc) < C.CRITICAL_T_EXCLUSION_BAND) {
40
30
  throw new IF97Error(`solveTH does not support temperatures within ${C.CRITICAL_T_EXCLUSION_BAND} K of the critical-point temperature Tc=${C.Tc} K; received T=${T} K`);
41
31
  }
42
- const region = detectRegionTH(T, h);
43
- switch (region) {
44
- case Region.Region1:
45
- return validateBackwardState(solveFixedTemperaturePressure(region1, T, h, saturationPressure(T), C.P_MAX, 'enthalpy', 'solveTH'), [
46
- { label: 'temperature', expected: T },
47
- { label: 'enthalpy', expected: h },
48
- ], {
49
- solverName: 'solveTH',
50
- expectedRegion: Region.Region1,
51
- });
52
- case Region.Region2: {
53
- const upper = T <= C.Tc
54
- ? saturationPressure(T)
55
- : T <= C.B23_T_MAX
56
- ? boundary23_T_to_P(T)
57
- : C.P_MAX;
58
- return validateBackwardState(solveFixedTemperaturePressure(region2, T, h, C.P_MIN, upper, 'enthalpy', 'solveTH'), [
59
- { label: 'temperature', expected: T },
60
- { label: 'enthalpy', expected: h },
61
- ], {
62
- solverName: 'solveTH',
63
- expectedRegion: Region.Region2,
64
- });
65
- }
66
- case Region.Region3: {
67
- const lower = T <= C.Tc
68
- ? saturationPressure(T)
69
- : boundary23_T_to_P(T);
70
- return validateBackwardState(solveFixedTemperaturePressure(solveRegion3PTBasic, T, h, lower, C.P_MAX, 'enthalpy', 'solveTH'), [
71
- { label: 'temperature', expected: T },
72
- { label: 'enthalpy', expected: h },
73
- ], {
74
- solverName: 'solveTH',
75
- expectedRegion: Region.Region3,
76
- });
77
- }
78
- case Region.Region4: {
79
- const endpoints = saturationEndpointsAtTemperature(T);
80
- assertRegion4StateAllowed(endpoints.pressure, endpoints.temperature, 'solveTH');
81
- const x = qualityFromSaturationProperty(endpoints.liquid.enthalpy, endpoints.vapor.enthalpy, h);
82
- return validateBackwardState(mixSaturationState(endpoints, x), [
83
- { label: 'temperature', expected: T },
84
- { label: 'enthalpy', expected: h },
85
- ], { solverName: 'solveTH', expectedRegion: Region.Region4 });
86
- }
87
- case Region.Region5:
88
- return validateBackwardState(solveFixedTemperaturePressure(region5, T, h, C.P_MIN, C.R5_P_MAX, 'enthalpy', 'solveTH'), [
89
- { label: 'temperature', expected: T },
90
- { label: 'enthalpy', expected: h },
91
- ], {
92
- solverName: 'solveTH',
93
- expectedRegion: Region.Region5,
94
- });
95
- default:
96
- throw new IF97Error(`solveTH cannot determine a valid IF97 state for T=${T} K, h=${h} kJ/kg`);
32
+ const state = uniqueFixedTemperatureState(T, h, 'enthalpy');
33
+ if (state === null) {
34
+ throw new IF97Error(`solveTH cannot determine a valid IF97 state for T=${T} K, h=${h} kJ/kg`);
97
35
  }
36
+ return state;
98
37
  }
@@ -1,5 +1,8 @@
1
1
  export type BackwardToleranceLabel = 'pressure' | 'temperature' | 'enthalpy' | 'entropy';
2
2
  export declare const BACKWARD_REGION4_QUALITY_TOLERANCE = 0.000001;
3
+ export declare const BACKWARD_HS_NEWTON_TOLERANCE = 1e-12;
4
+ export declare const BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE = 1e-13;
5
+ export declare function fixedTemperatureRootTolerance(property: 'enthalpy' | 'entropy'): number;
3
6
  export declare function backwardConstraintTolerance(label: BackwardToleranceLabel, expected: number): number;
4
7
  export declare function region4SaturationPressureTolerance(pressure: number): number;
5
8
  export declare function backwardSpecificVolumeTolerance(expected: number): number;
@@ -1,19 +1,25 @@
1
1
  export const BACKWARD_REGION4_QUALITY_TOLERANCE = 1e-6;
2
+ export const BACKWARD_HS_NEWTON_TOLERANCE = 1e-12;
3
+ export const BACKWARD_PRESSURE_PROPERTY_TEMPERATURE_TOLERANCE = 1e-13;
4
+ export function fixedTemperatureRootTolerance(property) {
5
+ return property === 'enthalpy' ? 1e-9 : 1e-12;
6
+ }
2
7
  export function backwardConstraintTolerance(label, expected) {
3
8
  switch (label) {
4
9
  case 'pressure':
10
+ return 1e-9 * Math.abs(expected);
5
11
  case 'temperature':
6
12
  return 1e-9 * Math.max(1, Math.abs(expected));
7
13
  case 'enthalpy':
8
- return 1e-6 * Math.max(1, Math.abs(expected));
14
+ return 1e-9 * Math.max(1, Math.abs(expected));
9
15
  case 'entropy':
10
- return 1e-6 * Math.max(1, Math.abs(expected));
16
+ return 1e-9 * Math.max(1, Math.abs(expected));
11
17
  default:
12
18
  return 1e-9;
13
19
  }
14
20
  }
15
21
  export function region4SaturationPressureTolerance(pressure) {
16
- return 1e-8 * Math.max(1, Math.abs(pressure));
22
+ return 1e-8 * Math.abs(pressure);
17
23
  }
18
24
  export function backwardSpecificVolumeTolerance(expected) {
19
25
  return 1e-6 * Math.max(1, Math.abs(expected));
@@ -1,16 +1,13 @@
1
1
  /**
2
2
  * Backward equation T,S → state.
3
3
  *
4
- * Given temperature and specific entropy, determines the IAPWS-IF97 region
5
- * and solves for pressure using a bracketed Newton method, then returns
6
- * the full set of basic thermodynamic properties.
4
+ * Returns a state only when temperature and entropy determine it uniquely.
7
5
  */
8
6
  import type { BasicProperties } from '../types.js';
9
7
  /**
10
8
  * Solve for full basic thermodynamic state from temperature and entropy.
11
9
  *
12
- * Detects the IAPWS-IF97 region, then inverts s(p, T) to recover pressure
13
- * within the appropriate bracket for that region.
10
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
14
11
  *
15
12
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
16
13
  * the entropy surface is nearly flat there, making pressure recovery unreliable.
@@ -20,5 +17,6 @@ import type { BasicProperties } from '../types.js';
20
17
  * @returns Basic thermodynamic properties (without transport)
21
18
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
22
19
  * @throws {IF97Error} if T is within the critical exclusion band
20
+ * @throws {AmbiguousStateError} if multiple physical states match T and s
23
21
  */
24
22
  export declare function solveTS(T: number, s: number): BasicProperties;
@@ -1,22 +1,11 @@
1
1
  import * as C from '../constants.js';
2
- import { IF97Error, OutOfRangeError, Region } from '../types.js';
3
- import { assertFiniteNumber } from '../core/input-validation.js';
4
- import { detectRegionTS } from '../core/region-detector.js';
5
- import { solveRegion3PTBasic } from '../core/region3-pt.js';
6
- import { region1 } from '../regions/region1.js';
7
- import { region2 } from '../regions/region2.js';
8
- import { region5 } from '../regions/region5.js';
9
- import { boundary23_T_to_P } from '../regions/boundaries.js';
10
- import { saturationPressure } from '../regions/region4.js';
11
- import { mixSaturationState, qualityFromSaturationProperty, saturationEndpointsAtTemperature, } from '../saturation/common.js';
12
- import { assertRegion4StateAllowed } from '../saturation/region4-boundaries.js';
13
- import { solveFixedTemperaturePressure } from './fixed-temperature-solver.js';
14
- import { validateBackwardState } from './solution-validation.js';
2
+ import { IF97Error, OutOfRangeError } from '../types.js';
3
+ import { assertFiniteNumber, withinClosedLowerBound } from '../core/input-validation.js';
4
+ import { uniqueFixedTemperatureState } from './fixed-temperature-candidates.js';
15
5
  /**
16
6
  * Solve for full basic thermodynamic state from temperature and entropy.
17
7
  *
18
- * Detects the IAPWS-IF97 region, then inverts s(p, T) to recover pressure
19
- * within the appropriate bracket for that region.
8
+ * Searches every valid isotherm branch and rejects ambiguous inputs.
20
9
  *
21
10
  * Temperatures within ±CRITICAL_T_EXCLUSION_BAND of Tc are rejected because
22
11
  * the entropy surface is nearly flat there, making pressure recovery unreliable.
@@ -26,6 +15,7 @@ import { validateBackwardState } from './solution-validation.js';
26
15
  * @returns Basic thermodynamic properties (without transport)
27
16
  * @throws {OutOfRangeError} if T is outside [T_MIN, T_MAX]
28
17
  * @throws {IF97Error} if T is within the critical exclusion band
18
+ * @throws {AmbiguousStateError} if multiple physical states match T and s
29
19
  */
30
20
  export function solveTS(T, s) {
31
21
  assertFiniteNumber('Temperature', T);
@@ -33,66 +23,15 @@ export function solveTS(T, s) {
33
23
  if (T < C.T_MIN || T > C.T_MAX) {
34
24
  throw new OutOfRangeError('Temperature', T, C.T_MIN, C.T_MAX);
35
25
  }
36
- if (s < C.S_MIN || s > C.S_MAX) {
26
+ if (!withinClosedLowerBound(s, C.S_MIN, 'entropy') || s > C.S_MAX) {
37
27
  throw new OutOfRangeError('Entropy', s, C.S_MIN, C.S_MAX);
38
28
  }
39
29
  if (Math.abs(T - C.Tc) < C.CRITICAL_T_EXCLUSION_BAND) {
40
30
  throw new IF97Error(`solveTS does not support temperatures within ${C.CRITICAL_T_EXCLUSION_BAND} K of the critical-point temperature Tc=${C.Tc} K; received T=${T} K`);
41
31
  }
42
- const region = detectRegionTS(T, s);
43
- switch (region) {
44
- case Region.Region1:
45
- return validateBackwardState(solveFixedTemperaturePressure(region1, T, s, saturationPressure(T), C.P_MAX, 'entropy', 'solveTS'), [
46
- { label: 'temperature', expected: T },
47
- { label: 'entropy', expected: s },
48
- ], {
49
- solverName: 'solveTS',
50
- expectedRegion: Region.Region1,
51
- });
52
- case Region.Region2: {
53
- const upper = T <= C.Tc
54
- ? saturationPressure(T)
55
- : T <= C.B23_T_MAX
56
- ? boundary23_T_to_P(T)
57
- : C.P_MAX;
58
- return validateBackwardState(solveFixedTemperaturePressure(region2, T, s, C.P_MIN, upper, 'entropy', 'solveTS'), [
59
- { label: 'temperature', expected: T },
60
- { label: 'entropy', expected: s },
61
- ], {
62
- solverName: 'solveTS',
63
- expectedRegion: Region.Region2,
64
- });
65
- }
66
- case Region.Region3: {
67
- const lower = T <= C.Tc
68
- ? saturationPressure(T)
69
- : boundary23_T_to_P(T);
70
- return validateBackwardState(solveFixedTemperaturePressure(solveRegion3PTBasic, T, s, lower, C.P_MAX, 'entropy', 'solveTS'), [
71
- { label: 'temperature', expected: T },
72
- { label: 'entropy', expected: s },
73
- ], {
74
- solverName: 'solveTS',
75
- expectedRegion: Region.Region3,
76
- });
77
- }
78
- case Region.Region4: {
79
- const endpoints = saturationEndpointsAtTemperature(T);
80
- assertRegion4StateAllowed(endpoints.pressure, endpoints.temperature, 'solveTS');
81
- const x = qualityFromSaturationProperty(endpoints.liquid.entropy, endpoints.vapor.entropy, s);
82
- return validateBackwardState(mixSaturationState(endpoints, x), [
83
- { label: 'temperature', expected: T },
84
- { label: 'entropy', expected: s },
85
- ], { solverName: 'solveTS', expectedRegion: Region.Region4 });
86
- }
87
- case Region.Region5:
88
- return validateBackwardState(solveFixedTemperaturePressure(region5, T, s, C.P_MIN, C.R5_P_MAX, 'entropy', 'solveTS'), [
89
- { label: 'temperature', expected: T },
90
- { label: 'entropy', expected: s },
91
- ], {
92
- solverName: 'solveTS',
93
- expectedRegion: Region.Region5,
94
- });
95
- default:
96
- throw new IF97Error(`solveTS cannot determine a valid IF97 state for T=${T} K, s=${s} kJ/(kg·K)`);
32
+ const state = uniqueFixedTemperatureState(T, s, 'entropy');
33
+ if (state === null) {
34
+ throw new IF97Error(`solveTS cannot determine a valid IF97 state for T=${T} K, s=${s} kJ/(kg·K)`);
97
35
  }
36
+ return state;
98
37
  }
@@ -4,11 +4,7 @@
4
4
  * Physical constants, critical point properties, and range limits
5
5
  * for the IAPWS Industrial Formulation 1997.
6
6
  */
7
- /** Specific gas constant for water [kJ/(kg·K)].
8
- * IAPWS-IF97, §2 (Table 1). Fixed value for IF97 internal consistency.
9
- * Do not replace transport correlations with this value; thermal conductivity
10
- * critical enhancement intentionally uses `R_IAPWS_2011_THERMAL`. */
11
- export declare const R = 0.461526;
7
+ export { R } from './gas-constant.js';
12
8
  /** Specific gas constant used by the IAPWS 2011 thermal conductivity λ₂ correlation [kJ/(kg·K)].
13
9
  * Intentionally differs from IF97's `R`; keep both constants distinct and named. */
14
10
  export declare const R_IAPWS_2011_THERMAL = 0.46151805;
@@ -16,13 +12,13 @@ export declare const R_IAPWS_2011_THERMAL = 0.46151805;
16
12
  export declare const Tc = 647.096;
17
13
  /** Temperature exclusion half-band around the critical point [K] */
18
14
  export declare const CRITICAL_T_EXCLUSION_BAND = 0.001;
19
- /** Region 4 endpoint snap tolerance for temperatures [K] */
15
+ /** Region 4 tolerance for outward temperature-boundary roundoff [K] */
20
16
  export declare const REGION4_TEMPERATURE_TOLERANCE = 1e-9;
21
17
  /** Safety margin used when clamping Region 4 temperatures below Tc [K] */
22
18
  export declare const REGION4_SUBCRITICAL_TEMPERATURE_MARGIN = 1e-8;
23
19
  /** Critical pressure [MPa] */
24
20
  export declare const Pc = 22.064;
25
- /** Region 4 endpoint snap tolerance for pressures [MPa] */
21
+ /** Relative tolerance for outward Region 4 pressure-boundary roundoff */
26
22
  export declare const REGION4_PRESSURE_TOLERANCE = 1e-9;
27
23
  /** Tolerance for identifying the critical h-s endpoint in Region 4 */
28
24
  export declare const REGION4_CRITICAL_HS_TOLERANCE = 0.000001;
@@ -44,14 +40,14 @@ export declare const P_MAX = 100;
44
40
  export declare const T_MIN = 273.15;
45
41
  /** Maximum temperature [K] (2273.15 K = 2000 °C) */
46
42
  export declare const T_MAX = 2273.15;
47
- /** Minimum specific entropy [kJ/(kg·K)] */
48
- export declare const S_MIN = -0.00015454959194;
49
- /** Maximum specific entropy [kJ/(kg·K)] */
50
- export declare const S_MAX = 13.904956083429227;
51
- /** Minimum specific enthalpy [kJ/kg] */
52
- export declare const H_MIN = -0.041587825987;
53
- /** Maximum specific enthalpy [kJ/kg] */
54
- export declare const H_MAX = 7376.980263598506;
43
+ /** Minimum specific entropy [kJ/(kg·K)], at T_MIN and P_MAX in Region 1. */
44
+ export declare const S_MIN = -0.00858228709261889;
45
+ /** Maximum specific entropy [kJ/(kg·K)], at T_MAX and P_MIN in Region 5. */
46
+ export declare const S_MAX: number;
47
+ /** Minimum specific enthalpy [kJ/kg], at T_MIN and P_MIN in Region 1. */
48
+ export declare const H_MIN = -0.041587825987655716;
49
+ /** Maximum specific enthalpy [kJ/kg], at T_MAX and P_MIN in Region 5. */
50
+ export declare const H_MAX: number;
55
51
  /** Region 2 minimum temperature for high-P subregions [K] (623.15 K = 350 °C) */
56
52
  export declare const R2_T_MIN = 623.15;
57
53
  /** Region 2 maximum temperature for backward eqs [K] (1073.15 K = 800 °C) */