【问题标题】:N body simulation in C++C++中的N体模拟
【发布时间】:2015-02-13 07:34:52
【问题描述】:

我正在尝试实现二维 n-body 模拟的 OpenMP 版本。

但是有一个问题:我假设每个粒子的初速度和加速度都为零。当粒子第一次聚集时,它们会高速分散,不再聚集。

这似乎不符合牛顿定律,对吧?

有人能解释一下为什么会发生这种情况以及我该如何解决这个错误吗?

这是我的代码的一部分:

/* update one frame */
void update() {
    int i, j;

    omp_set_num_threads(8);
    # pragma omp parallel private(j)
    {

    # pragma omp for schedule(static)
        for ( i = 0; i < g_N; ++i ) {
            g_pv[i].a_x = 0.0;
            g_pv[i].a_y = 0.0;
            for ( j = 0; j < g_N; ++j ) {
                if (i == j)
                    continue;
                double r_2 = pow((g_pv[i].pos_x - g_pv[j].pos_x),2) + pow((g_pv[i].pos_y - g_pv[j].pos_y),2);
                g_pv[i].a_x += (-1) * G * g_pv[j].m * (g_pv[i].pos_x - g_pv[j].pos_x) / (pow(r_2 + e,1.5));
                g_pv[i].a_y += (-1) * G * g_pv[j].m * (g_pv[i].pos_y - g_pv[j].pos_y) / (pow(r_2 + e,1.5));             
            } 
            g_pv[i].v_x += period * g_pv[i].a_x;
            g_pv[i].v_y += period * g_pv[i].a_y;
        }
    # pragma omp for schedule(static)
        for ( int i = 0; i < g_N; ++i ) {
            g_pv[i].pos_x += g_pv[i].v_x * period;
            g_pv[i].pos_y += g_pv[i].v_y * period;
        }
    }
}

不用担心 OpenMP,只需将其视为顺序版本即可。 OpenMP 不会对结果产生太大影响。

编辑:澄清一下,这里是整个代码(这部分可能有一些错误,但我描述的问题应该出现在上面的代码部分

# include <iostream>
# include <fstream>
# include <iomanip>
# include <cmath>
# include <vector>
# include <cstdlib>
# include <omp.h>

# include <GL/glew.h>
# include <GL/freeglut.h>
# include <GL/gl.h>

using namespace std;

/* the size of the opengl window */
# define WIDTH 2000
# define HEIGHT 2000

/* define the global constants */
const double G = 6.67 * pow(10, -11);
// const double G = 6.67;
const double e = 0.00001;
const double period = 1;

/* define the structure of particle */
struct particle
{
    double m;
    double pos_x;
    double pos_y;
    double v_x;
    double v_y;
    double a_x;
    double a_y;

    particle(double m = 0, double pos_x = 0, double pos_y = 0, 
            double v_x = 0, double v_y = 0, double a_x = 0, double a_y = 0)
    {
        this->m         = m;
        this->pos_x     = pos_x;
        this->pos_y     = pos_y;
        this->v_x       = v_x;
        this->v_y       = v_y;
        this->a_x       = a_x;
        this->a_y       = a_y;
    }
};

/* define the global data */
int g_N;                        // number of particles
vector<particle> g_pv;          // particle vector

void setUp();
void update();
void display();

int main(int argc, char ** argv) {

    /* set up the window */
    glutInit(&argc, argv);
    glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);
    glutInitWindowSize (WIDTH, HEIGHT);
    glutInitWindowPosition (100, 100);
    glutCreateWindow ("openmp");

    /* initialize */
    setUp();

    glutDisplayFunc(display);
    glutMainLoop();

    return 0;
}

/* read the input data */
void setUp() {
    glMatrixMode (GL_PROJECTION);
    glLoadIdentity ();
    /* Sets a 2d projection matrix
     * (0,0) is the lower left corner (WIDTH, HEIGHT) is the upper right */
    glOrtho (0, WIDTH, 0, HEIGHT, 0, 1);
    glDisable(GL_DEPTH_TEST);

    ifstream inFile;
    inFile.open("input_25.txt");

    inFile >> g_N;
    g_pv.resize(g_N);
    for ( int i = 0; i < g_N; ++i )
    {
        inFile >> g_pv[i].m >> g_pv[i].pos_x >> g_pv[i].pos_y
               >> g_pv[i].v_x >> g_pv[i].v_y >> g_pv[i].a_x >> g_pv[i].a_y; 
    }
    inFile.close();
}

/* display in openGL */
void display(void) {
    glClear(GL_COLOR_BUFFER_BIT);
    for(int i = 0; i < g_pv.size(); ++i) {
        /* Get the ith particle */
        particle p = g_pv[i];

        /* Draw the particle as a little square. */
        glBegin(GL_QUADS);
        glColor3f (1.0, 1.0, 1.0);
        glVertex2f(p.pos_x + 2, p.pos_y + 2);
        glVertex2f(p.pos_x - 2, p.pos_y + 2);
        glVertex2f(p.pos_x - 2, p.pos_y - 2);
        glVertex2f(p.pos_x + 2, p.pos_y - 2);
        glEnd();
    }   
    update();
    glutPostRedisplay();
    glFlush ();
}

/* update one frame */
void update() {
    int i, j;

    omp_set_num_threads(8);
    # pragma omp parallel private(j)
    {
        /* compute the force */
        # pragma omp for schedule(static)
            for ( i = 0; i < g_N; ++i ) {
                g_pv[i].a_x = 0.0;
                g_pv[i].a_y = 0.0;
                for ( j = 0; j < g_N; ++j ) {
                    if (i == j)
                        continue;
                    double r_2 = pow((g_pv[i].pos_x - g_pv[j].pos_x),2) + pow((g_pv[i].pos_y - g_pv[j].pos_y),2);
                    g_pv[i].a_x += (-1) * G * g_pv[j].m * (g_pv[i].pos_x - g_pv[j].pos_x) / (pow(r_2 + e,1.5));
                    g_pv[i].a_y += (-1) * G * g_pv[j].m * (g_pv[i].pos_y - g_pv[j].pos_y) / (pow(r_2 + e,1.5));             
                } 
                g_pv[i].v_x += period * g_pv[i].a_x;
                g_pv[i].v_y += period * g_pv[i].a_y;
            }

        /* compute the velocity */
        # pragma omp for schedule(static)
            for ( int i = 0; i < g_N; ++i ) {
                g_pv[i].pos_x += g_pv[i].v_x * period;
                g_pv[i].pos_y += g_pv[i].v_y * period;
            }
    }
}

【问题讨论】:

  • 您的j 循环似乎正在访问尚未由您的i 循环初始化的数组元素。也许在使用ij 进行双循环之前运行整个i 循环进行初始化?
  • 降低代码的复杂度,减少出错的可能性。
  • @user3558391:我怀疑这里的问题至少部分是由于您使用的集成方法不准确(一阶Euler method)。您是否考虑过改用Verlet integration?当粒子相互坍缩时,它会在势能中达到奇点,因此那里可能会出现较大的数值误差。
  • 我知道,这就是我的意思。在担心 OpenMP 之前,先让顺序版本正常工作。
  • 我制作了以下输入文件:2 100000000 500 500 0 0 0 0 1 530 500 0 0.005 0 0 它的行为与我期望的一样,其中一颗行星围绕另一颗行星运行。现在,如果0.0050 取代,那么较轻的行星将与较大的行星相撞。这就是说,力最终将变得几乎无限,导致爆炸(参见例如gromacs.org/Documentation/Terminology/Blowing_Up)。在短距离内添加排斥力(比如 Lennard-Jones 类型),事情可能会看起来更好。

标签: c++ openmp simulation physics numerical-methods


【解决方案1】:

我将我的评论扩展到一个答案(如Z boson 所建议),并就您如何解决该问题提出了一些建议。

这个问题确实属于Computational Science.SE,因为我不认为代码本身有什么问题,而是算法似乎有问题:随着粒子接近你可以获得G / pow(e,1.5) ~ G * 1e7的力.这是很大的。非常非常大(与您的时间步相比)。为什么?假设你有两颗行星,一颗大行星位于 (0, 0),一颗小行星位于 (0, 1),后者向前者加速非常大。下一步,这颗小行星将位于 (0, -100) 或其他位置,其上的力为零,这意味着它永远不会返回并且现在也具有相当大的速度。你的模拟有blown up

正如您所说,这与牛顿定律不符,因此这表明您的数值方案失败了。不用担心,有几种方法可以解决此问题。你已经预料到了,正如你添加的e。把它变大,比如10,你应该没问题。或者,设置一个非常小的时间步,period。如果粒子离得太近,您也可以不计算力,或者对行星碰撞时应该发生什么有一些启发式(可能是爆炸和消失,或抛出异常)。或者有一个排斥力说 r - 2 势或其他东西: g_pv[i].a_y += (-1) * G * g_pv[j].m * (g_pv[i].pos_y - g_pv[j].pos_y) * (1 / pow(r_2 + e,1.5) - 1 / pow(r_2 + e,2.5)); 这类似于现象学Lennard-Jones interaction 如何包含由泡利不相容原理产生的排斥。请注意,您可以增加排斥的锐度(如果您愿意,可以将2.5 更改为12.5),这意味着排斥对远处的影响较小(好),但需要更准确地解决它,导致更小的period(坏)。理想情况下,您将从不会导致冲突的初始配置开始,但这几乎是不可能预测的。最后,您可能希望使用上面列出的方法的组合。

使用 OpenMP 可能会稍微加快速度,但您确实应该使用用于远程部队的算法,例如 Barnes-Hut simulation。有关更多信息,请参阅他们发布的关于最新发展的2013 Summer School on Fast Methods for Long Range Interactions in Complex Particle Systemsbooklet(免费提供)。您也可能不想显示每个时间步:科学模拟通常每 5000 步左右保存一次。如果你想要好看的电影,你可以插入一些移动平均线来消除噪音(你的模拟没有温度或类似的东西,所以你可能不用平均就可以了)。此外,我不确定您的数据结构是否针对该作业进行了优化,或者您是否可能遇到缓存未命中。我不是真正的专家,所以也许其他人可能想对此进行权衡。最后,考虑不要使用pow,而是使用fast inverse square root 或类似方法。

【讨论】:

  • 我同意 OP 对所用方法的实现没有问题,但 OP 对该方法的期望是什么。但我认为 OP 的问题在物理标签的范围内(OpenMP 是一个主要的红鲱鱼)。
  • 你可以解释为什么“这不符合牛顿定律”(例如,违反了能量守恒以及为什么)。你对大质量和小质量的思想实验很好。你也可以给小质量一个微小的切向速度,这样它就可以避免奇点。您可以分析表明它永远不会获得逃逸速度,但使用 OP 的方法可以获得违反能量守恒的逃逸速度。
  • 看来我对逃逸速度的看法是错误的。它可能有一个hyperbolic trajectory(我已经很久没有想到这些事情了)。但无论如何,我认为你可以构建它,让它有一个轨道,并且在 OPs 方法之后它会在不应该的时候逃离轨道。
  • @Zboson 问题是,尤其是当有几个行星时,你不能对轨道的稳定性等做出太多假设。是的,对于 2 行星的情况,如果您确保有足够的切向速度以便行星不会靠得太近,那么放弃“软糖因子”是相对安全的。现在有 25 个行星(比如说),没有任何保证,你必须以某种方式解决这个问题(一种方法是使用 e)。
  • @Zboson - 开篇文章中的代码使用的是辛积分器。基本欧拉使用 x(t+Δt)=x(t)+v(t)Δt,v(t+Δt)=x(t)+a(x(t))Δt。 Symplectic Euler 使用 v(t+Δt)=x(t)+a(x(t))Δt,x(t+Δt)=x(t)+v(t+Δt)Δt。 OP 使用后者。问题不在于缺乏辛。这是该技术的低阶性质。
【解决方案2】:

第一个明显的问题是您使用的是简单的“泰勒积分方案”。关键是,由于您将无限小的 dt 逼近为有限的时间差 Δt,即您的 period,因此您正在将众所周知的运动方程扩展为Taylor expansion,有无限项......你正在截断。

一般:

xt+Δt = xt + xt Δt + 1/2 xt Δt2 + 1/6 x′ ″t Δt3 + O(Δt4)

xt在时间t的一阶导数,速度vxt 二阶导数,加速度a,...; O(Δt4) 是错误的顺序 - 在这个例子中,我们以 3rd 顺序截断扩展,我们有一个 local 4th 的错误。

在您的情况下,您使用的是(欧拉方法):

xt+Δt = xt + v t Δt + O(Δt2)

vt+Δt = vt + a t Δt + O(Δt2)

在这种情况下,由于您要停止对第一项的扩展,因此这是一阶近似值。您将得到 O(Δt2) 阶的局部误差,它对应于 O(Δt) 阶的全局误差。这就是截断错误的行为方式 - 请参阅reference 了解更多详细信息。

我非常了解改进欧拉方法的两种不同的积分方案:

而且我知道 Runge-Kutta 方法(从两篇引用的文章中找到参考资料)甚至是 更高阶,但我从未亲自使用过它们。

我这里说的顺序是近似值的截断顺序,与截断本身的误差顺序严格相关。

Leapfrog 方法是二阶的。

Verlet 方法是三阶的,局部误差为 O(Δt4)。这是一种三阶方法,即使您看不到任何三阶导数,因为它们在推导中被抵消,如 wikipedia reference 所示。

高阶积分方案可让您在不缩小时间步长 Δt 的情况下获得更精确的轨迹。

然而,这些积分方法所具有的最重要的属性是:

  • 可逆性
  • 它们是Symplectic,即它们确实节能

从参考资料中你会发现很多学习它的材料,但是一本好的 N-body 书会让你以更有条理的方式学习它。

最后说明一下这两种方案之间非常重要的区别:Verlet 方法不会自动给出速度,需要(直接)作为后续步骤进行评估。另一方面,Leapfrog 方法确实评估了位置和速度,但在不同的时间 - 使用这种方案,您不能同时评估这两个量。

一旦你有了一个好的集成方案,你需要担心你能容忍的最大误差是多少,此时需要进行一些误差分析,然后你需要实施多时间尺度的方法来获得准确的即使两个物体距离 O(Δt4) 或更大(考虑到引力吊索),也可以进行积分。

它们可以是全局的(即在任何地方减少period)或局部的(仅针对系统的某些分区,粒子太靠近)...但是此时您应该可以去并自行查找更多参考资料。

【讨论】:

  • 很高兴您提到 OP 的方法(欧拉方法)不节能。
  • 还值得一提的是,Runge-Kutta 不是辛的,因此通常不适合这种类型的模拟。例如,arxiv.org/abs/1412.5187 的第 18 页很好地说明了每种方法的执行方式(与该主题的大多数介绍性书籍一样)。
  • 有辛龙格库塔方法(例如,辛分区龙格库塔)。此外,辛性是否重要取决于感兴趣的时间跨度。如果时间跨度小于一百万年左右(并且如果系统是无碰撞的),那么高阶、高精度的技术可能比任何辛技术“更好”。如果系统不是无碰撞的,辛技术本身将无济于事。在碰撞系统的情况下需要大量的护理和喂养。将初始速度设置为零可确保系统不是无碰撞的。
  • 我给了@alarge 赏金。我认为你和他的回答都同样好(选民似乎也这么认为)。他先回答。如果我可以分配赏金,我会但我不能。 OP 的部分问题是“这似乎不符合牛顿定律,对吧?”。我希望看到一个数值示例,说明为什么他选择的方法违反了牛顿物理学(而不仅仅是说它确实如此),但至少您提到违反了能量守恒。
  • 即使没有赏金我也会回答的,实际上我不在乎:我喜欢这个问题。我认为@alarge 过于关注数字方面,然后介绍了“分子天体物理学”(我也是一名计算化学家……但我不会介绍 LJ 来处理行星碰撞),所以我不喜欢他的回答,但这是个人意见。另一方面,我的答案并不准确,直到我编辑它,更正了我忘记的细节。缺少:一些好的 天体物理学 N-body 参考是最好的 - 但在“微观”方面我无能为力。
猜你喜欢
  • 2015-04-13
  • 2020-04-10
  • 2018-08-08
  • 1970-01-01
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多