CoolProp 8.0.0
An open-source fluid property and humid air property database
CubicSplineCurve.cpp
Go to the documentation of this file.
2
3#include <cmath>
4#include <cstddef>
5#include <stdexcept>
6#include <string> // std::to_string + std::invalid_argument(string) — MSVC needs explicit include
7
8namespace CoolProp {
9namespace region {
10
11namespace {
12
13// Solve a tridiagonal system A x = d where A has subdiagonal a, diagonal
14// b, superdiagonal c. Standard Thomas algorithm — O(n) with one pass
15// forward, one pass back. Writes the solution into d in-place.
16void thomas(std::vector<double>& sub, std::vector<double>& diag, std::vector<double>& sup, std::vector<double>& d) {
17 const std::size_t n = diag.size();
18 // Defense in depth: every CubicSplineCurve build() call feeds the
19 // natural-cubic system whose diagonal is (h_lo + h_hi)/3 with both
20 // h_i strictly positive (we validate strictly-increasing a above),
21 // so the diagonal is always positive. But if someone routes a
22 // future call into this helper with a pathological matrix, the
23 // forward sweep silently divides by zero -- check explicitly so we
24 // surface a clear error instead of producing NaN coefficients.
25 constexpr double kTinyDiag = 1e-300;
26 for (std::size_t i = 0; i < n; ++i) {
27 if (std::abs(diag[i]) < kTinyDiag) {
28 throw std::invalid_argument("thomas: zero (or sub-normal) diagonal entry; tridiagonal system is singular");
29 }
30 }
31 // Forward sweep.
32 for (std::size_t i = 1; i < n; ++i) {
33 const double m = sub[i] / diag[i - 1];
34 diag[i] -= m * sup[i - 1];
35 if (std::abs(diag[i]) < kTinyDiag) {
36 throw std::invalid_argument("thomas: pivot vanished during forward sweep (system is singular at row " + std::to_string(i) + ")");
37 }
38 d[i] -= m * d[i - 1];
39 }
40 // Back substitution.
41 d[n - 1] /= diag[n - 1];
42 for (std::size_t i = n - 1; i-- > 0;) {
43 d[i] = (d[i] - sup[i] * d[i + 1]) / diag[i];
44 }
45}
46
47// Evaluate the cubic on segment [a_i, a_{i+1}] at parameter t in [0,1]
48// given knot values b_i, b_{i+1} and second derivatives M_i, M_{i+1}
49// and segment width h = a_{i+1} - a_i.
50//
51// p(t) = (1 - t) b_i + t b_{i+1}
52// + (h^2 / 6) [ ((1-t)^3 - (1-t)) M_i + (t^3 - t) M_{i+1} ]
53double segment_eval(double t, double h, double b_i, double b_ip1, double M_i, double M_ip1) noexcept {
54 const double s = 1.0 - t;
55 const double h2_6 = h * h / 6.0;
56 return s * b_i + t * b_ip1 + h2_6 * ((s * s * s - s) * M_i + (t * t * t - t) * M_ip1);
57}
58
59// dp/da on segment [a_i, a_{i+1}] at parameter t in [0,1]:
60//
61// dp/dt = (b_{i+1} - b_i) + (h^2 / 6) [ (-3(1-t)^2 + 1) M_i
62// + ( 3 t^2 - 1) M_{i+1} ]
63// dp/da = dp/dt / h
64// = (b_{i+1} - b_i)/h + (h/6) [ (1 - 3(1-t)^2) M_i
65// + (3 t^2 - 1) M_{i+1} ]
66double segment_eval_da(double t, double h, double b_i, double b_ip1, double M_i, double M_ip1) noexcept {
67 const double s = 1.0 - t;
68 const double h_6 = h / 6.0;
69 return (b_ip1 - b_i) / h + h_6 * ((1.0 - 3.0 * s * s) * M_i + (3.0 * t * t - 1.0) * M_ip1);
70}
71
72// Tight min/max of the spline on its full build interval.
73//
74// Sweep every segment, locate the (up to two) critical points of the
75// cubic from dp/dt = 0, and track the extremes against the knot values.
76// dp/dt is a quadratic in t — root-find with the standard formula,
77// clamped to [0, 1].
78std::pair<double, double> compute_bounds(const std::vector<double>& a, const std::vector<double>& b, const std::vector<double>& M) {
79 double lo = b.front();
80 double hi = b.front();
81 for (const double v : b) {
82 if (v < lo) {
83 lo = v;
84 }
85 if (v > hi) {
86 hi = v;
87 }
88 }
89 const std::size_t n_seg = a.size() - 1;
90 for (std::size_t i = 0; i < n_seg; ++i) {
91 const double h = a[i + 1] - a[i];
92 // dp/dt = (b_{i+1}-b_i) + (h^2/6)[(-3(1-t)^2+1) M_i + (3 t^2 - 1) M_{i+1}]
93 // Expand to A t^2 + B t + C = 0:
94 // coefficient of t^2 : (h^2/6) * 3 * (M_{i+1} - M_i)
95 // coefficient of t : (h^2/6) * 6 * M_i
96 // constant : (b_{i+1}-b_i) + (h^2/6)*(-3 M_i*1) + (h^2/6)*(-M_{i+1})
97 // = (b_{i+1}-b_i) - (h^2/6)*(3 M_i + M_{i+1})
98 // (Algebra: -3(1-t)^2 + 1 = -3 + 6 t - 3 t^2 + 1 = -2 + 6 t - 3 t^2.)
99 // Re-derive cleanly:
100 // -3(1-t)^2 + 1 = -3 + 6 t - 3 t^2 + 1 = -3 t^2 + 6 t - 2
101 // 3 t^2 - 1
102 // So dp/dt = D + (h^2/6) [(-3 t^2 + 6 t - 2) M_i + (3 t^2 - 1) M_{i+1}]
103 // = D + (h^2/6) [ 3 t^2 (M_{i+1} - M_i)
104 // + 6 t M_i
105 // + (-2 M_i - M_{i+1}) ]
106 // where D = b_{i+1} - b_i.
107 const double D = b[i + 1] - b[i];
108 const double k = h * h / 6.0;
109 const double A = k * 3.0 * (M[i + 1] - M[i]);
110 const double B = k * 6.0 * M[i];
111 const double C = D + k * (-2.0 * M[i] - M[i + 1]);
112 const auto try_t = [&](double t) {
113 if (t > 0.0 && t < 1.0) {
114 const double v = segment_eval(t, h, b[i], b[i + 1], M[i], M[i + 1]);
115 if (v < lo) {
116 lo = v;
117 }
118 if (v > hi) {
119 hi = v;
120 }
121 }
122 };
123 if (std::abs(A) < 1e-300) {
124 // Linear (degenerate quadratic): B t + C = 0.
125 if (std::abs(B) > 1e-300) {
126 try_t(-C / B);
127 }
128 } else {
129 const double disc = B * B - 4.0 * A * C;
130 if (disc >= 0.0) {
131 const double sq = std::sqrt(disc);
132 try_t((-B + sq) / (2.0 * A));
133 try_t((-B - sq) / (2.0 * A));
134 }
135 }
136 }
137 return {lo, hi};
138}
139
140} // namespace
141
142std::unique_ptr<CubicSplineCurve> CubicSplineCurve::build(std::vector<double> a, std::vector<double> b) {
143 if (a.size() != b.size()) {
144 throw std::invalid_argument("CubicSplineCurve::build: a and b must have the same size");
145 }
146 const std::size_t n = a.size();
147 if (n < 2) {
148 throw std::invalid_argument("CubicSplineCurve::build: need at least 2 knots");
149 }
150 for (std::size_t i = 1; i < n; ++i) {
151 if (!(a[i] > a[i - 1])) {
152 throw std::invalid_argument("CubicSplineCurve::build: a must be strictly increasing");
153 }
154 }
155
156 std::vector<double> M(n, 0.0);
157 if (n >= 3) {
158 // Build tridiagonal system for interior unknowns M[1..n-2] with
159 // natural BCs M[0] = M[n-1] = 0.
160 const std::size_t m = n - 2;
161 std::vector<double> sub(m, 0.0);
162 std::vector<double> diag(m, 0.0);
163 std::vector<double> sup(m, 0.0);
164 std::vector<double> rhs(m, 0.0);
165 for (std::size_t i = 0; i < m; ++i) {
166 const std::size_t k = i + 1; // global index
167 const double h_lo = a[k] - a[k - 1];
168 const double h_hi = a[k + 1] - a[k];
169 sub[i] = h_lo / 6.0;
170 diag[i] = (h_lo + h_hi) / 3.0;
171 sup[i] = h_hi / 6.0;
172 rhs[i] = (b[k + 1] - b[k]) / h_hi - (b[k] - b[k - 1]) / h_lo;
173 }
174 // Natural BC eliminates the off-diagonal terms touching M[0],
175 // M[n-1]: their coefficients in the rhs are zero, so sub[0] and
176 // sup[m-1] feed into nothing (they multiply unknowns at i=-1 and
177 // i=m, which don't exist in the reduced system).
178 sub[0] = 0.0;
179 sup[m - 1] = 0.0;
180 thomas(sub, diag, sup, rhs);
181 for (std::size_t i = 0; i < m; ++i) {
182 M[i + 1] = rhs[i];
183 }
184 }
185 // n == 2: M stays zero, the spline reduces to the straight line.
186
187 auto bnds = compute_bounds(a, b, M);
188 return std::unique_ptr<CubicSplineCurve>(new CubicSplineCurve(std::move(a), std::move(b), std::move(M), bnds.first, bnds.second));
189}
190
191CubicSplineCurve::CubicSplineCurve(std::vector<double> a, std::vector<double> b, std::vector<double> M, double b_min, double b_max)
192 : a_(std::move(a)), b_(std::move(b)), M_(std::move(M)), b_min_(b_min), b_max_(b_max) {
193 build_bucket_table_();
194}
195
196void CubicSplineCurve::build_bucket_table_() noexcept {
197 const std::size_t n = a_.size();
198 const double a_lo = a_.front();
199 const double a_hi = a_.back();
200 const double span = a_hi - a_lo;
201 // Guard against degenerate single-segment curves where span could
202 // be zero (build() already rejects this, but be defensive).
203 a_lo_inv_step_ = (span > 0.0) ? (static_cast<double>(kBuckets) / span) : 0.0;
204 // Sweep buckets left-to-right; maintain a single advancing knot
205 // index `i` so the build is O(kBuckets + n) total.
206 std::size_t i = 0;
207 const auto max_seg_idx = static_cast<std::uint16_t>(n - 2);
208 for (std::size_t k = 0; k < kBuckets; ++k) {
209 const double a_bucket = a_lo + static_cast<double>(k) * span / static_cast<double>(kBuckets);
210 // Advance i while a_[i+1] is still <= a_bucket. Loop invariant
211 // after this step: a_[i] <= a_bucket < a_[i+1] (modulo clamping).
212 while (i + 1 < n - 1 && a_[i + 1] <= a_bucket) {
213 ++i;
214 }
215 bucket_to_knot_[k] = (i > max_seg_idx) ? max_seg_idx : static_cast<std::uint16_t>(i);
216 }
217}
218
220 return State{a_, b_, M_, b_min_, b_max_};
221}
222
223std::unique_ptr<CubicSplineCurve> CubicSplineCurve::from_state(State s) {
224 const std::size_t n = s.a.size();
225 if (s.b.size() != n || s.M.size() != n) {
226 throw std::invalid_argument("CubicSplineCurve::from_state: a/b/M size mismatch");
227 }
228 if (n < 2) {
229 throw std::invalid_argument("CubicSplineCurve::from_state: need at least 2 knots");
230 }
231 for (std::size_t i = 1; i < n; ++i) {
232 if (!(s.a[i] > s.a[i - 1])) {
233 throw std::invalid_argument("CubicSplineCurve::from_state: a must be strictly increasing");
234 }
235 }
236 // Re-derive the tight (b_min, b_max) from the deserialized (a, b, M)
237 // rather than trusting the values shipped in `s`. A tampered or
238 // mis-versioned blob could otherwise drop a bogus AABB into the
239 // RegionAtlas and silently corrupt region dispatch. The recompute
240 // is O(n) — negligible vs the rest of deserialization. The private
241 // ctor rebuilds the bucket table from the knot array as part of
242 // construction, so no extra work needed here.
243 const auto bnds = compute_bounds(s.a, s.b, s.M);
244 return std::unique_ptr<CubicSplineCurve>(new CubicSplineCurve(std::move(s.a), std::move(s.b), std::move(s.M), bnds.first, bnds.second));
245}
246
247std::size_t CubicSplineCurve::locate(double a) const noexcept {
248 // O(1) amortized: hash into a precomputed bucket, then walk
249 // forward at most a few knots. Non-uniform knot spacing is fine
250 // — the build_bucket_table_ pass already absorbed it.
251 const std::size_t n = a_.size();
252 const double a_lo = a_.front();
253 // NaN/inf guard: the comparisons below are both false on NaN,
254 // and the cast to ptrdiff_t a few lines down is UB on non-finite
255 // values. Return the leftmost segment for any non-finite input
256 // — the eventual eval() math will propagate NaN to the caller
257 // through the standard return path.
258 if (!std::isfinite(a)) {
259 return 0;
260 }
261 if (a <= a_lo) {
262 return 0;
263 }
264 if (a >= a_.back()) {
265 return n - 2;
266 }
267 // Bucket index in [0, kBuckets). Clamp defensively in case
268 // a_lo_inv_step_ is 0 or rounding pushes us out.
269 auto k = static_cast<std::ptrdiff_t>((a - a_lo) * a_lo_inv_step_);
270 if (k < 0) {
271 k = 0;
272 } else if (k >= static_cast<std::ptrdiff_t>(kBuckets)) {
273 k = static_cast<std::ptrdiff_t>(kBuckets) - 1;
274 }
275 std::size_t i = bucket_to_knot_[static_cast<std::size_t>(k)];
276 // Refine forward. Loop bound matches the worst-case "knots per
277 // bucket" which is ~ceil(n / kBuckets) for any reasonable knot
278 // distribution (1 for n <= kBuckets, larger for dense knot tails
279 // on log-uniform sampling). Practical iterations: 0–1.
280 while (i + 1 < n - 1 && a_[i + 1] <= a) {
281 ++i;
282 }
283 return i;
284}
285
286double CubicSplineCurve::eval(double a) const noexcept {
287 const std::size_t i = locate(a);
288 const double h = a_[i + 1] - a_[i];
289 const double t = (a - a_[i]) / h;
290 return segment_eval(t, h, b_[i], b_[i + 1], M_[i], M_[i + 1]);
291}
292
293double CubicSplineCurve::eval_da(double a) const noexcept {
294 const std::size_t i = locate(a);
295 const double h = a_[i + 1] - a_[i];
296 const double t = (a - a_[i]) / h;
297 return segment_eval_da(t, h, b_[i], b_[i + 1], M_[i], M_[i + 1]);
298}
299
300std::pair<double, double> CubicSplineCurve::bounds() const noexcept {
301 return {b_min_, b_max_};
302}
303
304std::pair<double, double> CubicSplineCurve::a_range() const noexcept {
305 return {a_.front(), a_.back()};
306}
307
308} // namespace region
309} // namespace CoolProp