CoolProp 8.0.0
An open-source fluid property and humid air property database
SuperancillaryTemperatureBoundaryCurve.h
Go to the documentation of this file.
1#ifndef COOLPROP_REGION_SUPERANCILLARY_TEMPERATURE_BOUNDARY_CURVE_H
2#define COOLPROP_REGION_SUPERANCILLARY_TEMPERATURE_BOUNDARY_CURVE_H
3
4#include <cmath>
5#include <cstddef>
6#include <memory>
7#include <set> // transitively needed by superancillary.h
8#include <utility>
9#include <vector>
10
13
14namespace CoolProp {
15namespace region {
16
17// 1D boundary curve b = f(T) backed by the source backend's SuperAncillary.
18// T-parameterized sibling of SuperancillaryBoundaryCurve — that one
19// composes get_T_from_p with eval_sat for p-axis presets (PH, PT); this
20// one calls eval_sat(T, prop, Q) directly because the DT-indexed preset
21// uses T as the primary axis and needs no inversion.
22//
23// SCOPE. Sat dome curves only (Q in {0, 1}, prop_key in {'D','P','H',
24// 'S','U'}). Same scope as the pressure-parameterized sibling.
25//
26// EVAL_DA. Central FD on eval(T) with relative step h = max(1e-7*T, 1)
27// K — well inside the smooth interior of every property's Cheb
28// expansion. Endpoints clamped to [T_min, T_max] so a query at the
29// build edge doesn't step out into the SA's invalid region.
30//
31// EVAL_FAST. 1024-point linear-T table (no log scaling — T span is at
32// most factor ~3 from triple to critical). ~8 KB per curve.
34{
35 public:
37
38 SuperancillaryTemperatureBoundaryCurve(std::shared_ptr<SuperAncillary_t> sa, double T_min, double T_max, char prop_key, short Q,
39 double output_scale, double b_min, double b_max)
40 : sa_(std::move(sa)), T_min_(T_min), T_max_(T_max), prop_key_(prop_key), Q_(Q), output_scale_(output_scale), b_min_(b_min), b_max_(b_max) {
41 // Fail fast: every public eval / eval_da / eval_fast path
42 // dereferences sa_ unchecked. Both factory build() and
43 // from_state() null-guard; the direct-construction path also
44 // needs the guard.
45 if (!sa_) {
46 throw std::invalid_argument("SuperancillaryTemperatureBoundaryCurve: null SuperAncillary handle");
47 }
48 if (!(T_min_ > 0.0) || !(T_max_ > T_min_)) {
49 throw std::invalid_argument("SuperancillaryTemperatureBoundaryCurve: need 0 < T_min < T_max");
50 }
51 // Validate the deserialization-reachable invariants here, not
52 // only in build(): from_state() constructs directly, so a bad
53 // Q / prop_key from a corrupt blob must fail fast rather than
54 // silently produce a NaN-backed curve. Mirrors the SCOPE doc.
55 if (Q_ != 0 && Q_ != 1) {
56 throw std::invalid_argument("SuperancillaryTemperatureBoundaryCurve: Q must be 0 (liquid) or 1 (vapor)");
57 }
58 switch (prop_key_) {
59 case 'D':
60 case 'P':
61 case 'H':
62 case 'S':
63 case 'U':
64 break;
65 default:
66 throw std::invalid_argument("SuperancillaryTemperatureBoundaryCurve: unsupported prop_key (expect one of D,P,H,S,U)");
67 }
68 build_surrogate_();
69 }
70
71 // Factory: probes the SA across [T_min, T_max] to pre-compute b_min
72 // / b_max for the AABB cheap-reject. Throws if the SuperAncillary
73 // rejects the property key or the entire requested range.
74 //
75 // SuperAncillary returns properties in MOLAR units (mol/m^3 for D,
76 // J/mol for H, J/(mol K) for S). For mass-basis density consumers
77 // pass `output_scale = M` (kg/mol); for molar-basis pass 1.0.
78 static std::unique_ptr<SuperancillaryTemperatureBoundaryCurve> build(std::shared_ptr<SuperAncillary_t> sa, double T_min, double T_max,
79 char prop_key, short Q, double output_scale);
80
81 [[nodiscard]] double eval(double T) const noexcept override {
82 try {
83 return sa_->eval_sat(T, prop_key_, Q_) * output_scale_;
84 } catch (...) { // NOLINT(bugprone-empty-catch)
85 return std::nan("");
86 }
87 }
88
89 [[nodiscard]] double eval_da(double T) const noexcept override {
90 // Central FD on eval(T). Relative step h = max(1e-7*T, 1 K)
91 // gives ~1e-13 relative error in the derivative — Cheb
92 // expansions are C^infinity in the interior. Clamp the
93 // stencil to [T_min_, T_max_] so an endpoint query doesn't
94 // step into the SA's invalid range. Asymmetric stencils
95 // still divide by the actual span, preserving slope accuracy.
96 const double h = std::max(1e-7 * std::abs(T), 1.0);
97 const double T_hi = std::min(T + h, T_max_);
98 const double T_lo = std::max(T - h, T_min_);
99 if (!(T_hi > T_lo)) {
100 return std::nan("");
101 }
102 const double y_plus = eval(T_hi);
103 const double y_minus = eval(T_lo);
104 if (!std::isfinite(y_plus) || !std::isfinite(y_minus)) {
105 return std::nan("");
106 }
107 return (y_plus - y_minus) / (T_hi - T_lo);
108 }
109
110 // Fast 1e-6-accurate surrogate via 1024-point linear-T table. Used
111 // by Region::curve_contains for atlas dispatch. Out-of-range clamps
112 // to the nearest endpoint.
113 [[nodiscard]] double eval_fast(double T) const noexcept override {
114 const double idx_f = (T - T_min_) * inv_T_step_;
115 if (!(idx_f > 0.0)) {
116 return surrogate_table_.front();
117 }
118 const auto last = static_cast<double>(kSurrogatePoints - 1);
119 if (idx_f >= last) {
120 return surrogate_table_.back();
121 }
122 const auto i = static_cast<std::size_t>(idx_f);
123 const double frac = idx_f - static_cast<double>(i);
124 const double y0 = surrogate_table_[i];
125 const double y1 = surrogate_table_[i + 1];
126 return y0 + frac * (y1 - y0);
127 }
128
129 [[nodiscard]] std::pair<double, double> bounds() const noexcept override {
130 return {b_min_, b_max_};
131 }
132 [[nodiscard]] std::pair<double, double> a_range() const noexcept override {
133 return {T_min_, T_max_};
134 }
135
136 // State / from_state for the serializer. Only primitive scalars
137 // are stored; deserialization re-attaches the live SuperAncillary
138 // handle (typically the same instance the SVDSBTL backend owns).
139 struct State
140 {
141 double T_min;
142 double T_max;
144 short Q;
146 double b_min;
147 double b_max;
148 };
149 [[nodiscard]] State state() const {
150 return State{T_min_, T_max_, prop_key_, Q_, output_scale_, b_min_, b_max_};
151 }
152 static std::unique_ptr<SuperancillaryTemperatureBoundaryCurve> from_state(State s, std::shared_ptr<SuperAncillary_t> sa) {
153 if (!sa) {
154 throw std::invalid_argument("SuperancillaryTemperatureBoundaryCurve::from_state: null SuperAncillary handle");
155 }
156 return std::make_unique<SuperancillaryTemperatureBoundaryCurve>(std::move(sa), s.T_min, s.T_max, s.prop_key, s.Q, s.output_scale, s.b_min,
157 s.b_max);
158 }
159
160 private:
161 static constexpr std::size_t kSurrogatePoints = 1024;
162
163 // Build the 1024-point linear-T surrogate table. NOT noexcept:
164 // surrogate_table_.resize can throw std::bad_alloc.
165 void build_surrogate_() {
166 surrogate_table_.resize(kSurrogatePoints);
167 const double T_step = (T_max_ - T_min_) / static_cast<double>(kSurrogatePoints - 1);
168 inv_T_step_ = 1.0 / T_step;
169 bool any_finite = false;
170 for (std::size_t k = 0; k < kSurrogatePoints; ++k) {
171 const double T = T_min_ + static_cast<double>(k) * T_step;
172 surrogate_table_[k] = eval(T);
173 any_finite = any_finite || std::isfinite(surrogate_table_[k]);
174 }
175 // The backfill below assumes ≥1 finite sample exists. build()'s
176 // probe guarantees that, but from_state() can construct directly
177 // with a handle whose valid range doesn't overlap [T_min, T_max]
178 // — every sample non-finite. Fail fast rather than returning an
179 // all-NaN table that eval_fast() would silently interpolate.
180 if (!any_finite) {
181 throw std::runtime_error("SuperancillaryTemperatureBoundaryCurve: no finite samples in surrogate build range");
182 }
183 // Backfill any non-finite samples (rare: SA can reject a probe
184 // exotically close to the critical point) from the nearest
185 // finite neighbour. Storing a raw NaN would let eval_fast()
186 // interpolate through it and make Region::curve_contains
187 // false-miss valid points even though the exact eval() path
188 // still works. The factory's b_min/b_max probe already
189 // guarantees ≥1 finite sample exists, so the backward+forward
190 // scan always resolves.
191 for (std::size_t k = 0; k < kSurrogatePoints; ++k) {
192 if (std::isfinite(surrogate_table_[k])) continue;
193 double fill = std::nan("");
194 for (std::size_t j = k; j-- > 0;) {
195 if (std::isfinite(surrogate_table_[j])) {
196 fill = surrogate_table_[j];
197 break;
198 }
199 }
200 if (!std::isfinite(fill)) {
201 for (std::size_t j = k + 1; j < kSurrogatePoints; ++j) {
202 if (std::isfinite(surrogate_table_[j])) {
203 fill = surrogate_table_[j];
204 break;
205 }
206 }
207 }
208 surrogate_table_[k] = fill; // finite by the factory's probe guarantee
209 }
210 }
211
212 std::shared_ptr<SuperAncillary_t> sa_;
213 double T_min_;
214 double T_max_;
215 char prop_key_;
216 short Q_;
217 double output_scale_;
218 double b_min_;
219 double b_max_;
220 std::vector<double> surrogate_table_;
221 double inv_T_step_ = 0.0;
222};
223
224} // namespace region
225} // namespace CoolProp
226
227#endif // COOLPROP_REGION_SUPERANCILLARY_TEMPERATURE_BOUNDARY_CURVE_H