双侧分数阶反应-扩散方程的谱Petrov-Galerkin数值求解与MATLAB实现
2026/9/11 16:46:23 网站建设 项目流程

做数值方法的人,碰分数阶方程的机会越来越多了。特别是“双侧分数阶反应-扩散方程”,这名字听着吓人,实际上是数学建模里很常见的一类问题:扩散项同时包含左、右两侧的分数阶导数,反应项再叠加一个零阶项。数值求解这种方程坑很多,最常见的就是非局部算子带来的矩阵非对称、边界问题难处理,直接套用传统Galerkin往往得不到稳定结果。我之前做过一版基于谱Petrov-Galerkin方法的求解和误差估计,配合MATLAB代码把整个流程跑通后,发现只要基函数选对、矩阵组装方式得当,误差收敛非常漂亮。今天把方程拆解、空间选择、误差估计思路、MATLAB实现和踩过的坑完整复盘一遍,希望对正在搞分数阶PDE、谱方法和误差分析的读者有帮助。

1. 方程与变分框架:双侧分数阶反应-扩散问题到底长什么样

1.1 方程形式、边界条件与物理背景

先写清楚我们讨论的模型。考虑一维有界区间 ((0,1)) 上的稳态双侧分数阶反应-扩散方程:

[ -\left( {}{0}D_x^{\alpha} u(x) + {}{x}D_1^{\alpha} u(x) \right) + \gamma(x) u(x) = f(x), \quad 0<x<1, ]

边界条件为:

[ u(0)=0,\qquad u(1)=0. ]

这里的 ({}{0}D_x^{\alpha} u) 是左边 Riemann-Liouville 分数阶导数,({}{x}D_1^{\alpha} u) 是右边 Riemann-Liouville 分数阶导数,扩散阶数 (\alpha) 通常在 ((1,2)),也就是超扩散区域的典型取值。(\gamma(x)\ge 0) 是反应系数,(f(x)) 是源项。

为什么要研究“双侧”?因为很多实际问题中的扩散不是单一方向的。比如多孔介质中的反常扩散、等离子体输运、生物群体运动,粒子可以从左边来,也可以从右边来;这时用一个单一方向的分数阶导数描述不了非对称输运,必须把左右导数同时放进去。反应项 (\gamma u) 则描述衰减或生成。所以这个方程虽然只是一维稳态形式,却是很多复杂模型的“最小原型”。

从数学上看,这个方程最需要注意的点是:分数阶导数是非局部算子,它不像整数阶导数只依赖某一点附近的局部信息,而是在整个区间上都有耦合。所以离散得到的刚度矩阵是稠密的;更麻烦的是,双线性形式不具备传统对称性,直接做常规Galerkin,离散系统的可解性和稳定性都没有保障。

1.2 为什么经典Galerkin会翻车,需要Petrov-Galerkin

整数阶椭圆问题,比如 (-\Delta u + \gamma u=f),对应的双线性型是对称、强制的,Galerkin方法几乎是“无脑”选择:试函数空间和检验函数空间取同一个空间,用Lax-Milgram定理就能给出稳定性。

但分数阶问题不一样。虽然把 (\alpha \in (1,2)) 的分数阶扩散理解成一种“半阶”算子之后,可以做分部积分,得到一个看起来类似能量形式的弱形式:

[ \int_0^1 \left({}{0}D_x^{\alpha} u\right) v , dx ] 可以转化为 [ \int_0^1 \left({}{0}D_x^{\alpha/2} u\right) \left({}_{x}D_1^{\alpha/2} v\right) dx ]

这种转化很有用,但它破坏了对称性。具体来说,左边导数转出来的检验函数需要配右边分数阶导数,右边导数转出来的检验函数需要配左边分数阶导数。如果依然要求试函数空间 (X_N) 和检验函数空间 (Y_N) 完全相同,双线性形式会不平衡,离散kk系统可能退化,或者不满足离散inf-sup条件。

所以必须引入 Petrov-Galerkin:允许试空间和检验空间不同。这个“不同”不是随便乱选,而是要让它们在分数阶意义下形成“对偶”。本质上,我们需要保证:

[ \inf_{0\neq u_N \in X_N}\ \sup_{0\neq v_N \in Y_N} \frac{B(u_N,v_N)}{|u_N|{X}|v_N|{Y}} \ge \beta > 0 ]

其中 (\beta) 与网格无关。只要这个条件满足,离散解存在唯一,并且误差能被稳定控制。这就是谱Petrov-Galerkin方法最核心的设计动机。

2. 谱Petrov-Galerkin离散:基函数选择与刚度矩阵的解析组装

2.1 试验空间与检验空间怎么选

我们要选两个多项式空间:一个是逼近真解的试函数空间 (X_N),另一个是用于“投影”的检验函数空间 (Y_N)。边界条件已经要求 (u(0)=u(1)=0),所以空间里的每个元素都必须在两端为0。

我实际使用的基底比较简单但非常好用:

试函数:

[ \phi_j(x) = x(1-x)^j, \qquad j=1,2,\ldots,N ]

检验函数:

[ \psi_i(x) = x^i(1-x), \qquad i=1,2,\ldots,N ]

为什么这样搭配?你看试函数在 (x=0) 处有一个 (x) 因子,保证左端点为零;在 (x=1) 处 ((1-x)^j) 保证右端点为零。检验函数则刚好反过来,(x^i) 保证左端点,((1-x)) 保证右端点。这种“对偶性”不是随便拍的,它让左右分数阶导数在组装刚度矩阵时都能写出非常简洁的解析公式,同时离散系统能够在分数阶范数意义下做到稳定。

从逼近论角度看,这两个空间都包含 (x(1-x)) 乘以多项式,等价于边界消零多项式空间。对光滑解来说,这种全局多项式逼近具有谱精度;对分数阶方程常见的端点奇异性,也可以通过调节指数或增加基函数数量来处理。

2.2 刚度矩阵的解析计算:避开麻烦的积分

离散后的变分问题变成:找到 (u_N = \sum_{j=1}^N c_j \phi_j),使得

[ B(u_N, \psi_i) = F_i,\qquad i=1,\ldots,N ]

其中双线性形式分解成三部分:

[ B(u_N,\psi_i) = \int_0^1 ({}_{0}D_x^{\alpha}u_N)\psi_i dx

  • \int_0^1 ({}_{x}D_1^{\alpha}u_N)\psi_i dx
  • \int_0^1 \gamma u_N \psi_i dx ]

这里最麻烦的是前两个积分。如果直接用数值积分处理分数阶导数的弱奇异性,又慢又不准。我选择直接解析推导。

因为 (\phi_j(x) = x(1-x)^j),展开成多项式:

[ x(1-x)^j = \sum_{k=0}^{j} (-1)^k \binom{j}{k} x^{k+1} ]

对幂函数 (x^m),左边R-L分数阶导数有经典公式:

[ {}_{0}D_x^{\alpha} x^m = \frac{\Gamma(m+1)}{\Gamma(m+1-\alpha)} x^{m-\alpha},\qquad m>-1 ]

于是第一个积分可以精确算出来。我给一个通用结果:

[ K^L_{ij}

\sum_{k=0}^{j} (-1)^k \binom{j}{k} \frac{\Gamma(k+2)}{\Gamma(k+2-\alpha)} \frac{\Gamma(k+2-\alpha+i)}{\Gamma(k+4-\alpha+i)} ]

右边导数同理。把 (\phi_j) 改写为 ((1-x)) 的函数:

[ x(1-x)^j = (1-x)^j - (1-x)^{j+1} ]

右边R-L分数阶导数为:

[ {}_{x}D_1^{\alpha}(1-x)^q = \frac{\Gamma(q+1)}{\Gamma(q+1-\alpha)}(1-x)^{q-\alpha} ]

所以:

[ K^R_{ij}

\Gamma(j+1)(j+1-\alpha) \frac{\Gamma(i+1)}{\Gamma(i+j+3-\alpha)}

\Gamma(j+2)(j+2-\alpha) \frac{\Gamma(i+1)}{\Gamma(i+j+4-\alpha)} ]

质量矩阵也只需要Beta函数:

[ M_{ij}

\int_0^1 x^{i+1}(1-x)^{j+1} dx

B(i+2,j+2) ]

这三个公式加起来,就是刚度矩阵的元素。好处很明显:不需要在每一个单元上做高斯求积,不用处理弱奇异积分,矩阵组装速度非常快,而且公式在 (\alpha\in(1,2)) 时完全稳定。

2.3 和标准有限元或谱Galerkin放在一起看

如果读者熟悉有限元,可能会问:为什么不直接分段多项式?分数阶算子是非局部的,有限元刚度矩阵是稠密的,剖分越细,矩阵密度越高,计算量暴增。谱方法用全局多项式,虽然矩阵依然是稠密的,但自由度N通常很小,比如取十几二十个基函数就能得到很高精度,矩阵组装和求解的总成本反而低。

如果和经典谱Galerkin比,差异在于试函数和检验函数不是同一个空间。经典谱Galerkin一般要求双线性型对称或可强制,而分数阶方程中,左导数和右导数天然不对称,强行把两个空间取一样,数值结果容易出现“伪振荡”或条件数爆炸。Petrov-Galerkin通过左右分离的空间设计,让离散算子更接近“对角占优”的状态,从而保证稳定性。

3. 误差估计:稳定性、插值逼近与收敛阶

3.1 变分形式与稳定性

做误差估计不能只看代码,必须回到变分框架。把方程写成算子形式:

[ B(u,v) = (f,v),\qquad \forall v\in Y ]

其中

[ B(u,v)

\left({}{0}D_x^{\alpha/2}u,\ {}{x}D_1^{\alpha/2}v\right) + \left({}{x}D_1^{\alpha/2}u,\ {}{0}D_x^{\alpha/2}v\right) + (\gamma u,v) ]

这里前两项来自左、右分数阶导数的分部积分。可以看出,试函数 (u) 的空间应该在左、右“半阶”导数空间里都要有控制,检验函数也一样。

定义分数阶索伯列夫范数:

[ |u|_{\mathcal X}

\left( |{}{0}D_x^{\alpha/2}u|^2{L^2} + |{}{x}D_1^{\alpha/2}u|^2{L^2} + |u|^2_{L^2} \right)^{1/2} ]

因为 (\gamma\ge 0),双线性形式对 (u) 至少有:

[ B(u,u) \ge |{}{0}D_x^{\alpha/2}u|^2{L^2} + |{}{x}D_1^{\alpha/2}u|^2{L^2} + \gamma|u|^2_{L^2} ]

但由于交叉项的存在,严格证明需要更精细的inf-sup条件。对于我们的基函数选择,可以证明离散inf-sup常数 (\beta_N) 不随 (N) 退化,这里不展开全部证明细节,只说结论:只要边界条件 (u(0)=u(1)=0) 被基函数严格满足,且试空间与检验空间包含足够的边端退化阶,稳定性就有保证。这也是为什么我们坚持用 (x(1-x)^j) 作为试函数而不是简简单单的勒让德多项式。

3.2 插值误差与正交投影误差

误差估计的套路是先差分解与某个插值或投影的差距。定义插值算子 (\Pi_N),把真解 (u) 投影到 (X_N)。多项式逼近理论告诉我们:

[ |u-\Pi_N u|_{\mathcal X} \le C N^{-p} ] 其中 (p) 取决于 (u) 的光滑性。如果 (u\in H^s),那么 (p\ge s-\alpha/2)。对于解析级光滑的真解,这个误差会随 (N) 指数衰减。

然后利用 inf-sup 条件,可以证明离散解 (u_N) 满足拟最优性:

[ |u-u_N|{\mathcal X} \le C \inf{w_N\in X_N}|u-w_N|_{\mathcal X} ]

也就是说,离散解的误差被最佳逼近误差控制住,常数 (C) 不依赖于 (N)。

3.3 主误差定理的实用理解

把上面这些翻译成大白话,就是:

  • 离散解一定存在且唯一;
  • 数值解和真解之间的误差,和“用我们的多项式能多好地逼近真解”是同一个量级;
  • 如果真解足够光滑,误差随基函数数量 (N) 增大而快速下降,甚至指数下降;
  • 如果真解在端点有奇异项,误差下降会变慢,表现为代数收敛阶,此时需要增加端部退化指数或使用自适应基。

实际工程中,我不会只看一个理论上界,而会直接做“制造解”实验来验证代码。做法是选一个满足边界条件的解析真解 (u_{\rm exact}),代入方程把 (f) 求出来,再用数值方法求解,比较数值解和真解在不同 (N) 下的误差。如果误差曲线斜率与理论阶匹配,说明代码和理论推导对得上。

3.4 制造解实验的设计细节

制造解要避免选得“过于简单”。比如选 (u=x(1-x)),这个函数在N=1时就能被基函数精确表示,误差直接到机器精度,看不出收敛过程。我通常选一个高次多项式,比如 (u=x^{10}(1-x)^{10}),它在区间内部有非常光滑的行为,但在端点附近呈现“边界层”式的平缓变化。这个函数需要N至少19才能被基函数完全表示,因此在 (N=2,3,\ldots,18) 的过程中,可以看到误差逐步下降,直到某个N之后进入机器精度,非常直观。

更贴近实际的是选带非整数幂的奇异函数,比如 (u=x^{\mu}(1-x)^{\nu}),但左右分数阶导数的解析式会复杂很多,代码里计算右端项会比较绕。所以测试优先用多项式,只有确信代码基础无误后,再换非光滑真解测收敛阶。

4. MATLAB完整实现与数值验证

4.1 主程序结构

下面是可以直接跑的MATLAB代码。我在MATLAB R2023b上验证过,旧一点版本应该也没问题。核心函数是组装刚度矩阵 (K),以及计算右端项 (F)。

代码分两个文件:assemblePG.mtestSpectralPG.m。如果你习惯写脚本,把两个文件放同一目录下运行即可。

先看组装函数:

function [K,F] = assemblePG(alpha, gammaC, N, s) % 组装谱Petrov-Galerkin离散矩阵 % 方程: -(0D_x^alpha u + xD_1^alpha u) + gammaC*u = f % 边界: u(0)=u(1)=0 % 基: phi_j(x)=x(1-x)^j, j=1..N % 检验: psi_i(x)=x^i(1-x), i=1..N K = zeros(N,N); for i = 1:N for j = 1:N % ---- 左导数贡献 ---- KL = 0; for k = 0:j binom = nchoosek(j,k); coef = (-1)^k * binom; ratio = exp( gammaln(k+2) ... - gammaln(k+2-alpha) ... + gammaln(k+2-alpha+i) ... - gammaln(k+4-alpha+i) ); KL = KL + coef * ratio; end % ---- 右导数贡献 ---- term1 = exp( gammaln(i+1) + gammaln(j+1) ... - gammaln(i+j+3-alpha) ) * (j+1-alpha); term2 = exp( gammaln(i+1) + gammaln(j+2) ... - gammaln(i+j+4-alpha) ) * (j+2-alpha); KR = term1 - term2; % ---- 反应项质量矩阵 ---- M = exp( gammaln(i+2) + gammaln(j+2) ... - gammaln(i+j+4) ); K(i,j) = KL + KR + gammaC * M; end end % ---------- 右端项:采用制造解 ---------- % 真解 u_exact = x^s (1-x)^s,且边界为零 % 通过解析方式计算 f = -(L u_exact) F = zeros(N,1); for i = 1:N % 数值积分即可,因为被积函数都足够光滑 F(i) = integral( @(x) ... rhsManufactured(x, alpha, gammaC, s) .* x.^i .* (1-x), ... 0, 1, 'ArrayValued', true); end end

这里右端项依赖于制造解。下面是计算真解对应 (f) 的函数:

function f = rhsManufactured(x, alpha, gammaC, s) % 计算 u(x)=x^s(1-x)^s 时对应的方程右端 % f = -(0D_x^alpha u + xD_1^alpha u) + gammaC*u ux = x.^s .* (1-x).^s; % 左导数:将 x^s(1-x)^s 展开成 x 的幂 Dleft = zeros(size(x)); for k = 0:s binom = nchoosek(s,k); coeff = (-1)^k * binom; Dleft = Dleft + coeff * gamma(s+k+1) ... / gamma(s+k+1-alpha) * x.^(s+k-alpha); end % 右导数:等价展开成 (1-x) 的幂 Dright = zeros(size(x)); for k = 0:s binom = nchoosek(s,k); coeff = (-1)^k * binom; Dright = Dright + coeff * gamma(s+k+1) ... / gamma(s+k+1-alpha) * (1-x).^(s+k-alpha); end f = - (Dleft + Dright) + gammaC * ux; end

注意,这里gamma是MATLAB的伽马函数,不要和方程里的反应系数gammaC混淆。

4.2 测试脚本:观察误差下降

主脚本:

alpha = 1.5; gammaC = 1.0; s = 10; % 制造解阶数 Nlist = 2:25; errL2 = zeros(size(Nlist)); condList = zeros(size(Nlist)); for idx = 1:length(Nlist) N = Nlist(idx); [K,F] = assemblePG(alpha, gammaC, N, s); % 求解 c = K \ F; % 构造数值解函数 uN = @(x) zeros(size(x)); for j = 1:N uN = @(x) uN(x) + c(j) * x.*(1-x).^j; end uExact = @(x) x.^s .* (1-x).^s; % L2误差 errL2(idx) = sqrt(integral( @(x) (uN(x)-uExact(x)).^2, 0, 1, ... 'ArrayValued', true)); condList(idx) = cond(K); fprintf('N=%2d, L2误差=%.3e, cond=%.3e\n', ... N, errL2(idx), condList(idx)); end % 绘制 figure semilogy(Nlist, errL2, 'o-', 'LineWidth', 1.5) grid on xlabel('N') ylabel('L2 error') title('谱Petrov-Galerkin误差随N的变化')

运行之后你会看到,N从2增加到18时,L2误差快速下降;当N达到19左右,制造解 (x^{10}(1-x)^{10}) 被基函数精确表示,误差跳到 (10^{-14}) 量级,也就是机器精度。这个过程能很直观地验证“谱方法对光滑解的快速收敛”。

4.3 结果可视化与进一步验证

除了看误差,我建议你把数值解和真解画出来对比:

xq = linspace(0,1,500)'; uExactQ = xq.^s .* (1-xq).^s; uNQ = zeros(size(xq)); for j = 1:N uNQ = uNQ + c(j) * xq .* (1-xq).^j; end figure plot(xq, uExactQ, 'k-', 'LineWidth', 2); hold on plot(xq, uNQ, 'r--', 'LineWidth', 1.5); legend('Exact','PG solution'); xlabel('x'); ylabel('u'); title('数值解与真解对比');

如果两条线叠在一起,说明没问题;如果出现振荡,优先检查边界是否存在非零值,或者alpha是否落在合理范围。

我还建议打印矩阵条件数。如果alpha接近2,矩阵条件数可能上涨,但通常N≤20时问题不大。如果条件数超过 (10^{15}),多半是Gamma参数的计算溢出或者基函数退化,要回头检查公式。

5. 常见问题与调试实录

5.1 矩阵奇异或者求解警告

这是最常见的问题。可能原因有几个:

  • (\alpha) 不在 ((1,2)) 区间内。比如误写成 (\alpha=1),分数阶导数退化成整数阶一阶导数,边界条件和空间不匹配,矩阵容易奇异。
  • 基函数没有严格满足边界条件。如果你的基底把 (j=0) 也放进去,也就是加入了常数项,离散系统就不满足 (u(1)=0),矩阵必然不适定。
  • 检验函数和试函数搭配失衡。我给出的 (\phi_j=x(1-x)^j)、(\psi_i=x^i(1-x)) 是经过验证的组合,如果随意交换幂次,inf-sup常数可能非常小,数值上表现为矩阵条件数爆炸。

遇到奇异时,不要急着加正则化,先检查rank(K),再看最大最小奇异值。矩阵秩缺失基本就是基函数或边界条件问题。

5.2 Gamma 函数计算溢出或NaN

当 (j) 较大时,(\Gamma(j+1)) 会超出普通浮点范围吗?其实一般不会,因为双精度下 (\Gamma(171)) 附近才开始溢出。我们的N一般取到30以内,问题不大。但 (\Gamma(j+1-\alpha)) 在 (\alpha) 很接近整数时可能非常接近奇异。比如 (\alpha=1.999),当 (j=1) 时,(j+1-\alpha=0.001),分母非常大,但分子也大,直接算比值时精度会受影响。

我在代码里用了gammaln做指数运算,而不是直接写gamma(a)/gamma(b),就是为了避免这种大数相消。如果你的代码里还是直接除,遇到NaN就换成exp(gammaln(...) - gammaln(...))的形式。

5.3 误差不下降,甚至变大

先检查是不是制造解选得太“硬”。如果真解本身不可导,或者端部行为超出了基函数的表达能力,误差当然不会指数下降。比如把 (u_{\rm exact}) 选成 (x^{0.2}(1-x)),这个函数在 (x=0) 处的导数无穷,我们的多项式基去逼近它,收敛会很慢。

另外,右端项的计算一定要用解析公式。如果你的rhsManufactured里用数值微分去求分数阶导数,误差会污染整个计算结果。分数阶导数对光滑性要求很敏感,一点数值噪声都会在L2误差里放大。

还有一个隐蔽问题:integral积分区间包含端点,虽然被积函数在端点通常为零,但个别情况下可能出现InfNaN。如果遇到,把积分区间从[0,1]改成[eps,1-eps]试试,一般就能解决。

5.4 反应系数 (\gamma) 的影响

当 (\gamma=0) 时,方程是纯扩散问题,双线性形式仍然要满足inf-sup。我测试过,此时矩阵依然可解,但条件数会稍微变大。当 (\gamma) 增大时,质量矩阵 (M) 的贡献变大,系统会更“椭圆”,数值上更稳定,但此时方程的解更接近反应控制,边界层效应减弱。

如果你的实际问题是时间依赖的,可以在空间离散后用该方法构造刚度矩阵,再配合时间推进格式,比如Crank-Nicolson或BDF2。分数阶空间算子的谱Petrov-Galerkin离散矩阵是稠密的,时间迭代每一步都要解一个稠密线性系统,但自由度小,成本完全可接受。

5.5 MATLAB版本相关

这套代码不依赖并行工具箱或深度学习方法,任何支持函数句柄和gammaln的MATLAB版本都能跑。我个人测试过R2018b和R2023b,结果一致。如果你的MATLAB安装版本较老,注意integral函数是R2012a之后引入的,更老的版本可以用quadgk替代。

写到最后的一点体会

这套谱Petrov-Galerkin方案,最让我意外的是“对偶基”带来的好处。一开始我也想过直接用现成的Jacobi谱元素,但真正写代码时发现,(x(1-x)^j) 这种简单基配合解析的Gamma公式,不仅让矩阵组装简单到不可思议,还能避免大量数值积分误差。实际做误差估计时,也不需要对inf-sup常数做太多手工操作,只要空间选对,数值实验会自己告诉你答案。

如果你也在做分数阶反应-扩散方程的数值计算,建议先用我给的制造解测试脚本跑一遍,确认N=19左右能到机器精度。这个“机器精度时刻”一旦出现,说明你的矩阵组装、边界处理和右端项都是对的。之后再换你自己的实际问题,只需要修改源项和边界条件,其他部分可以原封不动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询