4

根据在这个答案中给我的建议,我在我的重力模拟器中实现了一个龙格-库塔积分器。

然而,在我模拟了太阳系一年之后,位置仍然偏离了 110 000 公里,这是不可接受的。

我的初始数据由 NASA 的 HORIZONS 系统提供。通过它,我获得了行星、冥王星、月球、火卫二和火卫一在特定时间点的位置和速度矢量。

这些矢量是 3D 的,但是,有些人告诉我,我可以忽略三维,因为行星在围绕太阳的盘子中排列自己,所以我做到了。我只是将 xy 坐标复制到我的文件中。

这是我改进的更新方法的代码:

"""
Measurement units:

[time] = s
[distance] = m
[mass] = kg
[velocity] = ms^-1
[acceleration] = ms^-2
"""

class Uni:
    def Fg(self, b1, b2):
        """Returns the gravitational force acting between two bodies as a Vector2."""

        a = abs(b1.position.x - b2.position.x) #Distance on the x axis
        b = abs(b1.position.y - b2.position.y) #Distance on the y axis

        r = math.sqrt(a*a + b*b)

        fg = (self.G * b1.m * b2.m) / pow(r, 2)

        return Vector2(a/r * fg, b/r * fg)

    #After this is ran, all bodies have the correct accelerations:
    def updateAccel(self):
        #For every combination of two bodies (b1 and b2) out of all bodies:
        for b1, b2 in combinations(self.bodies.values(), 2):
            fg = self.Fg(b1, b2) #Calculate the gravitational force between them

            #Add this force to the current force vector of the body:
            if b1.position.x > b2.position.x:
                b1.force.x -= fg.x
                b2.force.x += fg.x
            else:
                b1.force.x += fg.x
                b2.force.x -= fg.x


            if b1.position.y > b2.position.y:
                b1.force.y -= fg.y
                b2.force.y += fg.y
            else:
                b1.force.y += fg.y
                b2.force.y -= fg.y

        #For body (b) in all bodies (self.bodies.itervalues()):
        for b in self.bodies.itervalues():
            b.acceleration.x = b.force.x/b.m
            b.acceleration.y = b.force.y/b.m
            b.force.null() #Reset the force as it's not needed anymore.

    def RK4(self, dt, stage):
        #For body (b) in all bodies (self.bodies.itervalues()):
        for b in self.bodies.itervalues():
            rd = b.rk4data #rk4data is an object where the integrator stores its intermediate data

            if stage == 1:
                rd.px[0] = b.position.x
                rd.py[0] = b.position.y
                rd.vx[0] = b.velocity.x
                rd.vy[0] = b.velocity.y
                rd.ax[0] = b.acceleration.x
                rd.ay[0] = b.acceleration.y

            if stage == 2:
                rd.px[1] = rd.px[0] + 0.5*rd.vx[0]*dt
                rd.py[1] = rd.py[0] + 0.5*rd.vy[0]*dt
                rd.vx[1] = rd.vx[0] + 0.5*rd.ax[0]*dt
                rd.vy[1] = rd.vy[0] + 0.5*rd.ay[0]*dt
                rd.ax[1] = b.acceleration.x
                rd.ay[1] = b.acceleration.y

            if stage == 3:
                rd.px[2] = rd.px[0] + 0.5*rd.vx[1]*dt
                rd.py[2] = rd.py[0] + 0.5*rd.vy[1]*dt
                rd.vx[2] = rd.vx[0] + 0.5*rd.ax[1]*dt
                rd.vy[2] = rd.vy[0] + 0.5*rd.ay[1]*dt
                rd.ax[2] = b.acceleration.x
                rd.ay[2] = b.acceleration.y

            if stage == 4:
                rd.px[3] = rd.px[0] + rd.vx[2]*dt
                rd.py[3] = rd.py[0] + rd.vy[2]*dt
                rd.vx[3] = rd.vx[0] + rd.ax[2]*dt
                rd.vy[3] = rd.vy[0] + rd.ay[2]*dt
                rd.ax[3] = b.acceleration.x
                rd.ay[3] = b.acceleration.y

            b.position.x = rd.px[stage-1]
            b.position.y = rd.py[stage-1]

    def update (self, dt):
        """Pushes the uni 'dt' seconds forward in time."""

        #Repeat four times:
        for i in range(1, 5, 1):
            self.updateAccel() #Calculate the current acceleration of all bodies
            self.RK4(dt, i) #ith Runge-Kutta step

        #Set the results of the Runge-Kutta algorithm to the bodies:
        for b in self.bodies.itervalues():
            rd = b.rk4data
            b.position.x = b.rk4data.px[0] + (dt/6.0)*(rd.vx[0] + 2*rd.vx[1] + 2*rd.vx[2] + rd.vx[3]) #original_x + delta_x
            b.position.y = b.rk4data.py[0] + (dt/6.0)*(rd.vy[0] + 2*rd.vy[1] + 2*rd.vy[2] + rd.vy[3])

            b.velocity.x = b.rk4data.vx[0] + (dt/6.0)*(rd.ax[0] + 2*rd.ax[1] + 2*rd.ax[2] + rd.ax[3])
            b.velocity.y = b.rk4data.vy[0] + (dt/6.0)*(rd.ay[0] + 2*rd.ay[1] + 2*rd.ay[2] + rd.ay[3])

        self.time += dt #Internal time variable

算法如下:

  1. 更新系统中所有物体的加速度
  2. RK4(第一步)
  3. 转到 1
  4. RK4(二)
  5. 转到 1
  6. RK4(第三)
  7. 转到 1
  8. RK4(第四)

我的 RK4 实施是否搞砸了?还是我只是从损坏的数据开始(重要的机构太少而忽略了第三维)?

如何解决这个问题?


解释我的数据等...

我所有的坐标都是相对于太阳的(即太阳在 (0, 0))。

./my_simulator 1yr

Earth position: (-1.47589927462e+11, 18668756050.4)

HORIZONS (NASA):

Earth position: (-1.474760457316177E+11,  1.900200786726017E+10)

110 000 km通过从我的模拟器预测的坐标中减去 NASA 给出的地球 x 坐标得到了错误。

relative error = (my_x_coordinate - nasa_x_coordinate) / nasa_x_coordinate * 100
               = (-1.47589927462e+11 + 1.474760457316177E+11) / -1.474760457316177E+11 * 100
               = 0.077%

相对误差似乎微乎其微,但这仅仅是因为在我的模拟和美国宇航局的模拟中地球都离太阳很远。距离仍然很大,使我的模拟器毫无用处。

4

6 回答 6

4

这种模拟是出了名的不可靠。舍入误差累积并引入不稳定性。提高精度并没有多大帮助。问题是您(必须)使用有限步长,而自然使用零步长。

您可以通过减小步长来减少问题,因此需要更长的时间才能使错误变得明显。如果您不是实时执行此操作,则可以使用动态步长,如果两个或更多物体非常接近,则该步长会减小。

我对这类模拟所做的一件事是在每一步之后“重新归一化”以使总能量相同。整个系统的引力加上动能的总和应该是一个常数(能量守恒)。计算出每一步后的总能量,然后将所有物体的速度缩放一个常数,以保持总能量不变。这至少使输出看起来更合理。如果没有这种缩放,在每一步之后都会向系统添加或移除少量能量,并且轨道往往会爆炸到无穷大或螺旋进入太阳。

于 2013-03-03T07:14:10.583 回答
4

要对太阳系进行 RK4 集成,您需要非常好的精度,否则您的解决方案将很快发散。假设您已正确实现所有内容,您可能会看到 RK 用于此类模拟的缺点。

要验证是否是这种情况:尝试不同的积分算法。我发现使用Verlet 集成,太阳系模拟会变得不那么混乱。Verlet 也比 RK4 更容易实现。

Verlet(和衍生方法)在长期预测(如全轨道)方面通常优于 RK4 的原因是它们是辛的,也就是说,RK4 不保存动量。因此,即使在发散之后,Verlet 也会给出更好的行为(真实的模拟,但其中有错误),而 RK 一旦发散就会给出非物理行为。

另外:确保您尽可能好地使用浮点数。在太阳系中不要使用米为单位的距离,因为浮点数的精度在 0..1 区间内要好得多。使用 AU 或其他一些标准化比例比使用米要好得多。正如其他主题所建议的那样,确保您在时间上使用一个纪元,以避免在添加时间步长时累积错误。

于 2013-03-02T19:15:52.907 回答
4

110 000 km绝对误差是指什么相对误差?

通过将我预测的地球 x 坐标与 NASA 的地球 x 坐标相减,我得到了 110 000 公里的值。

我不确定您在这里计算的是什么,或者您所说的“NASA 的地球 x 坐标”是什么意思。那是从什么原点,在什么坐标系中,在什么时间的距离?(据我所知,地球绕太阳运行,所以它的 x 坐标相对于以太阳为中心的坐标系一直在变化。)

无论如何,您通过从“NASA 的地球 x 坐标”中减去您的计算值来计算出 110,000 公里的绝对误差。你似乎认为这是一个糟糕的答案。你的期望是什么?把它当场击中?在一米之内?一公里?你可以接受什么,为什么?

通过将误差差除以“NASA 的地球 x 坐标”,您会得到一个相对误差。把它想象成一个百分比。你得到什么价值?如果是 1% 或更少,恭喜你自己。那将是相当不错的。

您应该知道浮点数在计算机上并不精确。(你不能用二进制精确表示 0.1,就不能用十进制精确表示 1/3。)会有错误。您作为模拟器的工作是了解错误并尽可能减少它们。

你可能有一个步长问题。尝试将你的时间步长减少一半,看看你是否做得更好。如果这样做,则表示您的结果尚未收敛。再次减少一半,直到达到可接受的错误。

您的方程式可能条件不佳。如果这是真的,小的初始错误会随着时间的推移而放大。

我建议您对方程进行无量纲化并计算稳定性极限步长。您对“足够小”步长应该是多少的直觉可能会让您感到惊讶。

我还阅读了有关多体问题的更多信息。这是微妙的。

您也可以尝试使用数值积分库而不是积分方案。您将对方程式进行编程并将其提供给工业强度积分器。它可以让您深入了解是您的实现还是导致问题的物理原理。

就个人而言,我不喜欢你的实施。如果您考虑到数学向量,那将是一个更好的解决方案。相对位置的“如果”测试让我感到寒冷。矢量力学会使符号自然出现。

更新:

好的,你的相对误差很小。

当然,绝对错误确实很重要 - 取决于您的要求。如果你在一个星球上降落一辆车,你不想离开那么多。

因此,您需要停止对什么构成步长太小的假设,并做您必须做的事情以将错误驱动到可接受的水平。

您计算中的所有数量都是 64 位 IEEE 浮点数吗?如果没有,你永远不会到达那里。

64 位浮点数的精度约为 16 位。如果您需要更多,您将不得不使用像 Java 的 BigDecimal 这样的无限精度对象,或者 - 等待它 - 重新调整方程式以使用公里以外的长度单位。如果你用对你的问题有意义的东西来缩放你的所有距离(例如,地球的直径或地球轨道的长轴/短轴的长度),你可能会做得更好。

于 2013-03-02T16:58:24.110 回答
3

非常简单的更改将改善事情(正确使用浮点值)

  • 更改单位系统,以使用尽可能多的尾数位。使用仪表,你做错了...使用 AU,如上所述。更好的是,缩放事物以使太阳系适合 1x1x1 的盒子
  • 已经在另一篇文章中说过:您的时间,将其计算为 time = epoch_count * time_step,而不是通过添加!这样,您可以避免累积错误。
  • 在对多个值进行求和时,请使用高精度求和算法,例如 Kahan 求和。在 python 中,math.fsum 会为你做这件事。
于 2013-05-12T02:29:34.257 回答
1

力分解不应该是

th = atan(b, a);
return Vector2(cos(th) * fg, sin(th) * fg)

(参见http://www.physicsclassroom.com/class/vectors/Lesson-3/Resolution-of-Forceshttps://fiftyexamples.readthedocs.org/en/latest/gravity.html#solution

顺便说一句:你取平方根来计算距离,但你实际上需要平方距离......

为什么不简化

 r = math.sqrt(a*a + b*b)
 fg = (self.G * b1.m * b2.m) / pow(r, 2)

 r2 = a*a + b*b
 fg = (self.G * b1.m * b2.m) / r2

我不确定 python,但在某些情况下,您可以获得更精确的中间结果计算(英特尔 CPU 支持 80 位浮点数,但在分配给变量时,它们会被截断为 64 位): 如果存储在中间值中,浮点计算会发生变化“双”变量

于 2016-01-27T23:55:02.963 回答
0

目前尚不清楚您的行星坐标和速度在哪个参考系中。如果它在日心坐标系中(参考系与太阳相关),那么您有两个选择:(i)在非惯性参考系中执行计算(太阳不是静止的),(ii)转换位置和速度进入惯性重心参考系。如果您的坐标和速度在重心参考系中,那么您也必须有太阳的坐标和速度。

于 2018-04-04T23:01:55.957 回答