; g2 K/ S. S1 r2 ?. L+ E! O! k
一、内容梗概/ f* F9 Q b' F6 F# L
“Ocean Modelling for Beginners”第一章主要是一些编程语言和软件的介绍还有一些序言之类的内容,在此就不多做解释了。“The Decay Problem”是书中第二章最开始引入的一个问题,借此向读者介绍数值计算的基本知识。 - C) p7 ~& F) u' e% u$ @7 O/ _
“The Decay Problem” 的中文怎么称呼都好啦,衰退问题、腐败问题,总之就是假设某种物质的浓度是 C0C_0 ,这种物质的浓度随着时间变化会按照一定的速率逐渐减少,例如酒精挥发等等,这样一个过程可以由下面一个方程来表示: 1 G& |0 x# M) G1 s/ s
dCdt=−κ⋅C\frac{dC}{dt} = - \kappa \cdot C \\ ) O5 Y8 B) [' B. \" N- b
这是一个一阶常微分方程,其中变量 CC 表示某种物质的浓度, tt 代表时间, κ\kappa (希腊字母:kappa)是一个常量,影响衰减的速率。有了这样一个式子,我们就可以求解出任意时刻 tt 这种物质的浓度 C(t)C(t) 了。这个方程解起来也比较简单,对等式两边同时积分就能得到解,也就是未知函数 C(t)C(t) :
8 S" E3 {% d% L a, G4 Y C(t)=c⋅e−κ⋅tC(t) = c \cdot e^{-\kappa \cdot t} \\ ! H! A# X% \- K5 }2 _. J
然而,由上式可以看出这个解不是唯一的(注意e前面的c指的是任意常数不是浓度),要确定唯一的解,还要在一些自变量点上给出 C(t)C(t) 的值,也就是边界条件。这里我们可以设 t0t_0 时刻物质的浓度 C(t0)=C0C(t_{0}) = C_0 :
4 ?) b* ^* l+ X* }- X" M7 b {dCdt=−κ⋅C,t≥t0C(t0)=C0\left\{ \begin{array}{l} \frac{dC}{dt}= -\kappa \cdot C, t \geq t_0 \\ C(t_{0})=C_0 \end{array} \right. \\ 1 k6 i8 t {! n8 K* H
在合理假定下,我们就可以认为从 t0t_0 时刻对应的初始浓度 C0C_0 开始,上式中的常微分方程决定了未知函数 C(t)C(t) 在 t_0">t>t0t > t_0 时的变化情况,也就是说这个边界条件可以确定常微分方程的唯一解。相应地,C(t0)=C0C(t_{0}) = C_0叫做初始条件,像上式一样带初始条件的常微分方程问题被称为初值问题。 3 j3 k% z: W T; D$ E
那么讲了这么多,该如何利用计算机来求解这样的问题呢?这里就需要介绍另一个概念——有限差分(finite difference)了,形如 f(x+b)−f(x+a)f(x+b)-f(x+a) 的式子称为有限差分,将上式除以 b−ab-a ,我们就得到了差商,在数值计算中用有限差分来近似替代微分是一种非常重要的方法,有限差分通常分为三种前向差分(forward difference)、后向差分(backward difference)和中心差分(central difference)。前向差分的形式为 f(x+h)−f(x)f(x+h) - f(x) ,其中 hh 是步长,假设步长为 Δt\Delta t ,我们可以将上面的常微分方程改写成前向差分的形式:
' W2 A" ^( B; U1 R$ Q0 J dCdt=C(tn+Δt)−C(tn)Δt=−κ⋅C(tn)\frac{dC}{dt} = \frac{C(t_n + \Delta t) - C(t_n)}{\Delta t} = -\kappa \cdot C(t_n) \\
7 U% r/ c3 l) A 换一种表示方式,让 C(tn)C(t_n) 为 CnC_n 也就是 tnt_n 时刻的浓度为 CnC_n ,则 tn+1t_{n+1} 时刻的物质浓度为C(tn+Δt)C(t_n + \Delta t) 即 Cn+1C_{n+1} : f3 s( D7 f+ `6 W
Cn+1−CnΔt=−κCn\frac{C_{n+1} - C_n}{\Delta t} = -\kappa C_n \\
- Z/ K* d8 E3 p( [3 l' K 整理一下左右两边的项可以得到: 3 X0 A7 t5 i: _
Cn+1=Cn−Δt⋅κ⋅Cn=(1−Δt⋅κ)CnC_{n+1} = C_n - \Delta t \cdot \kappa \cdot C_n = (1-\Delta t \cdot \kappa) C_n \\
. K# ^9 ]1 X! w' f 这样的有限差分格式叫做显式格式(explicit scheme),利用上式,我们就可以从t0t_0开始“步进式”的计算出后续时刻的函数近似值,在计算机程序中利用一个循环就可以完成。细心的同学可能注意到在这种方法下 Δt\Delta t 和 κ\kappa 的值是不能随便取的,试想一下如果 1">Δt⋅κ>1\Delta t \cdot \kappa > 1 那么 t0t_0 时刻之后的浓度就会变成负数,现实中根本不可能存在这种情况。因此我们的时间步长 Δt\Delta t 和 κ\kappa 需要满足下面一个条件: : x* x* a) V* Q" q$ r4 C! E9 H, @/ F [
1 l/ m( J' U: j5 z c4 N9 l3 d 该条件被称为数值稳定性条件(condition of numerical stability)。换句话说,只有当上式被满足时,我们的预测值,也就是通过有限差分发计算出的t时刻的物质浓度,是稳定的。可以看出这种方法下, Δt\Delta t 的最大值要受 κ\kappa 的限制。
: L) _* y9 ^- v! z7 r x5 K 除了上述的显式格式以外,还有两种格式,分别是隐式格式(implicit scheme)和混合格式(hybrid scheme)。 ! }* X: T" Z* t* t# D4 O. w/ S
隐式格式4 K9 I( u8 L" y' t
这种格式是通过后向差分得来的,后向差分其实就是用 xx 和 x−hx - h 替代显式差分中的 x+hx + h 和 xx ,将之前的微分方程写成后向差分的形式:
- K4 L4 j& x8 b2 I% g6 L9 k Cn+1−CnΔt=−κCn+1\frac{C_{n+1} - C_n}{\Delta t} = -\kappa C_{n+1} \\ * x$ j. h H; g) \) s) q
和之前一样整理下等式左右两边,得到了这样一个结果:
/ J, N4 J; A" m. W0 a Cn+1=Cn(1+Δt⋅κ)C_{n+1} = \frac{C_n}{(1 + \Delta t \cdot \kappa)} \\
) U1 u' G$ C7 j* B& f 与显式格式相比,一个很大的好处是 Δt\Delta t 的取值不再受到 κ\kappa 的限制了。而且分母始终是大于1的( 0" >κ>0\kappa >0 ),因此浓度一定会随着时间逐渐减少。 / x3 V/ P9 h+ T2 w1 w5 N$ f
混合格式
u. A+ {: D) D2 o1 d1 d6 H 如字面意思,这种方法混合了上述两种格式,形式如下: % B# x8 [, }3 q( s: B& Z. o
Cn+1−CnΔt=−α⋅κ⋅Cn+1−(1−α)⋅κ⋅Cn\frac{C_{n+1} - C_n}{\Delta t} = - \alpha \cdot \kappa \cdot C_{n+1} - (1- \alpha) \cdot \kappa \cdot C_n \\
1 U0 o7 N; b4 h6 J! R9 P1 P7 m5 @0 g 整理一下可以得到: ( A0 [5 N3 u# ~, k+ t& |- f! r
Cn+1=1−(1−α)⋅κ⋅Δt1+α⋅κ⋅ΔtCnC_{n+1} = \frac{1-(1-\alpha) \cdot \kappa \cdot \Delta t}{ 1 + \alpha \cdot \kappa \cdot \Delta t} C_n \\ ' q, o( ~' q5 L" D0 U9 |0 j* V
其中 α\alpha (希腊字母:alpha)是一个权值,决定哪一种格式所占的比重大,当 alpha=1alpha =1 时这种方法完全变成了隐式格式,而当 α=0.5\alpha = 0.5 时,这种格式被称做半隐式格式(semi-implicit scheme)。 L; @' z3 d9 {: N
二、参考程序% i3 F. L' L1 W
上面就是“The Decay Problem”练习之前的大致内容和基本概念,接下来就进入编码阶段了。题目的要求是: κ=0.0001s−1\kappa = 0.0001 \quad s^{-1} , Δt=3600s\Delta t = 3600 s ,初始浓度 C0C_0 为100(%),模拟15个小时的浓度变化。 程序中我将三种方法都写了出来方便后面比较它们的异同,其中混合格式的 α\alpha 值取了0.5。同时程序中还包含了一个求 tt 时刻真实值的方法,利用的是文章一开始积分求出的 C(t)C(t) , c=C0c = C_0 。和猴子也能看懂的海洋数值模拟(零)里面说的一样,计算的部分用C++来写,数据处理和绘图利用python来完成,两部分代码如下: 5 z$ e, }4 ?1 y6 `& N
数值模拟程序9 Q. A* t- F. ^$ C" O6 m
#include <fstream>#include <cmath>#include <vector>#include <functional>
3 [" k- `+ H/ `; {* a7 P const double kappa = 0.0001f;# c* n5 H" b" n3 v+ j. q
const double alpha = 0.5f;
( I; Z$ P% |3 B* F const double dt = 3600; // 3600 seconds
1 E w* L# ~% ?$ U/ ? ; J' ~: T/ Y' a- X3 Y
// lambda expression+ j$ @- q$ f; X0 p* \" b" L
auto explicit_scheme = [](double conc)->double{ return (1 - dt * kappa) * conc; };
# @1 I; Z1 C S) r- N, e auto implicit_scheme = [](double conc)->double{ return conc / (1 + dt * kappa); };
3 {* M; a& f% H( z- I+ c* a auto hybrid_scheme = [](double conc)->double{ return (1 - (1 - alpha) * kappa * dt) * conc / (1 + alpha * kappa * dt); };
% ?& `. e' M2 \6 U& k; I8 N+ j% y0 g4 n7 ^' P% [
void Decay( double concentration,' g) M1 X+ L1 |6 @1 G3 A. r, B1 }
int steps,
- T. q; c& T. m+ V5 X/ _ const std::function<double(double)>& scheme,
$ }" L$ ^5 U5 ^. Y0 j const char* result_path )
- c$ g# \/ _& I/ _9 D: l) W$ { {
% V; D& q/ ]4 o9 V2 k std::vector<double> res{concentration};
6 d. i0 A+ [. V2 ~5 e for(int i = 0; i < steps; i++)/ T+ m+ ?) @# b0 G6 k
{
: H! ^0 H$ X# t5 h' j/ u2 ]* z: o; R double conc1 = scheme(res[i]);
0 j6 V6 t0 X* \- _8 w% S2 ^ res.push_back(conc1);* _ D! _2 K7 N! e: `/ y. i$ f
}% ^ b; b0 g9 j- W+ q4 L0 x9 q
! c- n+ S0 J# S* ^ std::ofstream file(result_path);
- R; F3 I+ I- ]5 i8 p1 F+ |$ s if(file.is_open())
[) y# b4 D; b4 I* q ?9 w' B {, ]: g3 T5 [8 c5 ~ u4 K% q {
for(auto val : res)
6 _$ t# s! u; F/ W& } file << val << "\n";
2 @ `* c* ]. b, k4 Y4 c/ y! A file.close();% D( R+ t$ K3 V' {
}
5 R2 t2 J9 r! H0 F' x) f }' i* F) `2 H5 ?" n8 [( n
7 h* z6 v x3 @ Z* j. y
void AnalyticalSolution(double concentration, int steps)
- i) z$ S8 D- v {
+ w0 j7 S4 u4 a) N std::vector<double> res{concentration};7 p1 P1 [/ X" m
double t = 0;
. N) [7 L( d0 t for(int i = 1; i <= steps; i++)
; Q: P% ^ o% \! r8 T! e {0 {7 v, l0 v) o7 ~8 H
t += dt;
$ @) x. t, M0 ]: k7 V double ct = std::exp(-kappa * t) * concentration;
3 L7 w/ e, f7 p. W; ] res.push_back(ct);
7 s' G' h, W, ^7 C; F }; H" T; z3 m& R1 c+ ~
* X7 }6 k( l( }2 F" Q: W# w! y std::ofstream file("res_analytical.txt");
% L; c# g3 {3 Z/ @$ c if(file.is_open())# j) T9 N4 i# _5 @, e' r
{3 \2 S S8 F( `" _* y/ q) a3 l% D P
for(auto val : res)/ r7 k8 [% ]( E
file << val << "\n";
2 ], Y6 \9 F* |2 p. T7 U file.close();, V- }) t$ J# K$ j; k, n
}
. g" M, y7 y& D- ]1 r7 T( S }$ Y# c- I# _; ?
6 W: [5 O3 P& t" k) l int main(): F8 Y4 l' V) k9 `
{
8 Y% R( n- i! L6 Y5 l+ p // the initial concentration is 100 % ?5 \: d3 n3 e) ^8 Z# i7 J
double concentration = 100;
# b# }; t; {. y // which means the total simulating time is steps * dt seconds
8 a/ M: z5 u" x. y! S3 d int steps = 15;
% }8 D1 N+ @* J. e Decay(concentration, steps, explicit_scheme, "res_explicit.txt");
" p7 \+ u+ g! ^2 K Decay(concentration, steps, implicit_scheme, "res_implicit.txt");" k7 c9 w; r* O* G* r) t3 T) }$ o/ K
Decay(concentration, steps, hybrid_scheme, "res_hybrid.txt");
: k3 o$ X6 H8 W: j, r: o+ p AnalyticalSolution(concentration, steps);" v) U2 D0 x5 i7 l' C0 a
return 0;
, f4 t$ n3 |" Y }; S- k$ w2 }* E4 F) w% }" J
: A% `; w' r* G5 V
绘图程序$ P' O+ [2 w( x' p9 c4 R
import numpy as np' H4 ]. p: r& S4 p
import matplotlib.pyplot as plt
. i9 `+ o# Q8 x3 }+ }9 Z: ^0 @% u
plt.figure(Decay Problem)
3 g4 n" _7 k0 m% f. d0 @3 [) A2 B( G
res_explicit = np.loadtxt(res_explicit.txt).T3 W9 B1 E3 _4 B6 S1 G8 l+ T9 c
res_implicit = np.loadtxt(res_implicit.txt).T8 Y1 W* R( F1 g( b2 G
res_hybrid = np.loadtxt(res_hybrid.txt).T
. h+ @1 S( Z; q% s1 Z2 ~ analytical = np.loadtxt(res_analytical.txt).T0 s) f/ @! y" o4 u. t- m/ g
7 T, i* J% e. L [' w# m( [4 v& m time = np.arange(0, 16, 1)* p8 t6 m7 l$ s) D
plt.plot(time, analytical, color=black, linestyle=dashed, label=analytical)
3 D% `: u/ j0 j! ]! f; x plt.plot(time, res_explicit, label=explicit)" G' Y1 s" i2 Q' q* }4 M0 {& a
plt.plot(time, res_implicit, label=implicit)
' K/ X7 v& B$ t4 d0 q plt.plot(time, res_hybrid, label=hybrid)
. `' F* D( F5 h# H1 q8 L U. a2 J% r1 |$ M& t" k3 Z
plt.legend()) T; g$ b6 _( k6 _) R. t m+ p
plt.title(Decay Problem)2 g' Z$ {. K& m/ Q5 O6 w- R
plt.xlabel(Time (hours))# J G6 `; Y0 _6 C
plt.ylabel(Concentration (100%))
6 C& I @# y9 \" u% v8 C plt.ylim(0, 100)9 s: U/ j0 |. H$ {7 E. o
plt.xlim(0, 15)
& \& _9 m0 t0 X5 ?- ]7 T1 {9 _ plt.xticks(time)
6 R9 x" v" e4 G- `7 J1 y plt.grid(True)
8 Z! _5 {2 R$ F+ Y2 Y; r" V
# v" R: `- T* C/ @ plt.show()3 u* H* I$ C( B1 |1 @. l
5 Y; A2 C* X' }8 f7 ~8 `1 X
三、运行结果
6 ?4 U4 O7 m) j% s! H 结果如下图1所示,程序模拟了初始浓度为100%的某物质从零时刻开始15个小时的浓度变化情况,分别采用了显式、隐式以及半隐式的差分方法,对比黑色虚线的真实值可以看出同一时刻显示差分法的计算出的浓度比真实值要少,相反,隐式差分法要多一些。令人惊讶的是半隐式差分法(hybrid),几乎和真实值的曲线吻合,从此图可以直观的看出三种不同的差分方法的区别。 + y9 d) o/ e `. Z: Q+ @" F8 e7 ?
图 1. Decay Problem程序模拟结果参考文献Jochen Kampf. Ocean Modelling for Beginners. Springer. 2009. 喻文健. 数值分析与算法(第2版). 清华大学出版社. 2015.Wikipedia. 有限差分. https://en.wikipedia.org/wiki/Finite_difference.之前的文章
$ b1 Z9 @1 g$ M* U4 u 猴子也能看懂的海洋数值模拟(零)
, f1 D. t( m! n- g4 s! q
; G9 Q0 W A( O( d% @& A- u( t; p6 z. k8 z* n& y
' J# G+ n$ E5 a
0 R! `* K- ^, ~ |