Runge-Kutta RK4不比Verlet更好吗?

7

我只是在测试游戏中轨道动力学的几种整合方案。这里我使用了具有恒定和自适应步长的RK4方法,链接如下: http://www.physics.buffalo.edu/phy410-505/2011/topic2/app1/index.html

我将其与简单的Verlet整合法进行了比较(以及欧拉法,但其性能非常差)。似乎RK4与恒定步长并不比Verlet更好。使用自适应步长的RK4更好,但提升并不是很大。我想知道是否出了什么问题?或者在哪种意义上说RK4比Verlet要好得多?

关键在于每个RK4步骤需要评估4次力,而每个Verlet步骤只需要评估1次力。因此,为了获得相同的性能,我可以将时间步长设为Verlet的4倍小。使用4倍较小的时间步长,Verlet比具有恒定步长的RK4更精确,与具有自适应步长的RK4几乎相当。

请查看以下图像: https://lh4.googleusercontent.com/-I4wWQYV6o4g/UW5pK93WPVI/AAAAAAAAA7I/PHSsp2nEjx0/s800/kepler.png

10T表示10个轨道周期,以下数字48968、7920、48966是所需的力评估次数。

以下是使用pylab的Python代码:

from pylab import * 
import math

G_m1_plus_m2 = 4 * math.pi**2

ForceEvals = 0

def getForce(x,y):
    global ForceEvals
    ForceEvals += 1
    r = math.sqrt( x**2 + y**2 )
    A = - G_m1_plus_m2 / r**3
    return x*A,y*A

def equations(trv):
    x  = trv[0]; y  = trv[1]; vx = trv[2]; vy = trv[3];
    ax,ay = getForce(x,y)
    flow = array([ vx, vy, ax, ay ])
    return flow

def SimpleStep( x, dt, flow ):
    x += dt*flow(x)

def verletStep1( x, dt, flow ):
    ax,ay = getForce(x[0],x[1])
    vx   = x[2] + dt*ax; vy   = x[3] + dt*ay; 
    x[0]+= vx*dt;        x[1]+= vy*dt;
    x[2] = vx;        x[3] = vy;

def RK4_step(x, dt, flow):    # replaces x(t) by x(t + dt)
    k1 = dt * flow(x);     
    x_temp = x + k1 / 2;   k2 = dt * flow(x_temp)
    x_temp = x + k2 / 2;   k3 = dt * flow(x_temp)
    x_temp = x + k3    ;   k4 = dt * flow(x_temp)
    x += (k1 + 2*k2 + 2*k3 + k4) / 6

def RK4_adaptive_step(x, dt, flow, accuracy=1e-6):  # from Numerical Recipes
    SAFETY = 0.9; PGROW = -0.2; PSHRINK = -0.25;  ERRCON = 1.89E-4; TINY = 1.0E-30
    scale = flow(x)
    scale = abs(x) + abs(scale * dt) + TINY
    while True:
        x_half = x.copy();  RK4_step(x_half, dt/2, flow); RK4_step(x_half, dt/2, flow)
        x_full = x.copy();  RK4_step(x_full, dt  , flow)
        Delta = (x_half - x_full)
        error = max( abs(Delta[:] / scale[:]) ) / accuracy
        if error <= 1:
            break;
        dt_temp = SAFETY * dt * error**PSHRINK
        if dt >= 0:
            dt = max(dt_temp, 0.1 * dt)
        else:
            dt = min(dt_temp, 0.1 * dt)
        if abs(dt) == 0.0:
            raise OverflowError("step size underflow")
    if error > ERRCON:
        dt *= SAFETY * error**PGROW
    else:
        dt *= 5
    x[:] = x_half[:] + Delta[:] / 15
    return dt    

def integrate( trv0, dt, F, t_max, method='RK4', accuracy=1e-6 ):
    global ForceEvals
    ForceEvals = 0
    trv = trv0.copy()
    step = 0
    t = 0
    print "integrating with method: ",method," ... "
    while True:
        if method=='RK4adaptive':
            dt = RK4_adaptive_step(trv, dt, equations, accuracy)
        elif method=='RK4':
            RK4_step(trv, dt, equations)
        elif method=='Euler':
            SimpleStep(trv, dt, equations)
        elif method=='Verlet':
            verletStep1(trv, dt, equations)
        step += 1
        t+=dt
        F[:,step] = trv[:]
        if t > t_max:
            break
    print " step = ", step


# ============ MAIN PROGRAM BODY =========================

r_aphelion   = 1
eccentricity = 0.95
a = r_aphelion / (1 + eccentricity)
T = a**1.5
vy0 = math.sqrt(G_m1_plus_m2 * (2 / r_aphelion - 1 / a))
print " Semimajor axis a = ", a, " AU"
print " Period T = ", T, " yr"
print " v_y(0) = ", vy0, " AU/yr"
dt       = 0.0003
accuracy = 0.0001

#                 x        y     vx  vy
trv0 = array([ r_aphelion, 0,    0, vy0 ])             

def testMethod( trv0, dt, fT, n, method, style ):
    print " "
    F = zeros((4,n));
    integrate(trv0, dt, F, T*fT, method, accuracy);
    print "Periods ",fT," ForceEvals ",  ForceEvals
    plot(F[0],F[1], style ,label=method+" "+str(fT)+"T "+str(  ForceEvals ) ); 

testMethod( trv0, dt, 10, 20000  , 'RK4', '-' )
testMethod( trv0, dt, 10, 10000  , 'RK4adaptive', 'o-' )
testMethod( trv0, dt/4, 10, 100000, 'Verlet', '-' )
#testMethod( trv0, dt/160, 2, 1000000, 'Euler', '-' )

legend();
axis("equal")
savefig("kepler.png")
show();
3个回答

15

我知道这个问题已经有一段时间了,但这与其中一种方法的“优越性”或您的编程无关-它们只是在不同的事情上表现出色。(所以,这个答案实际上不是关于代码的。甚至也不是关于编程的。它更多地涉及数学...)

Runge-Kutta求解器族在攻击几乎任何问题时精度都非常高,而且在自适应方法的情况下,性能也很好。然而,它们不是辛积分器,简单地说,就是它们在问题中不能保持能量不变。

另一方面,Verlet方法可能需要比RK方法小得多的步长才能最小化解的振荡,但该方法是辛的。

您的问题是能量守恒的;在任意数量的旋转之后,行星体的总能量(动能+势能)应该与初始条件相同。这对于Verlet积分器是正确的(至少作为时间窗口平均值),但使用RK族的积分器则不会,随着时间的推移,RK求解器将累积一个不减的误差,因为在数值积分中损失了能量。

要验证这一点,请尝试保存系统中的总能量,在每个时间步骤上绘制它(您可能需要做更多的旋转才能注意到差异)。RK方法将稳定地降低能量,而Verlet方法将在恒定能量周围产生振荡。

如果这正是您需要解决的确切问题,我还建议使用Kepler方程来解决该问题,该方程可以通过分析求解。即使对于带有许多行星、卫星等的相当复杂的系统,其行星间的相互作用也是如此微不足道,以至于您可以为每个天体和其自身旋转中心单独使用Kepler方程而不会损失太多精度。然而,如果您正在编写游戏,您可能真的对其他问题感兴趣,这只是一个示例学习-在这种情况下,请阅读各种求解器族的属性,并选择适合您问题的求解器。


5

我不确定我是否能回答你的具体问题,但以下是我的想法。

你定义了一个非常简单的力模型,在这种情况下,节省一些步骤可能并不能提高性能,因为在RK4中计算新步骤可能需要更长时间。如果力模型更复杂,带有自适应步长的RK4可能会为您节省大量时间。从您的图中我认为Verlet也偏离了正确的解,成为了一个重复的椭圆形。

对于轨道力学,您还可以尝试RK7(8)自适应积分器、Adams-Bashforth多步法或Gauss Jackson方法。这里有一篇论文展示了其中一些方法:http://drum.lib.umd.edu/bitstream/1903/2202/7/2004-berry-healy-jas.pdf

最后,如果您的力模型始终是一个简单的中心力,就像这个例子一样,请看一下开普勒方程。解决它是准确、快速的,并且您可以跳到任意时间点。


嗨,感谢您的回答和论文,我计划在那里放置一个更复杂的力模型(更多行星),但仍然RK4addaptive在这个精度下最多比简单的verlet快6倍。这篇论文看起来很有趣,但我不确定我想花太多时间自己实现它。我更喜欢寻找一些代码。我找到了一些,但我没有成功编译它们。 http://burtleburtle.net/bob/java/orbit/index.html http://www.unige.ch/~hairer/software.html https://code.google.com/p/exoflight/source/browse/trunk/Exoflight/src/com/fasterlight/exo/orbit/integ/RungeKuttaFehlberg45.java - Prokop Hapala

2

最终,我使用了自适应的Runge-Kutta-Fehlberg(RKF45)算法。有趣的是,当我要求更高的精度(最佳精度为1E-9)时,它比要求更低的精度(<1e-6)更快(需要的步数较少),因为在较低的精度下解决方案是不稳定的,并且由于被舍弃的步骤太长且不准确而浪费了很多迭代次数。当我要求更好的精度(1E-12)时,它需要更多的步骤,因为时间步长更短。

对于圆形轨道,可以将精度降低到1e-5并获得3倍的速度增益,但是当我需要模拟高离心率(椭圆形轨道)时,我更倾向于保持1E-9的安全精度。

以下是代码,如果有人想解决类似的问题:http://www.openprocessing.org/sketch/96977,它还显示了模拟一单位轨迹长度所需的力评估次数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接