#!/usr/bin/env python # coding: utf-8 # # # # # Cubic Splines # # ### Modules - Curve Fitting #
# By Jonas Tjemsland, Eilif S. Øyre and Jon Andreas Støvneng #
# Last edited: January 26th 2018 # ___ # # # *Splines* is a type of data *interpolation*, a method of (re)constructing a function between a given set of data points. Interpolation can be used to represent complicated and computationally demanding functions as e.g. polynomials. Then, using a table of a few function evaluations, one can easily approximate the true function with high accuracy. # # In spline interpolation the data is interpolated by several low-degree polynomials. This differs from polynomial interpolation, in which the data is interpolated by a single polynomial of a high order. For a general discussion on polynomial interpolation we refer you to our notebook on [polynomial interpolation](https://nbviewer.jupyter.org/urls/www.numfys.net/media/notebooks/polynomial_interpolation.ipynb). The simplest example of spline interpolation is linear splines, where the data points are simply connected by straight lines. We are going to discuss interpolation by *cubic splines*, which interpolates the polynomials using cubic polynomials with continuous first and second derivatives. We will create an algorithm and some functions for computing a cubic spline. # # We start by importing needed packages and setting common figure parameters. # In[1]: import numpy as np import matplotlib.pyplot as plt import scipy.sparse as sp import scipy.linalg as la get_ipython().run_line_magic('matplotlib', 'inline') # Set some figure parameters newparams = {'figure.figsize': (15, 7), 'axes.grid': False, 'lines.markersize': 10, 'lines.linewidth': 2, 'font.size': 15, 'mathtext.fontset': 'stix', 'font.family': 'STIXGeneral'} plt.rcParams.update(newparams) # ## A Simple Example # Assume that we are given four data points: $\{(0, 0), (1, -1), (2, 2), (3, 0)\}$. The cubic spline interpolating these points is # $$ # S(x) = # \begin{cases} # -\frac{12}{5}x + \frac{7}{5}x^3, & 0\leq x < 1,\\ # -1 + \frac{9}{5}(x - 1) + \frac{21}{5}(x-1)^2 - 3(x-1)^3, & 1 \leq x < 2,\\ # 2 + \frac{6}{5}(x - 2) -\frac{24}{5}(x-2)^2 + \frac{8}{5}(x-2)^3, & 2 \leq x < 3.\\ # \end{cases} # $$ # Let's plot the data points, linear spline and cubic spline! # In[2]: n = 200 x1 = np.linspace(0, 1, n) x2 = np.linspace(1, 2, n) x3 = np.linspace(2, 3, n) # Cubic spline S1 = -12/5*x1 + 7/5*x1**3 S2 = -1 + 9/5*(x2 - 1) + 21/5*(x2 - 1)**2 - 3*(x2 - 1)**3 S3 = 2 + 6/5*(x3 - 2) - 24/5*(x3 - 2)**2 + 8/5*(x3 - 2)**3 plt.plot(np.concatenate([x1, x2, x3]), np.concatenate([S1, S2, S3]), label="Cubic spline") # Linear spline plt.plot([0, 1, 2, 3], [0, -1, 2, 0], "--", label="Linear spline") # Data points plt.plot([0, 1, 2, 3], [0, -1, 2, 0], "o", label="Data points") plt.legend() plt.show() #     **Exercise:** Check that the cubic spline above has continuous first and second derivatives. # ## Definition # # A general cubic spline $S(x)$ interpolating the $n$ data points $\{(x_1,y_1), (x_2, y_2),..., (x_n, y_n)\}$ can be written as # # \begin{equation} # S(x) = # \begin{cases} # S_1(x) &= y_1 + b_1(x - x_1) + c_1(x-x_1)^2 + d_1(x-x_1)^3, & \text{for } x\in[x_1, x_2],\\ # S_2(x) &= y_2 + b_2(x - x_2) + c_2(x-x_2)^2 + d_2(x-x_2)^3, & \text{for } x\in[x_2, x_3],\\ # &\vdots&\\ # S_{n-1} # (x) &= y_{n-1} + b_{n-1}(x - x_{n-1}) + c_{n-1}(x-x_{n-1})^2 + d_{n-1}(x-x_{n-1})^3, & \text{for } x\in[x_{n-1}, x_n],\\ # \end{cases} # \label{eq:spline} # \end{equation} # # for some constants $b_i, c_i, d_i$, $i=1, ..., n$. As mentioned in the introduction, we demand that the spline in continuous and has continuous first and second derivatives. This gives the following properties: [1] # #    1\. $S_i(x_i)=y_i$ and $S_i(x_{i+1})=y_{i+1}$ for $i=1,...,n-1$, #    2\. $S_{i-1}'(x_i)=S_{i}'(x_i)$ and $S_i(x_{i+1})=y_{i+1}$ for $i=2,...,n-1$, #    3\. $S_{i-1}''(x_i)=S_{i}''(x_i)$ and $S_i(x_{i+1})=y_{i+1}$ for $i=2,...,n-1$. # # The three properties make sure that the spline is continuous and smooth. # # The total number of constants $b_i, c_i, d_i$ that we need to compute is $3(n-1)$. # # ## Endpoint conditions # # Note that the total number of conditions imposed by the properties above is $3n-5$. However, the total number of coefficients $b_i, c_i, d_i$ we need to compute is $3(n-1)$. Hence, we need two additional conditions to make the spline $S(x)$ unique. This is achieved thru endpoint conditions. # # There are several choices of endpoint conditions (see e.g. [1]). We will be considering *natural cubic splines*, # #    4a\. $S''_1(x_1)= 0$ and $S''_{n-1}(x_n)=0$, # # and *not-a-knot cubic splines* # #    4b\. $S_1'''(x_2)=S_2'''(x_2), \; S_{n-2}'''(x_{n-1})=S_{n-1}'''(x_{n-1})$. # #     **Exercise:** Which endpoint condition is used in the example above? # ## Algorithm # # From property 1, 2 and 3 we obtain # # \begin{equation} # y_{i+1} = y_{i}+b_{i}(x_{i+1}-x_i) + c_i(x_{i+1}-x_i)^2 + d_i(x_{i+1}-x_i)^3, \quad i=1,...,n-1, # \label{eq:prop1} # \end{equation} # # \begin{equation} # 0 = b_i + 2c_i(x_{i+1}-x_i) + 3d_i(x_{i+1}-x_i)^2-b_{i+1}, \quad i=1,...,n-2, # \label{eq:prop2} # \end{equation} # # and # # \begin{equation} # 0 = c_i+3d_i(x_{i+1}-x_i)-c_{i+1}, \quad i=1,...,n-2, # \label{eq:prop3} # \end{equation} # # respectively. The derivation is straight forward, and is left as an exercise for the reader. If we solve these equations, we obtain the constants $b_i, c_i, d_i$ and thus the cubic spline. To simplify the notation, we define $\Delta x_i=x_{i+1}-x_i$ and $\Delta y_i = y_{i+1}-y_i$. By using equation \eqref{eq:prop1} and \eqref{eq:prop3} we obtain the following expressions for $b_i$ and $d_i$ in terms of the $c$-coefficients: # # \begin{align} # d_i &= \frac{c_{i+1}-c_i}{3\Delta x_i}, \label{eq:d}\\ # b_i &= \frac{\Delta y_i}{\Delta x_i}-\frac{1}{3}\Delta x_i (2 c_i + c_{i+1}).\label{eq:b} # \end{align} # If we insert this into equation \eqref{eq:prop2} we obtain, # # $$\Delta x_ic_i + 2(\Delta x_i + \Delta x_{i+1})c_{i+1}+\Delta x_{i+2}c_{i+2} = 3\left(\frac{\Delta y_{i+1}}{\Delta x_{i+1}}-\frac{\Delta y_{i}}{\Delta x_{i}}\right)$$ # # which is $n-2$ equations for $c_1,..., c_n$. The *natural spline* endpoint condition gives $c_1=c_n=0$. We can write this as the matrix equation # # $$ # \begin{pmatrix} # 1 & 0 & 0 & 0 & \cdots & 0 & 0 \\ # \Delta x_1 & 2\Delta x_1 + 2\Delta x_2 & \Delta x_2 & 0 & \cdots&0&0\\ # 0 & \Delta x_2 & 2\Delta x_2 + 2\Delta x_3 & \Delta x_3 & \cdots&0&0\\ # \vdots & \vdots & \ddots & \ddots & \ddots & \vdots & \vdots\\ # 0 & 0 & 0 & 0 & \Delta x_{n-2} & 2\Delta x_{n-2} + 2\Delta x_{n-1} & \Delta x_{n-1}\\ # 0 & 0 & 0 & 0 & 0 & 0 & 1 # \end{pmatrix} # \begin{pmatrix} # c_1 \\ c_2 \\ \\ \vdots \\ \\ c_n # \end{pmatrix} = # \begin{pmatrix} # 0 \\ # 3\left(\frac{\Delta y_{2}}{\Delta x_2}-\frac{\Delta y_1}{\Delta x_1}\right) \\ # \vdots # \\ # 3\left(\frac{\Delta y_{n-1}}{\Delta x_{n-1}}-\frac{\Delta y_{n-2}}{\Delta x_{n-2}}\right) \\ # 0 # \end{pmatrix}. # $$ # # The algorithm for finding the spline is now quite apparent. We start by constructing the matrix equation, then solve it to find $c_i$ and in turn compute $b_i$ and $d_i$ via the equations \eqref{eq:d} and \eqref{eq:b}. # # The first and last row of the matrix is altered with the *not-a-knot* endpoint conditions. Note that property 4b implies $d_1=d_2$ and $d_{n-2}=d_{n-1}$. If we insert this into equation \eqref{eq:d} for $d_i$, we obtain # $$\Delta x_2 c_1 -(\Delta x_1 + \Delta x_2) c_2 + \Delta x_1 c_3 = 0,$$ # $$\Delta x_{n-1} c_{n-1} -(\Delta x_{n-2} + \Delta x_{n-1})c_{n-1}+\Delta x_{n-2}c_n = 0.$$ # The first row in with the *not-a-knot* end conditions becomes $$(\Delta x_2\;\; -(\Delta x_1 + \Delta x_2)\;\; \Delta x_1\;\; 0\;\; 0\;\; ...).$$ Likewise, the last row becomes $$(0\;\; ... \;\; 0 \;\; \Delta x_{n-1}\;\; -(\Delta x_{n-2} + \Delta x_{n-2})\;\; \Delta x_{n-2}).$$ # # We now proceed to create a function that computes the cubic spline that interpolates the points $\{(x_1,y_1), (x_2, y_2),..., (x_n, y_n)\}$. Note that the matrix equation above is tridiagonal, and it can therefore be stored as a $3\times n$ array and solved effectively by using [scipy.linalg.solve_banded](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_banded.html). In the *not-a-knot*-case the matrix becomes a banded matrix with two upper and lower diagonals. # #     **Exercise:** Derive equations \eqref{eq:prop1}, \eqref{eq:prop2} and \eqref{eq:prop3} from the properties 1-3. # In[3]: def cubic_spline_coeffs(x, y, endpoint="natural"): """ Computes the coefficients in the cubic spline that interpolates the points (x_1,y_1), (x_2, y_2),..., (x_n, y_n). Parameters: x: array_like, shape (n>2,). x-value of the points being interpolated. Values must be real and in strictly increasing order. y: array_like, shape (n>2,) y-value of the points being interpolated. Values must be real. Returns: array, shape (3, n). The coefficients b, c and d, stored in the first, second and third row, respectively. """ x = np.asarray(x) y = np.asarray(y) n = len(x) dx = np.diff(x) dy = np.diff(y) # Find the vector for the right hand side rhs = np.zeros(n) rhs[1:-1] = 3*(dy[1:]/dx[1:] - dy[:-1]/dx[:-1]) # Compute the matrix and store a matrix diagonal ordered form if (endpoint == "natural"): matrix = np.zeros((3, n)) bands = (1, 1) matrix[1, 1:-1] = 2*(dx[:-1] + dx[1:]) # Diagonal matrix[1, 0] = matrix[1, -1] = 1 matrix[0, 2:] = dx[1:] # Upper diagonal matrix[2, :-2] = dx[:-1] # Lower diagonal if (endpoint == "not-a-knot"): matrix = np.zeros((5, n)) bands = (2, 2) matrix[2, 1:-1] = 2*(dx[:-1] + dx[1:]) # Diagonal matrix[1, 2:] = dx[1:] # Upper diagonal matrix[3, :-2] = dx[:-1] # Lower diagonal # First row matrix[2, 0] = dx[1] matrix[1, 1] = -dx[0] - dx[1] matrix[0, 2] = dx[0] # Last row matrix[2, -1] = dx[-2] matrix[3, -2] = -dx[-2] - dx[-3] matrix[4, -3] = dx[-1] # Call a solver for a banded matrix c = la.solve_banded(bands, matrix, rhs, overwrite_ab=True, overwrite_b=True, check_finite=False) # Find the remainding coefficients d = np.diff(c)/(3*dx) b = dy/dx - dx*(2*c[:-1] + c[1:])/3 return b, c, d # We also need a function that can evaluate the spline given the coefficients. # In[4]: def cubic_spline_eval(x, xdata, ydata, b, c, d): """ Evaluates the cubic spline that interpolates {(xdata, ydata)} at x with coefficients b, c and d. Parameters: x: array_like, shape(m,). x-values (axis) at which the spline is evaluated. a, b, c, d: array_like, shapes (n,), (n-1,), (n,) and (n-1,). Coefficients of the spline. Return: array, shape(m,). Function evaluation of the spline. """ x = np.asarray(x) y = np.zeros(len(x)) m = 0 for i in range(len(xdata) - 1): n = np.sum(x < xdata[i + 1]) - m xx = x[m:m + n] - xdata[i] y[m:m + n] = ydata[i] + b[i]*xx + c[i]*xx**2 + d[i]*xx**3 m = m + n xx = x[m:] - xdata[-2] y[m:] = ydata[-2] + b[-1]*xx + c[-2]*xx**2 + d[-1]*xx**3 return y # ## Example # We are now ready to find the cubic spline of a general set of data points. To get some basis for comparison, we will interpolate some points from $\sin(x)$. # In[5]: def func(x): return np.sin(x) xdata = np.asarray([0, 2, 4, 6, 8, 10])*np.pi/5 ydata = func(xdata) x = np.linspace(xdata[0] - 2, xdata[-1] + 2, 200) y = func(x) b, c, d = cubic_spline_coeffs(xdata, ydata, "natural") ya = cubic_spline_eval(x, xdata, ydata, b, c, d) b, c, d = cubic_spline_coeffs(xdata, ydata, "not-a-knot") yb = cubic_spline_eval(x, xdata, ydata, b, c, d) plt.figure() plt.plot(x, y, "--", label=r"$\sin(x)$") plt.plot(x, ya, label="Natural cubic spline") plt.plot(x, yb, label="Not-a-knot cubic spline") plt.plot(xdata, ydata, 'o', label="Data points") plt.xlim(x[0], x[-1]) plt.ylim(np.max(y)*1.1, np.min(y)*1.1) plt.legend() plt.show() # Note that we have defined the cubic splines outside the domain of the data points. In the not-a-knot cubic spline outer polynomials are extended. That is, the polynomial at $x