我记得关于单个微分方程的求法我已经写过笔记了,可是竟然忘记放在什么地方了 😦。关于微分方程组的求法,这里有一个可以用、但是有点抽象的项目tosys 可以尝试运行一下。这篇笔记主要是对一些实现细节上的补充和扩展。
算法原理
龙格-库塔(Runge-Kutta,以下简称RK)法可以求微分方程(组)的数值解,但其实求微分方程的数值解不止这一种方法,只不过RK 精度较高思路简单而更为常用。
对于函数y=y(x),其可以在x=xn 处展开为泰勒级数:
y(xn+1)=y(xn)+hy′(ϵ),xn<ϵ<xn+1
令y′=f(xn,yn),则可得:
y(xn+1)=y(xn)+hf(ϵ,y(ϵ)),xn<ϵ<xn+1
令k∗ 为f(ϵ,y(ϵ)) 在区间(xn,xn+1) 上的平均值,就能精确求得y(xn+1) 的值:
y(xn+1)=y(xn)+hk∗
欧拉法
在上式中,如果取k∗=k1=f(xn,yn),则可以得到显式欧拉(Euler)法,思路简单可惜只有一阶精度。
于是我们可以取k∗=2k1+k2=2f(xn,yn)+f(xn+1,yn+1),就得到了改进欧拉法,具有二阶精度。
取得点越多,则计算的精度越高……
龙格库塔公式
在区间[xn,xn+1] 内取n 个点,就被称为n 阶龙格-库塔公式,一般称为RKn。实际中常用到四阶,也就是RK4:
⎩⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎧k1k2k3k4y(xn+1)=====f(xn,yn)f(xn+2h,yn+2hk1)f(xn+2h,yn+2hk2)f(xn+h,yn+hk3)y(xn)+61(k1+2k2+2k3+k4)hxn+1=xn+h
RK4 的算法很直接,实现起来也很简单。需要注意的是计算K2,k3 的自变量是相同的。然后我们以Lorenz
混沌公式来看一下微分方程组中的RK4 逻辑是怎样的。已知Lorenz
公式如下:
⎩⎪⎨⎪⎧f1(x,y,z)f2(x,y,z)f3(x,y,z)===x˙y˙z˙===−σx+σy−xz+rx−yxy−bz
其中 x,y,z 均是关于t 的函数,则其对应的RK4 计算步骤应如下所示:
⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧k1l1p1k2l2p2k3l3p3k4l4p4============f1(xn,yn,zn)f2(xn,yn,zn)f3(xn,yn,zn)f1(xn+2hk1,yn+2hl1,zn+2hp1)f2(xn+2hk1,yn+2hl1,zn+2hp1)f3(xn+2hk1,yn+2hl1,zn+2hp1)f1(xn+2hk2,yn+2hl2,zn+2hp2)f2(xn+2hk2,yn+2hl2,zn+2hp2)f3(xn+2hk2,yn+2hl2,zn+2hp2)f1(xn+hk3,yn+hl3,zn+hp3)f2(xn+hk3,yn+hl3,zn+hp3)f3(xn+hk3,yn+hl3,zn+hp3)===−σxn+σyn−xnzn+rxn−ynxnyn−bzn
最后得到:
⎩⎪⎨⎪⎧xn+1yn+1zn+1===xn+6h(k1+2k2+2k3+k4)yn+6h(l1+2l2+2l3+l4)zn+6h(p1+2p2+2p3+p4)
可见,微分方程组中没有明显用到时间t,而是蕴含在在步长h 中。
效率vs通用
在tosys-system.py 的代码中,我采用了字典/哈希表的方式来存储状态变量,然后通过循环遍历来计算新的状态和赋值操作。这样在使用上用户就无需修改类代码、只需在system
对象中不断添加状态属性就好了。
# ...
class System():
def __init__(self, start=0, end=0, dt=0, input=lambda t: 1):
# ...
self.state = OrderedDict() # 有序字典,可以按添加顺序进行遍历,非必须
self.equation = OrderedDict()
# ...
# ...
def RK4(self):
# ...
for t in numpy.arange(start, end, dt):
reset_state(next_state)
for state in self.state:
reset_state(temp_state)
k1 = dt*self.equation[state](t, temp_state)
for key in temp_state:
temp_state[key] += 0.5*k1
k2 = dt*self.equation[state](t+0.5*k1, temp_state)
for key in temp_state:
temp_state[key] += 0.5*(k2-k1)
k3 = dt*self.equation[state](t, temp_state)
for key in temp_state:
temp_state[key] += (k3-k2)
k4 = dt*self.equation[state](t, temp_state)
next_state[state] = self.state[state] + (k1+2*k2+2*k3+k4)/6.
# ...
但是有一点:哈希表再快也快不过指针,也就是硬编码。于是还有一种思路,就是不使用哈希表,而是让用户手动继承system
类,并将状态量作为类的属性进行计算。也许这在解释型语言中算不上什么,但是在编译型语言中,编译器就可以将类的属性直接编码为指针了。但这样做需要用户直接接触龙格库塔算法,多少有些烦人。如果有比较好的语法糖或者宏命令的话,则可以优先选择此项。
参考资料
- 多元变量的龙格-库塔(Runge-Kutta)公式
- 龙格-库塔方法
- Runge-Kutta(龙格-库塔)方法 | 基本思想 + 二阶格式 + 四阶格式
📅 2022-08-15 Aachen