CoolProp 8.0.0
An open-source fluid property and humid air property database
PiecewiseChebyshevCurve.h
Go to the documentation of this file.
1#ifndef COOLPROP_REGION_PIECEWISE_CHEBYSHEV_CURVE_H
2#define COOLPROP_REGION_PIECEWISE_CHEBYSHEV_CURVE_H
3
4#include <cstddef>
5#include <cstdint>
6#include <functional>
7#include <memory>
8#include <utility>
9#include <vector>
10
12
13namespace CoolProp {
14namespace region {
15
16// 1D curve b = f(a) represented as a piecewise Chebyshev expansion.
17//
18// The primary axis can be parametrised in two ways:
19// - LINEAR : pieces are uniform in a; Chebyshev variable s in [-1,1]
20// maps to a via a = a_mid + (a_hi - a_lo)/2 * s.
21// - LOG : pieces are uniform in log(a); s maps to log(a).
22//
23// Each piece carries its own Chebyshev coefficient vector c_0..c_N for
24// the expansion f(a) = sum_{k=0}^{N} c_k T_k(s) on its sub-interval.
25//
26// Analytic derivative df/da is computed via the standard Chebyshev
27// recurrence on the coefficients (no finite differences) — see
28// eval_da().
29//
30// CONTINUITY CAVEAT. The curve is value-continuous across piece
31// boundaries (adjacent Chebyshev fits share their endpoint samples)
32// but the *derivative* is generally discontinuous — each piece's
33// expansion is independent. When this curve drives the secondary
34// axis of a Region, the kink at each piece boundary propagates
35// through η normalization into the SVD reconstruction and appears as
36// a thin band of elevated error at the corresponding axis value.
37// Phase 2a's e2e validation surfaced this empirically for Water:
38// SAT_PIECES=6 placed a visible "green band" at 4/6 of the log-p
39// range (~770 kPa). When a smoother boundary is required, use
40// CubicSplineCurve (C² continuous everywhere).
41//
42// Extracted and generalised from the ihb/SBTL Cheb1D / Cheb1DPiece
43// classes (originally parametrised on log(p) only).
45{
46 public:
47 enum class ParamScale : std::uint8_t
48 {
49 LINEAR,
50 LOG
51 };
52
53 // Build by sampling f at Chebyshev-Lobatto nodes inside each of
54 // n_pieces sub-intervals of [a_lo, a_hi], with each piece carrying a
55 // degree-N expansion (so N+1 nodes per piece including the endpoints).
56 //
57 // f is called O(n_pieces * (N+1)) times. Adjacent pieces share an
58 // endpoint sample; the build computes them twice for simplicity (the
59 // extra cost is in the wash and keeps the code clean).
60 static std::unique_ptr<PiecewiseChebyshevCurve> build(double a_lo, double a_hi, std::size_t n_pieces, std::size_t degree, ParamScale scale,
61 const std::function<double(double)>& f);
62
63 // Plain-data snapshot of a single piece's coefficient state.
65 {
66 double t_lo;
67 double t_hi;
69 double t_mid;
70 std::vector<double> coeffs; // size degree+1
71 std::vector<double> deriv_coeffs; // size degree (drops trivially-zero last coeff)
72 };
73 // Snapshot of the whole curve.
74 struct State
75 {
76 double a_lo;
77 double a_hi;
79 std::vector<PieceState> pieces;
80 double b_min;
81 double b_max;
82 };
83 [[nodiscard]] State state() const;
84
85 // Trusted factory: reconstructs from a previously-captured State
86 // without re-sampling f. Used by the SBTL deserializer.
87 static std::unique_ptr<PiecewiseChebyshevCurve> from_state(State s);
88
89 [[nodiscard]] double eval(double a) const noexcept override;
90 [[nodiscard]] double eval_da(double a) const noexcept override;
91 [[nodiscard]] std::pair<double, double> bounds() const noexcept override;
92 [[nodiscard]] std::pair<double, double> a_range() const noexcept override;
93
94 private:
95 struct Piece
96 {
97 double t_lo; // transformed left bound (a or log a)
98 double t_hi; // transformed right bound
99 double inv_half_span; // 2 / (t_hi - t_lo); for s = inv_half_span*(t - t_mid)
100 double t_mid; // (t_lo + t_hi) / 2
101 std::vector<double> coeffs; // size degree+1; Chebyshev coefficients
102 // Pre-computed derivative coefficients on this piece — size
103 // degree (the trivially-zero c_N coefficient of the derivative
104 // expansion is dropped). eval_da used to recompute these on
105 // every call (heap allocation + recurrence); precomputing here
106 // saves the malloc and one wasted Clenshaw step per call.
107 std::vector<double> deriv_coeffs;
108 };
109
110 PiecewiseChebyshevCurve(double a_lo, double a_hi, ParamScale scale, std::vector<Piece> pieces, double b_min, double b_max);
111
112 [[nodiscard]] std::size_t locate_piece(double t) const noexcept;
113
114 // Transform a in [a_lo, a_hi] into the Chebyshev primary coordinate
115 // (a itself for LINEAR, log(a) for LOG).
116 [[nodiscard]] double to_t(double a) const noexcept;
117
118 // d t / d a — the Jacobian of the primary-axis transform.
119 // LINEAR : 1
120 // LOG : 1 / a
121 [[nodiscard]] double dt_da(double a) const noexcept;
122
123 double a_lo_;
124 double a_hi_;
125 ParamScale scale_;
126 std::vector<Piece> pieces_;
127 double b_min_;
128 double b_max_;
129};
130
131} // namespace region
132} // namespace CoolProp
133
134#endif // COOLPROP_REGION_PIECEWISE_CHEBYSHEV_CURVE_H