CoolProp 8.0.0
An open-source fluid property and humid air property database
CubicSplineCurve.h
Go to the documentation of this file.
1#ifndef COOLPROP_REGION_CUBIC_SPLINE_CURVE_H
2#define COOLPROP_REGION_CUBIC_SPLINE_CURVE_H
3
4#include <array>
5#include <cstdint>
6#include <memory>
7#include <utility>
8#include <vector>
9
11
12namespace CoolProp {
13namespace region {
14
15// Natural cubic spline interpolant through n supplied (a_i, b_i) knots,
16// presented as a BoundaryCurve.
17//
18// Construction solves a tridiagonal system for the second derivatives
19// M_i = p''(a_i) under the natural boundary condition M_0 = M_{n-1} = 0.
20// Evaluation is the standard segment-cubic formula in terms of the
21// (b_i, M_i) tables, which makes the analytic derivative immediate.
22//
23// All concrete subclasses of BoundaryCurve provide *analytic* eval_da;
24// here the segment derivative is the piecewise quadratic obtained by
25// differentiating the spline polynomial term-by-term — no finite
26// differences at runtime.
27//
28// SMOOTHNESS CAVEAT. Natural cubic spline assumes the source function
29// is smooth. When the knot sequence captures a kink or near-cusp in
30// the underlying b(a), the global-C² requirement forces oscillation
31// in the neighbourhood of the kink (Gibbs-like overshoot). Phase 2a's
32// e2e validation surfaced this for Propane: at the pressure where
33// the LIQUID-floor lambda crosses the melting line and bumps T up,
34// h_lo(p) has a slope kink, and the resulting spline shows a thin
35// vertical band of elevated error on the LIQUID side at ~250-400
36// kJ/kg. Workarounds when smoothness can't be guaranteed: build a
37// PiecewiseChebyshevCurve over a uniform partition (which absorbs
38// the kink at one piece boundary but trades the global-C² guarantee
39// for a single C⁰ point), or split the data into two splines.
40class CubicSplineCurve final : public BoundaryCurve
41{
42 public:
43 // Build from sorted knots. a values must be strictly increasing.
44 // n >= 2 required (n == 2 degenerates to the line between the two
45 // points with M = 0). Throws std::invalid_argument on malformed
46 // input.
47 static std::unique_ptr<CubicSplineCurve> build(std::vector<double> a, std::vector<double> b);
48
49 // Plain-data snapshot for serialization. Captures the natural-
50 // spline state: knot positions a, knot values b, the
51 // second-derivative table M, and the tight bounds (b_min, b_max)
52 // computed by build() via cubic-extremum root finding.
53 struct State
54 {
55 std::vector<double> a;
56 std::vector<double> b;
57 std::vector<double> M;
58 double b_min;
59 double b_max;
60 };
61 [[nodiscard]] State state() const;
62
63 // Trusted factory: reconstructs a CubicSplineCurve from a State
64 // snapshot WITHOUT re-running the tridiagonal solve. Used by the
65 // SBTL deserializer. Validates the state shape (a/b/M all same
66 // length, a strictly increasing) and rejects malformed input.
67 static std::unique_ptr<CubicSplineCurve> from_state(State s);
68
69 [[nodiscard]] double eval(double a) const noexcept override;
70 [[nodiscard]] double eval_da(double a) const noexcept override;
71 [[nodiscard]] std::pair<double, double> bounds() const noexcept override;
72 [[nodiscard]] std::pair<double, double> a_range() const noexcept override;
73
74 private:
75 CubicSplineCurve(std::vector<double> a, std::vector<double> b, std::vector<double> M, double b_min, double b_max);
76
77 // O(1) amortized interval lookup. Indexed-search variant of the
78 // NR hunt/locate idiom: a precomputed uniform bucket table maps a
79 // linear `a` coordinate to a starting knot index in constant
80 // time, then a tiny refinement loop (typically 0–1 iterations)
81 // nails the final segment. Handles non-uniform knot spacing
82 // without ever falling back to bisection.
83 [[nodiscard]] std::size_t locate(double a) const noexcept;
84
85 // Build bucket_to_knot_ from the current `a_` knot positions.
86 // Called by build() and from_state(); writes only this table, no
87 // other state is touched. Idempotent — safe to call twice.
88 void build_bucket_table_() noexcept;
89
90 std::vector<double> a_; // knot positions, size n
91 std::vector<double> b_; // knot values, size n
92 std::vector<double> M_; // second derivatives at knots, size n
93 double b_min_; // tight min of the spline on [a_[0], a_[n-1]]
94 double b_max_; // tight max of the spline on [a_[0], a_[n-1]]
95
96 // Indexed-search machinery. Hot-path additions; not serialized
97 // (rebuilt on construction / from_state).
98 //
99 // bucket_to_knot_[k] = largest knot index i such that
100 // a_[i] <= a_[0] + k * (a_[n-1] - a_[0]) / kBuckets
101 // is true, clamped to [0, n-2]. At lookup time we hash `a` into
102 // a bucket in O(1), then walk forward at most a few knots. For
103 // log-uniform sampling (typical SBTL pressure curves), a 256-slot
104 // table reliably hits the right segment with zero refinement on
105 // smooth queries.
106 static constexpr std::size_t kBuckets = 256;
107 double a_lo_inv_step_ = 0.0; // = kBuckets / (a_[n-1] - a_[0])
108 std::array<std::uint16_t, kBuckets> bucket_to_knot_{};
109};
110
111} // namespace region
112} // namespace CoolProp
113
114#endif // COOLPROP_REGION_CUBIC_SPLINE_CURVE_H