• 算法特征:
    ①. pre-smoothing提取低频残差; ②. 向下插值计算残差补偿; ③.向上插值填充残差补偿; ④. post-smoothing降低整体残差
  • 算法推导:
    Part Ⅰ: 算法原理
    考虑一般线性系统:
    \begin{equation}
    Ax = b
    \label{eq_1}
    \end{equation}
    给定某初始值$x^{0}$, 残差为:
    \begin{equation}
    r^{0} = b - Ax^{0}
    \label{eq_2}
    \end{equation}
    以该残差为基础, 计算残差补偿:
    \begin{equation}
    \delta x^{0} = A^{-1}r^{0}
    \label{eq_3}
    \end{equation}
    填充残差补偿, 式(\ref{eq_1})解表示如下:
    \begin{equation}
    x = x^{0} + \delta x^{0}
    \label{eq_4}
    \end{equation}
    Part Ⅱ: 误差(即残差补偿)特征
    现以Jacobi迭代为例, 分解系数矩阵$A$, 式(\ref{eq_1})转换为
    \begin{equation}
    (L + D + U)x = b
    \label{eq_5}
    \end{equation}
    其中, $L$、$D$、$U$分别为系数矩阵$A$的下三角矩阵、对角矩阵及上三角矩阵. Jacobi迭代公式表示如下:
    \begin{equation}
    x^{k+1} = Jx^k + D^{-1}b
    \label{eq_6}
    \end{equation}
    其中, $J = -D^{-1}(L + U)$, 代表Jacobi迭代矩阵. 定义第$k$步迭代误差:
    \begin{equation}
    e^k = x^k - x
    \label{eq_7}
    \end{equation}
    根据式(\ref{eq_6})可知, 迭代误差满足如下条件:
    \begin{equation}
    e^{k+1} = Je^k
    \label{eq_8}
    \end{equation}
    结合上式, 对矩阵$J$进行特征分解, 则有
    \begin{equation}
    e^k = V\Lambda^kV^{-1}e^0
    \label{eq_9}
    \end{equation}
    其中, 矩阵$V$和$\Lambda$分别由矩阵$J$的本征矢和本征值组成. 显然, Jacobi迭代的收敛性由相应迭代矩阵的本征值决定. 若矩阵$J$所有本征值绝对值均小于1, 则Jacobi迭代收敛; 否则, 不收敛.
    现以有限差分法离散化1维Poisson's equation($\Delta u = f$)为例, 采用Jacobi迭代求解该线性系统, 并分析误差成分:
    \begin{equation}
    \begin{split}
    A &= \begin{bmatrix}-2 & 1 &  &  \\  1 & \ddots & \ddots &  \\ & \ddots & \ddots & 1  \\ &  & 1 & -2 \end{bmatrix} \frac{1}{\Delta x^2}   \\
    D &= -2I\frac{1}{\Delta x^2}  \\
    L + U &= \begin{bmatrix} 0 & 1 &  & \\ 1 & \ddots & \ddots & \\ & \ddots & \ddots & 1 \\ & & 1 & 0 \end{bmatrix} \frac{1}{\Delta x^2}   \\
    J &= \begin{bmatrix} 0 & 1/2 &  & \\ 1/2 & \ddots & \ddots & \\ & \ddots & \ddots & 1/2 \\ & & 1/2 & 0 \end{bmatrix}
    \end{split}
    \label{eq_10}
    \end{equation}
    矩阵$J$之本征矢与本征值分别为:
    \begin{equation}
    \begin{split}
    V_{ij} &= \sin\frac{ij\pi}{n+1}   \\
    \lambda_j &= \cos\frac{j\pi}{n+1}
    \end{split}
    \label{eq_11}
    \end{equation}
    其中, $i$、$j$取$[1, n]$之间的整数. 显然, 矩阵$J$之本征值$\lambda \in (-1, 1)$, Jacobi迭代在该线性系统上收敛.
    观察本征矢取值, 当$j$逐渐增大时, 频率逐渐升高. 由此可将误差大致分为低频组分与高频组分. 低频组分, 误差分布平滑, 利于近似表达; 高频组分, 误差分布不平滑, 不利于近似表达.
    观察本征值取值, 当$j$逐渐增大时, 其变化趋势为: $1 \rightarrow 0 \rightarrow -1$. 低频部分, 误差收敛速度慢; 中频部分, 误差收敛速度快; 高频部分, 误差收敛速度慢.
    对于低频部分, 误差分布平滑, 可采用向下插值技术将离散点上的残差由精细网格过渡至粗糙网格(即降低$n + 1$的取值), 以提升误差组分的频率, 达到快速收敛之目的, 再由向上插值将离散点上的误差由粗糙网格过渡至精细网格, 获取一个可以接受的残差补偿; 对于中频部分, 误差收敛速度快, 无需特殊处理; 对于高频部分, 由于误差收敛速度慢且分布不平滑, 需要引入under-relaxation技术.
    Part Ⅲ: under-relaxation技术
    结合式(\ref{eq_6}), 采用如下凸组合改写Jacobi迭代:
    \begin{equation}
    x^{k+1} = (1 - \alpha)x^k + \alpha [-D^{-1}(L + U)x^k + D^{-1}b], \quad \text{where }\alpha \in (0, 1)
    \label{eq_12}
    \end{equation}
    根据式(\ref{eq_7})之定义, 误差方程转换如下:
    \begin{equation}
    e^{k+1} = [(1 - \alpha )I - \alpha D^{-1}(L + U)]e^k
    \label{eq_13}
    \end{equation}
    此时, 迭代矩阵及其本征值分别为:
    \begin{equation}
    \begin{cases}
    J_\alpha = (1 - \alpha)I - \alpha D^{-1}(L + U)   \\
    \lambda_\alpha = 1 - \alpha + \alpha\lambda
    \end{cases}
    \label{eq_14}
    \end{equation}
    根据式(\ref{eq_11}), 本征值具体取值为:
    \begin{equation}
    \lambda_j^\alpha = 1 - \alpha + \alpha \cdot \cos\frac{j\pi}{n+1} \in (1 - 2\alpha, 1)
    \label{eq_15}
    \end{equation}
    若$\alpha = 0.5$, 则本征值取值区间为$(0, 1)$. 此时已成功消除分布不平滑且收敛速度慢的误差组分, 使Jacobi迭代在多重网格上求解Poisson's equation时快速收敛.
  • 代码实现:
    Part Ⅰ:
    现以Poisson's equation为例进行算法实施, 原始图像、Laplace变换图像及边界条件图像分别如下方左、中、右三图所示:

    Part Ⅱ:
    Multigrid实现如下:

残差分析图代码Python_迭代

残差分析图代码Python_迭代_02

1 # 多重网格法求解Poisson's equation
  2 
  3 import copy
  4 import numpy
  5 from PIL import Image
  6 from matplotlib import pyplot as plt
  7 from matplotlib import animation
  8 
  9 
 10 
 11 def get_F(picName):
 12     '''
 13     根据图片数据获取Poisson's equation等式右侧值
 14     '''
 15     img = Image.open(picName)
 16     arr = numpy.array(img).astype(float)
 17     img.close()
 18     
 19     dx = 1 / (arr.shape[1] - 1)
 20     
 21     F = numpy.zeros(arr.shape)
 22     for i in range(1, F.shape[0]-1):
 23         for j in range(1, F.shape[1]-1):
 24             F[i, j] = ((arr[i-1, j] + arr[i+1, j] + arr[i, j-1] + arr[i, j+1]) - 4 * arr[i, j]) / dx ** 2
 25     return F
 26 
 27 
 28     
 29 def get_B(picName):
 30     '''
 31     根据图片数据获取Poisson's equation边界条件, 非边界值填充0
 32     '''
 33     img = Image.open(picName)
 34     B = numpy.array(img).astype(float)
 35     img.close()
 36     B[1:-1, 1:-1] = 0
 37     return B
 38 
 39 
 40 
 41 def jacobi(U, F):
 42     '''
 43     Jacobi迭代法求解Poisson's equation: ΔU = F
 44     U: 迭代解初始值, 含边界条件
 45     F: Poisson's equation右侧值, 边界值无效
 46     横轴长度固定取1
 47     '''
 48     dx = 1 / (U.shape[1] - 1)
 49     UNew = copy.deepcopy(U)
 50     for i in range(1, UNew.shape[0]-1):
 51         for j in range(1, UNew.shape[1]-1):
 52             UNew[i, j] = (U[i-1, j] + U[i+1, j] + U[i, j-1] + U[i, j+1] - dx ** 2 * F[i, j]) / 4
 53     return UNew
 54 
 55 
 56     
 57 class Multigrid(object):
 58     '''
 59     多重网格法之实现
 60     '''
 61     def __init__(self, B, F, alpha=0.5):
 62         self.__B = B                # 边界条件, 矩阵非边界值无效
 63         self.__F = F                # Poisson's equation等式右侧
 64         self.__alpha = alpha        # under-relaxation凸组合因子
 65         
 66         self.__UList = list()       # 记录每帧迭代解
 67         
 68         
 69     def get_solu(self):
 70         return self.__UList
 71         
 72         
 73     def optimize(self, maxIter=3000, epsilon=1.e-1):
 74         '''
 75         maxIter: 最大迭代次数
 76         epsilon: 收敛判据, 迭代解趋于稳定则收敛
 77         '''
 78         self.__UList.clear()
 79         
 80         U0 = copy.deepcopy(self.__B)
 81         F0 = copy.deepcopy(self.__F)
 82         alpha = self.__alpha
 83         
 84         self.__UList.append(numpy.rint(U0).astype("int"))
 85         for idx in range(maxIter):
 86             R0 = self.__calc_residual(U0, F0)
 87             R0Norm = numpy.linalg.norm(R0)
 88             print("iterCnt: {:3d},   ResidualNorm: {}".format(idx, R0Norm))
 89             if R0Norm <= epsilon:
 90                 break
 91             
 92             U0 = self.__do_multigrid(U0, F0, alpha)
 93             self.__UList.append(numpy.rint(U0).astype("int"))
 94             
 95         return U0
 96         
 97     
 98     def __do_multigrid(self, U0, F0, alpha):
 99         # pre-smoothing
100         Uf = self.__smoothing(U0, F0, alpha, 10)
101         Rf = self.__calc_residual(Uf, F0)                     # 残差计算
102         # downward interpolation - find grid to coarse grid
103         Rc = self.__downward_interpolate(Rf)                  # 向下插值 - 残差计算
104         deltaU0 = numpy.zeros(Rc.shape)
105         if (Rc.shape[0] >= 5 and Rc.shape[1] >= 5):
106             deltaUc = self.__do_multigrid(deltaU0, Rc, alpha)
107         else:
108             deltaUc = self.__smoothing(deltaU0, Rf, alpha, 10)
109         # upward interpolation - coarse grid to fine grid
110         deltaUf = self.__upward_interpolate(deltaUc, Uf)      # 向上插值 - 残差补偿计算
111         Uf += deltaUf
112         # post-smoothing
113         U0 = self.__smoothing(Uf, F0, alpha, 10)
114         return U0
115             
116             
117     def __upward_interpolate(self, deltaUc, Uf):
118         deltaUf = numpy.zeros(Uf.shape)
119         deltaUf[2:-2:2, 2:-2:2] = deltaUc[1:-1, 1:-1]
120         deltaUf[1:-1:2, 2:-2:2] = (deltaUf[0:-2:2, 2:-2:2] + deltaUf[2::2, 2:-2:2]) / 2
121         deltaUf[:, 1:-1:2] = (deltaUf[:, 0:-2:2] + deltaUf[:, 2::2]) / 2
122         return deltaUf
123             
124             
125     def __downward_interpolate(self, Rf):
126         '''
127         向下插值 - R中边界值无效
128         '''
129         RcOri = Rf[2:-2:2, 2:-2:2]
130         RcTra = numpy.zeros((RcOri.shape[0]+2, RcOri.shape[1]+2))
131         RcTra[1:-1, 1:-1] = RcOri
132         return RcTra
133     
134     
135     def __calc_residual(self, U, F):
136         '''
137         残差计算
138         '''
139         dx = 1 / (U.shape[1] - 1)
140         R = numpy.zeros(U.shape)
141         
142         for i in range(1, U.shape[0]-1):
143             for j in range(1, U.shape[1]-1):
144                 R[i, j] = F[i, j] - (U[i-1, j] + U[i+1, j] + U[i, j-1] + U[i, j+1] - 4 * U[i, j]) / dx ** 2
145         return R
146             
147             
148     def __smoothing(self, U, F, alpha, iterNum=5):
149         for idx in range(iterNum):
150             U = (1 - alpha) * U + alpha * jacobi(U, F)       # under-relaxation
151         return U
152         
153 
154 
155 class MultigridPlot(object):
156 
157     fig = None
158     ax1 = None
159     line = None
160     txt = None
161     UList = None
162     
163     @classmethod
164     def plot_ani(cls, mgObj):
165         mgObj.optimize()
166         
167         cls.UList = mgObj.get_solu()
168         cls.fig = plt.figure(figsize=(5, 4))
169         cls.ax1 = plt.subplot()
170         cls.line = cls.ax1.imshow(cls.UList[0], cmap="gray")
171         cls.txt = cls.ax1.text(x=100, y=100, s="")
172         
173         ani = animation.FuncAnimation(cls.fig, cls.update, frames=numpy.arange(len(cls.UList)), blit=True, interval=1000, repeat=True)
174         
175         cls.ax1.xaxis.set_visible(False)
176         cls.ax1.yaxis.set_visible(False)
177         cls.fig.tight_layout()
178         ani.save("plot_ani.gif", writer="PillowWriter", fps=3, dpi=1000)
179         plt.close()
180     
181     
182     @classmethod
183     def update(cls, frame):
184         cls.line.set_data(cls.UList[frame])
185         cls.txt.set_text("iterCnt: {}".format(frame))
186         return cls.line, cls.txt
187         
188         
189     @staticmethod
190     def plot_fig(mgObj):
191         U = mgObj.optimize()
192         img = Image.fromarray(U).convert("L")
193         img.save("plot_fig.png")
194         img.close()
195         
196 
197 
198 if __name__ == "__main__":
199     picName = "./telescope-gray.jpg"
200     F = get_F(picName)
201     B = get_B(picName)
202     
203     img = Image.fromarray(F).convert("L")
204     img.save("F.png")
205     img.close()
206     
207     img2 = Image.fromarray(B).convert("L")
208     img2.save("B.png")
209     img2.close()
210     
211     mgObj = Multigrid(B, F)
212     MultigridPlot.plot_fig(mgObj)
213     # MultigridPlot.plot_ani(mgObj)
  • View Code
  • 结果展示:
  • 残差分析图代码Python_插值_03

  • 显然, 通过Multigrid方法, 根据中间Laplace变换图像及右侧边界条件图像即可实现对左侧原始图像的快速求解, 迭代速度较常规迭代方法(Jacobi迭代、Gauss-Seidel迭代等)提升明显.
  • 使用建议:
    ①. 适用于结构化网格, 计算过程在精细网格与粗糙网格间多次变换;
    ②. 适用于求解椭圆型方程, 尤其是线性椭圆型方程.
  • 参考文档: 
    Numerical Methods for PDE 2016 by Professor Qiqi Wang & Jacob White from MIT