在零重力的二维空间中优化粒子的引力计算

5
我已经用Python创建了一个小型的粒子可视化程序。我在一个零重力的二维空间中计算粒子的运动,每个粒子之间相互吸引力大小取决于粒子的质量和距离。
我使用Pygame进行可视化,并且一切都按计划进行(包括计算),但是我需要极限优化计算速度。目前系统可以以良好的帧率计算大约100-150个粒子。我把所有计算放在一个单独的线程中,这样给了我更多的提升,但远远达不到我的要求。
我看了看Scipy和Numpy,但由于我不是科学家或数学专家,所以感到很困惑。看起来我走在正确的道路上,但我不知道该怎么做。
我需要通过嵌套循环计算我拥有的所有粒子之间的吸引力,而且由于我需要找出是否存在碰撞,我必须再次执行相同的操作。
写那种代码让我心碎......
Numpy能够计算数组与数组,但我还没有找到任何方法可以计算一个数组中所有项与另一个数组中的所有项。有吗?
如果有的话,我可以创建并组合几个数组并进行更快的计算,必须有一种函数可以获取两个数组中值相匹配的索引(即碰撞检测)。
以下是今天的吸引/碰撞计算:
class Particle:
    def __init__(self):
        self.x = random.randint(10,790)
        self.y = random.randint(10,590)
        self.speedx = 0.0
        self.speedy = 0.0
        self.mass = 4

#Attraction    
for p in Particles:
    for p2 in Particles:
        if p != p2:
            xdiff = P.x - P2.x
            ydiff = P.y - P2.y
            dist = math.sqrt((xdiff**2)+(ydiff**2))
            force = 0.125*(p.mass*p2.mass)/(dist**2)
            acceleration = force / p.mass
            xc = xdiff/dist
            yc = ydiff/dist
            P.speedx -= acceleration * xc
            P.speedy -= acceleration * yc
for p in Particles:
    p.x += p.speedx
    p.y += p.speedy

#Collision
for P in Particles:
   for P2 in Particles:
        if p != P2:
            Distance = math.sqrt(  ((p.x-P2.x)**2)  +  ((p.y-P2.y)**2)  )
            if Distance < (p.radius+P2.radius):
                p.speedx = ((p.mass*p.speedx)+(P2.mass*P2.speedx))/(p.mass+P2.mass)
                p.speedy = ((p.mass*p.speedy)+(P2.mass*P2.speedy))/(p.mass+P2.mass)
                p.x = ((p.mass*p.x)+(P2.mass*P2.x))/(p.mass+P2.mass)
                p.y = ((p.mass*p.y)+(P2.mass*P2.y))/(p.mass+P2.mass)
                p.mass += P2.mass
                p.radius = math.sqrt(p.mass)
                Particles.remove(P2)

3
本文回顾了优化引力模拟的常见方法,包括Barnes-Hut算法。专家们通常使用3D来进行模拟,但我认为2D情况都是类似的。http://www.cs.hut.fi/~ctl/NBody.pdf - Russell Borogove
如果你对数学不感兴趣(“我不是科学家或数学大师,我只是感到困惑”),那么我认为你需要寻找一个能够解决这个问题的库。请参考https://dev59.com/RljUa4cB1Zd3GeqPQVa7和https://dev59.com/xHE95IYBdhLWcg3wd9tK。 - andrew cooke
@nagisa,谢谢你提供的链接,我会更深入地研究Psyco。也许最终我会写一个C/C++模块,但首先我想要理解事情是如何工作的。 - ztripez
@Russell Borogove 谢谢,现在我有一段时间的睡前阅读了。 - ztripez
@Ztripez 你可能会发现以下资源很有用:http://www.artcompsci.org。它详细介绍了解决这些N体问题的许多不同策略,并提供了大量Ruby代码。Ruby代码应该很容易转换到Python;比将其转换为C++要容易得多。 - Mike Bailey
显示剩余2条评论
5个回答

5

您可以先尝试使用< strong >复数:在这种形式主义下,相关的引力和动力学公式非常简单,并且也可以相当快(因为NumPy可以在内部进行计算,而不是您分别处理x和y坐标)。例如,两个粒子之间在z和z'处的力量就很简单:

(z-z')/abs(z-z')**3

NumPy可以快速计算所有z/z'对的此类量。例如,所有z-z'值的矩阵可以从坐标的1D数组Z中简单地得到,即Z-Z[:,numpy.newaxis](计算1/abs(z-z')**3时,对角线项[z=z']需要一些特殊注意:它们应该被设置为零)。
至于时间演化,您可以使用SciPy的快速微分方程例程,它们比逐步欧拉积分更准确。
无论如何,深入研究NumPy都将非常有用,尤其是如果您计划进行科学计算,因为NumPy非常快速。

虽然这个方程看起来很好,但它是否适用于三维情况呢?因为这似乎只适用于Ztripez上面发布的二维版本。 - Mike Bailey
将三维空间推广的方法就是牛顿反比例定律的向量形式:对于由3D向量M和M'定义的两个点,M对M'的作用力为(M-M')/|M-M'|**3... 复数的好处在于它们非常类似于2D向量。 - Eric O. Lebigot

4
我曾经在这方面工作过,过去加速碰撞计算的方法之一是实际上存储附近粒子的列表。
基本上,想法是在重力计算中执行以下操作:
for (int i = 0; i < n; i++)
{
    for (int j = i + 1; j < n; j++)
    {
        DoGravity(Particle[i], Particle[j]);
        if (IsClose(Particle[i], Particle[j]))
        {
            Particle[i].AddNeighbor(Particle[j]);
            Particle[j].AddNeighbor(Particle[i]);
        }
    }
}

然后,您只需要遍历所有粒子,并逐个进行碰撞检测。在最好情况下,这通常是像 O(n) 这样的一些东西,但在最坏情况下,它很容易降级为 O(n^2)
另一种选择是尝试将粒子放入一个 Octree 中。建立一个类似于 O(n) 的结构,然后可以查询它以查看是否有任何接近的粒子。在那时,您只需对该对进行碰撞检测。我认为这样做是 O(n log n)
不仅如此,而且您还可以使用 Octree 来加速重力计算。与 O(n^2) 的行为不同,它也降至 O(n log n)。大多数 Octree 实现都包括一个“打开参数”,用于控制您将要进行的速度与准确性权衡。因此,Octree 倾向于比直接成对计算不够准确,并且编程复杂,但它们也使大规模模拟成为可能。
如果您以这种方式使用 Octree,则会执行所谓的 Barnes-Hut Simulation注意:由于您正在使用 2D,因此与Octree相对应的 2D 类比称为 Quadtree。有关更多信息,请参见以下维基百科文章:http://en.wikipedia.org/wiki/Quadtree

Barnes-Hut模拟看起来非常有趣。 - ztripez

4

为了进行快速计算,您需要将x、y、speedx、speedy和m存储在numpy数组中。例如:

import numpy as np

p = np.array([
    (0,0),
    (1,0),
    (0,1),
    (1,1),
    (2,2),
], dtype = np.float)

p是一个5x2的数组,存储粒子的x和y位置。要计算每一对粒子之间的距离,可以使用以下方法:

print np.sqrt(np.sum((p[:, np.newaxis] - p[np.newaxis, :])**2, axis=-1))

输出如下:
[[ 0.          1.          1.          1.41421356  2.82842712]
 [ 1.          0.          1.41421356  1.          2.23606798]
 [ 1.          1.41421356  0.          1.          2.23606798]
 [ 1.41421356  1.          1.          0.          1.41421356]
 [ 2.82842712  2.23606798  2.23606798  1.41421356  0.        ]]

或者你可以使用来自SciPy的cdist函数:
from scipy.spatial.distance import cdist
print cdist(p, p)

啊,这看起来像是我要求的东西。然而我完全不知道如何继续。 - ztripez

2

不确定这是否对你有所帮助,但这是我为解决同样问题而工作的方案的一部分。我没有注意到通过这种方式进行操作会有很大的性能提升,当粒子数量达到200左右时仍然开始变慢,但也许它可以给你一些启示。

C++模块用于计算2D平面上引力的x和y分量:

#include <Python.h>
#include <math.h>

double _acceleration(double &Vxa, double &Vya, double &Vxb, double &Vyb, double xa, double ya, double xb, double yb, double massa, double massb)
{
   double xdiff = xa - xb;
   double ydiff = ya - yb;
   double distance = sqrt(xdiff*xdiff + ydiff*ydiff) * pow(10, 5);

   if (distance <= 0)
      distance = 1;

   double force = (6.674 * pow(10, -11))*(massa*massb)/(distance*distance);

   double acca = force / massa;
   double accb = force / massb;
   double xcomponent = xdiff/distance;
   double ycomponent = ydiff/distance;

   Vxa -= acca * xcomponent;
   Vya -= acca * ycomponent;
   Vxb += accb * xcomponent;
   Vyb += accb * ycomponent;

   return distance;
}

static PyObject* gforces(PyObject* self, PyObject* args)
{
   double Vxa, Vya, Vxb, Vyb, xa, ya, xb, yb, massa, massb, distance;

   if (!PyArg_ParseTuple(args, "dddddddddd", &Vxa, &Vya, &Vxb, &Vyb, &xa, &ya, &xb, &yb, &massa, &massb))
      return NULL;

   distance = _acceleration(Vxa, Vya, Vxb, Vyb, xa, ya, xb, yb, massa, massb);

   return Py_BuildValue("ddddd", Vxa, Vya, Vxb, Vyb, distance);
}

static PyMethodDef GForcesMethods[] = {
{"gforces", gforces, METH_VARARGS, "Calculate the x and y acceleration of two masses and the distance between the two."},
{NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initgforces(void)
{
(void) Py_InitModule("gforces", GForcesMethods);
}

如果您将其编译为pyd文件,则应该获得一个Python对象,您可以导入它。但是,您必须正确设置所有编译器和链接器选项。我正在使用dev-C ++,并将我的编译器选项设置为-shared -o gforces.pyd,链接器设置为-lpython27(确保您使用与已安装版本相同的版本),并将Python目录路径添加到包含和库选项卡中。

该对象接受参数(p1.speedx,p1.speedy,p2.speedx,p2.speedy,p1.x,p1.y,p2.x,p2.y,p1.mass,p2.mass),并返回新的p1.speedx,p1.speedy,p2.speedx,p2.speedy以及p1 p2之间的距离。

使用上述模块,我还尝试通过将返回的距离与粒子半径的总和进行比较来削减一些碰撞检测步骤,如下所示:

def updateForces(self):         #part of a handler class for the particles
    prevDone = []
    for i in self.planetGroup:  #planetGroup is a sprite group from pygame
        prevDone.append(i.ID)
        for j in self.planetGroup:
            if not (j.ID in prevDone):               #my particles have unique IDs
                distance = i.calcGForce( j )         #calcGForce just calls the above  
                if distance <= i.radius + j.radius:  #object and assigns the returned 
                    #collision handling goes here    #values for the x and y speed
                                                     #components to the particles

希望这有点帮助。欢迎提供任何进一步的建议或指出我的严重错误,我也是新手。


1

(这个也许应该放在注释中,但我没有足够的声望去做到这一点)

我不明白你如何进行时间步进。你有

P.speedx -= acceleration * xc
P.speedy -= acceleration * yc

但是要在时间t+delta_t获得新速度,您需要执行以下操作

P.speedx -= acceleration * xc * delta_t
P.speedy -= acceleration * yc * delta_t

然后像这样更新位置:

P.x = P.x + P.speedx * delta_t
P.y = P.y + P.speedy * delta_t

关于速度问题,也许将粒子信息存储在numpy数组中而不是类中会更好?但我认为你无法避免循环。
此外,你是否看过wikipedia,其中描述了一些加速计算的方法。
(由于Mike的评论进行了编辑)

xcyc变量是指从粒子i指向j的单位向量的组成部分。 - Mike Bailey
@Mike:我明白了。但是acceleration_x -= acceleration * xc。我猜Ztripez所做的是隐式地假设时间步长为1。 - Mauro
确实。"正确"的方法是明确包含一个dt参数,就像你建议的那样。由于他没有这个参数,就好像他在一个时间单位内向前迈进了一步。我强烈建议他包括一个步长参数,因为使用前向欧拉方法,1是一个非常大的时间步长。 - Mike Bailey

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