/* * spline.h * * simple cubic spline interpolation library without external * dependencies * * --------------------------------------------------------------------- * Copyright (C) 2011, 2014, 2016, 2021 Tino Kluge (ttk448 at gmail.com) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * --------------------------------------------------------------------- * */ #ifndef TK_SPLINE_H #define TK_SPLINE_H #include #include #include #include #include #ifdef HAVE_SSTREAM #include #include #endif // HAVE_SSTREAM // not ideal but disable unused-function warnings // (we get them because we have implementations in the header file, // and this is because we want to be able to quickly separate them // into a cpp file if necessary) #if !defined(_MSC_VER) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" #endif namespace tk { // spline interpolation class spline { public: // spline types enum spline_type { linear = 10, // linear interpolation cspline = 30, // cubic splines (classical C^2) cspline_hermite = 31 // cubic hermite splines (local, only C^1) }; // boundary condition type for the spline end-points enum bd_type { first_deriv = 1, second_deriv = 2, not_a_knot = 3 }; protected: std::vector m_x,m_y; // x,y coordinates of points // interpolation parameters // f(x) = a_i + b_i*(x-x_i) + c_i*(x-x_i)^2 + d_i*(x-x_i)^3 // where a_i = y_i, or else it won't go through grid points std::vector m_b,m_c,m_d; // spline coefficients double m_c0; // for left extrapolation spline_type m_type; bd_type m_left, m_right; double m_left_value, m_right_value; bool m_made_monotonic; void set_coeffs_from_b(); // calculate c_i, d_i from b_i size_t find_closest(double x) const; // closest idx so that m_x[idx]<=x public: // default constructor: set boundary condition to be zero curvature // at both ends, i.e. natural splines spline(): m_type(cspline), m_left(second_deriv), m_right(second_deriv), m_left_value(0.0), m_right_value(0.0), m_made_monotonic(false) { ; } spline(const std::vector& X, const std::vector& Y, spline_type type = cspline, bool make_monotonic = false, bd_type left = second_deriv, double left_value = 0.0, bd_type right = second_deriv, double right_value = 0.0 ): m_type(type), m_left(left), m_right(right), m_left_value(left_value), m_right_value(right_value), m_made_monotonic(false) // false correct here: make_monotonic() sets it { this->set_points(X,Y,m_type); if(make_monotonic) { this->make_monotonic(); } } // modify boundary conditions: if called it must be before set_points() void set_boundary(bd_type left, double left_value, bd_type right, double right_value); // set all data points (cubic_spline=false means linear interpolation) void set_points(const std::vector& x, const std::vector& y, spline_type type=cspline); // adjust coefficients so that the spline becomes piecewise monotonic // where possible // this is done by adjusting slopes at grid points by a non-negative // factor and this will break C^2 // this can also break boundary conditions if adjustments need to // be made at the boundary points // returns false if no adjustments have been made, true otherwise bool make_monotonic(); // evaluates the spline at point x double operator() (double x) const; double deriv(int order, double x) const; // solves for all x so that: spline(x) = y std::vector solve(double y, bool ignore_extrapolation=true) const; // returns the input data points std::vector get_x() const { return m_x; } std::vector get_y() const { return m_y; } double get_x_min() const { assert(!m_x.empty()); return m_x.front(); } double get_x_max() const { assert(!m_x.empty()); return m_x.back(); } #ifdef HAVE_SSTREAM // spline info string, i.e. spline type, boundary conditions etc. std::string info() const; #endif // HAVE_SSTREAM }; namespace internal { // band matrix solver class band_matrix { private: std::vector< std::vector > m_upper; // upper band std::vector< std::vector > m_lower; // lower band public: band_matrix() {}; // constructor band_matrix(int dim, int n_u, int n_l); // constructor ~band_matrix() {}; // destructor void resize(int dim, int n_u, int n_l); // init with dim,n_u,n_l int dim() const; // matrix dimension int num_upper() const { return (int)m_upper.size()-1; } int num_lower() const { return (int)m_lower.size()-1; } // access operator double & operator () (int i, int j); // write double operator () (int i, int j) const; // read // we can store an additional diagonal (in m_lower) double& saved_diag(int i); double saved_diag(int i) const; void lu_decompose(); std::vector r_solve(const std::vector& b) const; std::vector l_solve(const std::vector& b) const; std::vector lu_solve(const std::vector& b, bool is_lu_decomposed=false); }; double get_eps(); std::vector solve_cubic(double a, double b, double c, double d, int newton_iter=0); } // namespace internal // --------------------------------------------------------------------- // implementation part, which could be separated into a cpp file // --------------------------------------------------------------------- // spline implementation // ----------------------- void spline::set_boundary(spline::bd_type left, double left_value, spline::bd_type right, double right_value) { assert(m_x.size()==0); // set_points() must not have happened yet m_left=left; m_right=right; m_left_value=left_value; m_right_value=right_value; } void spline::set_coeffs_from_b() { assert(m_x.size()==m_y.size()); assert(m_x.size()==m_b.size()); assert(m_x.size()>2); size_t n=m_b.size(); if(m_c.size()!=n) m_c.resize(n); if(m_d.size()!=n) m_d.resize(n); for(size_t i=0; i& x, const std::vector& y, spline_type type) { assert(x.size()==y.size()); assert(x.size()>=3); // not-a-knot with 3 points has many solutions if(m_left==not_a_knot || m_right==not_a_knot) assert(x.size()>=4); m_type=type; m_made_monotonic=false; m_x=x; m_y=y; int n = (int) x.size(); // check strict monotonicity of input vector x for(int i=0; i rhs(n); for(int i=1; i2); bool modified = false; const int n=(int)m_x.size(); // make sure: input data monotonic increasing --> b_i>=0 // input data monotonic decreasing --> b_i<=0 for(int i=0; i=m_y[i]) && (m_y[i]>=m_y[ip1]) && m_b[i]>0.0) ) { modified=true; m_b[i]=0.0; } } // if input data is monotonic (b[i], b[i+1], avg have all the same sign) // ensure a sufficient criteria for monotonicity is satisfied: // sqrt(b[i]^2+b[i+1]^2) <= 3 |avg|, with avg=(y[i+1]-y[i])/h, for(int i=0; i=0.0 && m_b[i+1]>=0.0 && avg>0.0) || (m_b[i]<=0.0 && m_b[i+1]<=0.0 && avg<0.0) ) { // input data is monotonic double r = sqrt(m_b[i]*m_b[i]+m_b[i+1]*m_b[i+1])/std::fabs(avg); if(r>3.0) { // sufficient criteria for monotonicity: r<=3 // adjust b[i] and b[i+1] modified=true; m_b[i] *= (3.0/r); m_b[i+1] *= (3.0/r); } } } if(modified==true) { set_coeffs_from_b(); m_made_monotonic=true; } return modified; } // return the closest idx so that m_x[idx] <= x (return 0 if x::const_iterator it; it=std::upper_bound(m_x.begin(),m_x.end(),x); // *it > x size_t idx = std::max( int(it-m_x.begin())-1, 0); // m_x[idx] <= x return idx; } double spline::operator() (double x) const { // polynomial evaluation using Horner's scheme // TODO: consider more numerically accurate algorithms, e.g.: // - Clenshaw // - Even-Odd method by A.C.R. Newbery // - Compensated Horner Scheme size_t n=m_x.size(); size_t idx=find_closest(x); double h=x-m_x[idx]; double interpol; if(xm_x[n-1]) { // extrapolation to the right interpol=(m_c[n-1]*h + m_b[n-1])*h + m_y[n-1]; } else { // interpolation interpol=((m_d[idx]*h + m_c[idx])*h + m_b[idx])*h + m_y[idx]; } return interpol; } double spline::deriv(int order, double x) const { assert(order>0); size_t n=m_x.size(); size_t idx = find_closest(x); double h=x-m_x[idx]; double interpol; if(xm_x[n-1]) { // extrapolation to the right switch(order) { case 1: interpol=2.0*m_c[n-1]*h + m_b[n-1]; break; case 2: interpol=2.0*m_c[n-1]; break; default: interpol=0.0; break; } } else { // interpolation switch(order) { case 1: interpol=(3.0*m_d[idx]*h + 2.0*m_c[idx])*h + m_b[idx]; break; case 2: interpol=6.0*m_d[idx]*h + 2.0*m_c[idx]; break; case 3: interpol=6.0*m_d[idx]; break; default: interpol=0.0; break; } } return interpol; } std::vector spline::solve(double y, bool ignore_extrapolation) const { std::vector x; // roots for the entire spline std::vector root; // roots for each piecewise cubic const size_t n=m_x.size(); // left extrapolation if(ignore_extrapolation==false) { root = internal::solve_cubic(m_y[0]-y,m_b[0],m_c0,0.0,1); for(size_t j=0; j0) ? (m_x[i]-m_x[i-1]) : 0.0; double eps = internal::get_eps()*512.0*std::min(h,1.0); if( (-eps<=root[j]) && (root[j]0 && x.back()+eps > new_root) { x.back()=new_root; // avoid spurious duplicate roots } else { x.push_back(new_root); } } } } // right extrapolation if(ignore_extrapolation==false) { root = internal::solve_cubic(m_y[n-1]-y,m_b[n-1],m_c[n-1],0.0,1); for(size_t j=0; j0); assert(n_u>=0); assert(n_l>=0); m_upper.resize(n_u+1); m_lower.resize(n_l+1); for(size_t i=0; i0) { return (int)m_upper[0].size(); } else { return 0; } } // defines the new operator (), so that we can access the elements // by A(i,j), index going from i=0,...,dim()-1 double & band_matrix::operator () (int i, int j) { int k=j-i; // what band is the entry assert( (i>=0) && (i=0) && (j diagonal, k<0 lower left part, k>0 upper right part if(k>=0) return m_upper[k][i]; else return m_lower[-k][i]; } double band_matrix::operator () (int i, int j) const { int k=j-i; // what band is the entry assert( (i>=0) && (i=0) && (j diagonal, k<0 lower left part, k>0 upper right part if(k>=0) return m_upper[k][i]; else return m_lower[-k][i]; } // second diag (used in LU decomposition), saved in m_lower double band_matrix::saved_diag(int i) const { assert( (i>=0) && (i=0) && (idim(); i++) { assert(this->operator()(i,i)!=0.0); this->saved_diag(i)=1.0/this->operator()(i,i); j_min=std::max(0,i-this->num_lower()); j_max=std::min(this->dim()-1,i+this->num_upper()); for(int j=j_min; j<=j_max; j++) { this->operator()(i,j) *= this->saved_diag(i); } this->operator()(i,i)=1.0; // prevents rounding errors } // Gauss LR-Decomposition for(int k=0; kdim(); k++) { i_max=std::min(this->dim()-1,k+this->num_lower()); // num_lower not a mistake! for(int i=k+1; i<=i_max; i++) { assert(this->operator()(k,k)!=0.0); x=-this->operator()(i,k)/this->operator()(k,k); this->operator()(i,k)=-x; // assembly part of L j_max=std::min(this->dim()-1,k+this->num_upper()); for(int j=k+1; j<=j_max; j++) { // assembly part of R this->operator()(i,j)=this->operator()(i,j)+x*this->operator()(k,j); } } } } // solves Ly=b std::vector band_matrix::l_solve(const std::vector& b) const { assert( this->dim()==(int)b.size() ); std::vector x(this->dim()); int j_start; double sum; for(int i=0; idim(); i++) { sum=0; j_start=std::max(0,i-this->num_lower()); for(int j=j_start; joperator()(i,j)*x[j]; x[i]=(b[i]*this->saved_diag(i)) - sum; } return x; } // solves Rx=y std::vector band_matrix::r_solve(const std::vector& b) const { assert( this->dim()==(int)b.size() ); std::vector x(this->dim()); int j_stop; double sum; for(int i=this->dim()-1; i>=0; i--) { sum=0; j_stop=std::min(this->dim()-1,i+this->num_upper()); for(int j=i+1; j<=j_stop; j++) sum += this->operator()(i,j)*x[j]; x[i]=( b[i] - sum ) / this->operator()(i,i); } return x; } std::vector band_matrix::lu_solve(const std::vector& b, bool is_lu_decomposed) { assert( this->dim()==(int)b.size() ); std::vector x,y; if(is_lu_decomposed==false) { this->lu_decompose(); } y=this->l_solve(b); x=this->r_solve(y); return x; } // machine precision of a double, i.e. the successor of 1 is 1+eps double get_eps() { //return std::numeric_limits::epsilon(); // __DBL_EPSILON__ return 2.2204460492503131e-16; // 2^-52 } // solutions for a + b*x = 0 std::vector solve_linear(double a, double b) { std::vector x; // roots if(b==0.0) { if(a==0.0) { // 0*x = 0 x.resize(1); x[0] = 0.0; // any x solves it but we need to pick one return x; } else { // 0*x + ... = 0, no solution return x; } } else { x.resize(1); x[0] = -a/b; return x; } } // solutions for a + b*x + c*x^2 = 0 std::vector solve_quadratic(double a, double b, double c, int newton_iter=0) { if(c==0.0) { return solve_linear(a,b); } // rescale so that we solve x^2 + 2p x + q = (x+p)^2 + q - p^2 = 0 double p=0.5*b/c; double q=a/c; double discr = p*p-q; const double eps=0.5*internal::get_eps(); double discr_err = (6.0*(p*p)+3.0*fabs(q)+fabs(discr))*eps; std::vector x; // roots if(fabs(discr)<=discr_err) { // discriminant is zero --> one root x.resize(1); x[0] = -p; } else if(discr<0) { // no root } else { // two roots x.resize(2); x[0] = -p - sqrt(discr); x[1] = -p + sqrt(discr); } // improve solution via newton steps for(size_t i=0; i1e-8) { x[i] -= f/f1; } } } return x; } // solutions for the cubic equation: a + b*x +c*x^2 + d*x^3 = 0 // this is a naive implementation of the analytic solution without // optimisation for speed or numerical accuracy // newton_iter: number of newton iterations to improve analytical solution // see also // gsl: gsl_poly_solve_cubic() in solve_cubic.c // octave: roots.m - via eigenvalues of the Frobenius companion matrix std::vector solve_cubic(double a, double b, double c, double d, int newton_iter) { if(d==0.0) { return solve_quadratic(a,b,c,newton_iter); } // convert to normalised form: a + bx + cx^2 + x^3 = 0 if(d!=1.0) { a/=d; b/=d; c/=d; } // convert to depressed cubic: z^3 - 3pz - 2q = 0 // via substitution: z = x + c/3 std::vector z; // roots of the depressed cubic double p = -(1.0/3.0)*b + (1.0/9.0)*(c*c); double r = 2.0*(c*c)-9.0*b; double q = -0.5*a - (1.0/54.0)*(c*r); double discr=p*p*p-q*q; // discriminant // calculating numerical round-off errors with assumptions: // - each operation is precise but each intermediate result x // when stored has max error of x*eps // - only multiplication with a power of 2 introduces no new error // - a,b,c,d and some fractions (e.g. 1/3) have rounding errors eps // - p_err << |p|, q_err << |q|, ... (this is violated in rare cases) // would be more elegant to use boost::numeric::interval const double eps = internal::get_eps(); double p_err = eps*((3.0/3.0)*fabs(b)+(4.0/9.0)*(c*c)+fabs(p)); double r_err = eps*(6.0*(c*c)+18.0*fabs(b)+fabs(r)); double q_err = 0.5*fabs(a)*eps + (1.0/54.0)*fabs(c)*(r_err+fabs(r)*3.0*eps) + fabs(q)*eps; double discr_err = (p*p) * (3.0*p_err + fabs(p)*2.0*eps) + fabs(q) * (2.0*q_err + fabs(q)*eps) + fabs(discr)*eps; // depending on the discriminant we get different solutions if(fabs(discr)<=discr_err) { // discriminant zero: one or two real roots if(fabs(p)<=p_err) { // p and q are zero: single root z.resize(1); z[0] = 0.0; // triple root } else { z.resize(2); z[0] = 2.0*q/p; // single root z[1] = -0.5*z[0]; // double root } } else if(discr>0) { // three real roots: via trigonometric solution z.resize(3); double ac = (1.0/3.0) * acos( q/(p*sqrt(p)) ); double sq = 2.0*sqrt(p); z[0] = sq * cos(ac); z[1] = sq * cos(ac-2.0*M_PI/3.0); z[2] = sq * cos(ac-4.0*M_PI/3.0); } else if (discr<0.0) { // single real root: via Cardano's fromula z.resize(1); double sgnq = (q >= 0 ? 1 : -1); double basis = fabs(q) + sqrt(-discr); double C = sgnq * pow(basis, 1.0/3.0); // c++11 has std::cbrt() z[0] = C + p/C; } for(size_t i=0; i1e-8) { z[i] -= f/f1; } } } // ensure if a=0 we get exactly x=0 as root // TODO: remove this fudge if(a==0.0) { assert(z.size()>0); // cubic should always have at least one root double xmin=fabs(z[0]); size_t imin=0; for(size_t i=1; ifabs(z[i])) { xmin=fabs(z[i]); imin=i; } } z[imin]=0.0; // replace the smallest absolute value with 0 } std::sort(z.begin(), z.end()); return z; } } // namespace internal } // namespace tk #if !defined(_MSC_VER) #pragma GCC diagnostic pop #endif #endif /* TK_SPLINE_H */