三点三次自然样条插值


Ⅰ. 背景 –> Ⅱ. 原理 –> Ⅲ. 示例 –> Ⅳ. C 代码与 Python Scipy 库对比


Ⅰ. 背景

前面三次埃尔米特插值中,提到了无人车拐弯,要求旋转相等平缓,样条(Spline)源自于工程领域,工人为了用一条平滑的曲线穿过固定的节点,使用弹性的木条自然安置在这些点列上. 如下图所示.

Oops ! Error ? Browser: https://zh.m.wikipedia.org/wiki/File:Spline_(PSF).png

Ⅱ. 原理

对于自变量从小到大排序的点列:

\{(x_0,y_0),(x_1,y_1),(x_2,y_2),\ldots,(x_n,y_n)\}

在每个节点区间 [x_{k-1},x_{k}],\ k=1,2,3,\ldots,n 内,有不超过三次多项式:

\forall x\in[x_{k-1},x_{k}],\ \ s_k(x)=a_k+b_kx+c_kx^2+d_kx^3

定义样条插值多项式:

S(x)=\mbox{Combine}\mapsto\bigcup_{k=1}^{n} s_k(x)

这个结合构成的大函数节点处函数值与真实观测值相等,且二阶可导:

\forall i,\ S(x_i)=y_i\ \ \& \ \ S(x)\in\mathbb{C}^2[x_0,x_n]

简言之,就是一个每个小区间上都是不超过三次的多项式分段组合构成的大函数,且工程师们希望节点交界处尽量光滑,要求二阶可导. 至于多分段样条插值的原理,公式排版起来极其复杂,参考 USTC 的三弯矩方程求解算法.

http://staff.ustc.edu.cn/~lgliu/Courses/GAMES102_2020/documents/GAMES102-suppl-2-CubicSpline.pdf

这里为了简化数学模型,方便理解,Illusionna 重定义三点三次自然样条插值.

(x_0<x_1<x_2)

(y_0,\ y_1,\ y_2)

\begin{align}
    {\rm Redefine}: S(x)
    =
    \begin{cases}
    s_1(x),\ \ x_0\leqslant x\leqslant x_1
    \\
    s_2(x),\ \ x_1\leqslant x\leqslant x_2
    \notag
    \end{cases}
\end{align}

其中,小函数 s_1(x)s_2(x) 都是三次多项式,满足:

S(x_i)=y_i,\ \ i=1,2,3,\ \ s_1'(x_1)=s_2'(x_1),\ \ s_1''(x_1)=s_2''(x_1)

这里的“自然”插值,转化成数学语言,即端节点处二阶导数恒为零:

s_1''(x_0)=s_2''(x_2)\equiv0


Ⅲ. 示例

引用 <Burden, Annette and Burden, Richard and Faires, J. Numerical Analysis, 9th ed. (2010)【右击,在新的标签页中打开链接】> 的示例点列.

已知三个节点,计算三次自然样条插值多项式【** 类似 Python 的可迭代对象拆包】.

\{^{**}(\bm{\vec{x}},\bm{\vec{y}})\}=\{(1,2),(2,3),(3,5)\}

设小函数:

s_1(x)=a_1+b_1(x-x_0)+c_1(x-x_0)^2+d_1(x-x_0)^3

s_2(x)=a_2+b_2(x-x_1)+c_2(x-x_1)^2+d_2(x-x_1)^3

由于:

s_1(x_0)=s_1(1)=2\Longrightarrow a_1=2

s_2(x_1)=s_2(2)=3\Longrightarrow a_2=3

s_1(2)=3\Longrightarrow a_1+b_1+c_1+d_1=3

s_2(3)=5\Longrightarrow a_2+b_2+c_2+d_2=5

消元:

\begin{align}
    \begin{cases}
    b_1+c_1+d_1=1
    \\
    b_2+c_2+d_2=2
    \end{cases}
\tag{$\ast$}
\end{align}

又因为:

s_1'(x_1)=s_2'(x_1)\ \ \& \ \ s_1''(x_1)=s_2''(x_1)

所以求一阶导数和二阶导数,化简:

\begin{align}
    \begin{cases}
    b_1+2c_1+3d_1=b_2,\ \ {\rm From:}\ s_1'(2)=s_2'(2)
    \\
    c_1+3d_1=c_2,\ \ {\rm From:}\ s_1''(2)=s_2''(2)
    \end{cases}
\tag{$\star$}
\end{align}

并且,因为是自然样条插值:

s_1''(x_0)=s_2''(x_2)\equiv0

所以:

\begin{align}
    \begin{cases}
    c_1=0,\ \ {\rm From:}\ s_1''(1)=0
    \\
    c_2+3d_2=0,\ \ {\rm From:}\ s_2''(3)=0
    \end{cases}
\tag{$\divideontimes$}
\end{align}

联立方程组 (\ast.)(\star.)(\divideontimes.) 整理得到:

\begin{align}
    S(x)=
    \begin{cases}
    \dfrac{1}{4}(x-1)^3+\dfrac{3}{4}(x-1)+2,\ x\in[1,2]
    \\
    -\dfrac{1}{4}(x-2)^3+\dfrac{3}{4}(x-2)^2+\dfrac{3}{2}(x-2)+3,\ x\in[2,3]
    \end{cases}
\notag
\end{align}


Ⅳ. C 代码与 Scipy 对比

C 代码算法实现原理建议先阅读:

<Natural Cubic Spline Algorithm【右击,在新的标签页中打开链接】>

<NaturalCubicSpline.tex【点击下载 .tex 源文件】>

C 代码只实现求解多分段样条插值多项式系数,并没有插值函数,若需要则先编译成动态链接库供 Python 调用进行插值.

NaturalCubicSpline.c
 1/*
 2System --> Linux & gcc8.1.0
 3File ----> NaturalCubicSpline.c
 4Author --> Illusionna
 5Create --> 2024/2/21 22:16:30
 6'''
 7-*- Encoding: UTF-8 -*-
 8*/
 9
10// 生成 Python 调用需要的动态链接库: gcc --share -o spline.dll NaturalCubicSpline.c
11
12# include <stdio.h>
13# include <malloc.h>
14
15double ** Spline(double * X, double * Y, int n){
16    int i, j;
17    n--;
18    double stepArray[n], alphaArray[n], lArray[n+1], muArray[n+1], zArray[n+1], cArray[n+1], bArray[n], dArray[n];
19    // Step 1.
20    for (i=0; i<n; ++i){
21        stepArray[i] = X[i+1] - X[i];
22    }
23    // Step 2.
24    for (i=1; i<n; ++i){
25        alphaArray[i] = (3 * (Y[i+1] - Y[i]) / stepArray[i]) - (3 * (Y[i] - Y[i-1]) / stepArray[i-1]);
26    }
27    // Step 3.
28    lArray[0] = 1;
29    muArray[0] = 0;
30    zArray[0] = 0;
31    // Step 4.
32    for (i=1; i<n; ++i){
33        lArray[i] = 2 * (X[i+1] - X[i-1]) - stepArray[i-1] * muArray[i-1];
34        muArray[i] = stepArray[i] / lArray[i];
35        zArray[i] = (alphaArray[i] - stepArray[i-1] * zArray[i-1]) / lArray[i];
36    }
37    // Step 5.
38    lArray[n] = 1;
39    zArray[n] = 0;
40    cArray[n] = 0;
41    // Step 6.
42    for (j=n-1; j>=0; --j){
43        cArray[j] = zArray[j] - muArray[j] * cArray[j+1];
44        bArray[j] = ((Y[j+1] - Y[j]) / stepArray[j]) - (stepArray[j] * (cArray[j+1] + 2 * cArray[j]) / 3);
45        dArray[j] = (cArray[j+1] - cArray[j]) / (3 * stepArray[j]);
46    }
47    // Information.
48    printf("\033[036mThe coefficients of each interval cubic polynomial:\033[0m\n");
49    printf("S(x)=a+b(x-xk)+c(x-xk)^2+d(x-xk)^3, xk < x < x_{k+1}\n");
50    printf("%5s %9s %8s %8s %8s\n", "xk", "a", "b", "c", "d");
51    for (i=0; i<n; ++i){
52        printf("%.5f %9.5f %8.5f %9.5f %9.5f\n", X[i], Y[i], bArray[i], cArray[i], dArray[i]);
53    }
54    double ** result;
55    int columns = 5;
56    result = (double**)malloc(n * sizeof(double*));
57    for (i=0; i<n; ++i){
58        result[i] = (double *)malloc(columns * sizeof(double));
59        result[i][0] = X[i];
60        result[i][1] = Y[i];
61        result[i][2] = bArray[i];
62        result[i][3] = cArray[i];
63        result[i][4] = dArray[i];
64    }
65    return result;
66}
67
68/*
69main() 为测试代码.
70*/
71
72// int main(){
73//     double X[] = {1, 2, 3};
74//     double Y[] = {2, 3, 5};
75//     printf("\033[H\033[J");
76//     int n = sizeof(X) / sizeof(X[0]);
77//     double **cs;
78//     cs = Spline(X, Y, n);
79//     printf("\033[033mTest for getting the return value (2-dimension array pointer)\033[0m.\n");
80//     for (int i=0; i<n-1; ++i){
81//         for (int j=0; j<5; ++j){
82//             printf("%f ", cs[i][j]);
83//         }
84//     }
85//     return 0;
86// }
Oops ! Error ?

NaturalCubicSpline.py
  1'''
  2# System --> Windows & Python3.8.0
  3# File ----> NaturalCubicSpline.py
  4# Author --> Illusionna
  5# Create --> 2024/2/22 21:08:56
  6'''
  7# -*- Encoding: UTF-8 -*-
  8
  9
 10import os
 11import ctypes   # 先编译生成动态链接库: gcc --share -o spline.dll NaturalCubicSpline.c
 12import numpy as np
 13from bisect import bisect_left
 14import matplotlib.pyplot as plt
 15from scipy.interpolate import CubicSpline   # 与官方标准库比对测试用.
 16
 17def cls() -> None:
 18    os.system('cls')
 19    global root
 20    root = os.getcwd()
 21cls()
 22
 23
 24class NATURAL_CUBIC_SPLINE:
 25    def __init__(self, X:list, Y:list) -> None:
 26        self.__X = X
 27        self.__Y = Y
 28        self.__pos = False
 29        if len(X) == len(Y):
 30            lib = ctypes.CDLL(root + './spline.dll')
 31            self.__Spline = lib.__getattr__('Spline')
 32            self.__Spline.restype = ctypes.POINTER(ctypes.POINTER(ctypes.c_double))
 33            self.__Spline.argtypes = [
 34                ctypes.POINTER(ctypes.c_double),
 35                ctypes.POINTER(ctypes.c_double),
 36                ctypes.c_int
 37            ]
 38        else:
 39            assert print(f'\033[031m数组 X 长度: {len(X)}, 数组 Y 长度 {len(Y)}, 长度不一致.\033[0m')
 40
 41    def Coefficients(self) -> list:
 42        ptr = ctypes.c_double * len(self.__X)
 43        a = self.__Spline(
 44            ptr(* self.__X),
 45            ptr(* self.__Y),
 46            len(self.__X)
 47        )
 48        self.__coefficients = [
 49            [a[i][j] for j in range(0, 5, 1)]
 50            for i in range(0, len(self.__X)-1, 1)
 51        ]
 52        self.__pos = True
 53        return self.__coefficients
 54
 55    def Interpolate(self, x:float) -> 'function':
 56        def Parameters(period:int) -> tuple:
 57            xk = self.__coefficients[period][0]
 58            a = self.__coefficients[period][1]
 59            b = self.__coefficients[period][2]
 60            c = self.__coefficients[period][3]
 61            d = self.__coefficients[period][4]
 62            return (xk, a, b, c, d)
 63
 64        def Calculate() -> 'function':
 65            idx = bisect_left(self.__X, x)
 66            n = len(self.__X)
 67            if (idx == 0) | (idx == 1):
 68                (xk, a, b, c, d) = Parameters(0)
 69            elif (idx == n) | (idx == n-1):
 70                (xk, a, b, c, d) = Parameters(-1)
 71            else:
 72                (xk, a, b, c, d) = Parameters(idx-1)
 73            y = lambda x: a + b*(x-xk) + c*(x-xk)**2 + d*(x-xk)**3
 74            return y(x)
 75
 76        if self.__pos == True:
 77            return Calculate()
 78        else:
 79            self.Coefficients()
 80            return Calculate()
 81
 82
 83def Test1() -> None:
 84    X = [1, 2, 3, 7.23]
 85    Y = [2, 3, 5, -1.75]
 86    nodes:list = [1.25, 1.5, 1.75, 2.25, 2.5, 2.75, 4, 5, 6]    # 待预测节点横坐标向量.
 87    # ---------------------------------------------------------------------------------
 88    obj = NATURAL_CUBIC_SPLINE(
 89        X = X,
 90        Y = Y
 91    )
 92    # print(obj.Coefficients())     # 打印样条插值多项式系数.
 93    f = lambda x: obj.Interpolate(x)
 94    print(f'\033[036mIllusionna 三次自然样条插值\033[0m预测结果:\n{list(map(f, nodes))}')
 95    # ---------------------------------------------------------------------------------
 96    cs = CubicSpline(
 97        x = X,
 98        y = Y
 99    )
100    print(f'\033[033mScipy 库三次样条插值\033[0m预测结果:\n{cs(nodes).tolist()}')
101
102def Test2() -> None:
103    X = np.linspace(0, 20, 100)
104    Y = np.sin(X)
105    nodes:list = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 16, 18, 19]    # 待预测节点横坐标向量.
106    # ---------------------------------------------------------------------------------
107    obj = NATURAL_CUBIC_SPLINE(
108        X = X,
109        Y = Y
110    )
111    f = lambda x: obj.Interpolate(x)
112    print(f'\033[036mIllusionna 三次自然样条插值\033[0m预测结果:\n{list(map(f, nodes))}')
113    # ---------------------------------------------------------------------------------
114    cs = CubicSpline(
115        x = X,
116        y = Y
117    )
118    print(f'\033[033mScipy 库三次样条插值\033[0m预测结果:\n{cs(nodes).tolist()}')
119    # ---------------------------------------------------------------------------------
120    plt.scatter(X, Y, s=10)
121    plt.plot(nodes, list(map(f, nodes)), 'r-')
122    plt.plot(nodes, cs(nodes).tolist(), 'g--')
123    plt.legend(['Observations', 'Illusionna predicts nodes', 'Scipy predicts nodes'])
124    plt.show()
125
126
127if __name__ == '__main__':
128    Test1()
129    # Test2()
Oops ! Error ?