Алгоритм верлета скорости - увеличение энергии для задачи n тел

Проблема

Я реализовал алгоритм верлета скорости для вычисления траекторий двух тел, взаимодействующих друг с другом гравитационно (только ньютоновская гравитация). Меньшее тело, вращающееся по орбите, имеет очень маленькую массу, тело, находящееся в центре орбиты, имеет большую массу.

Теоретически скорость Верле не должна изменять полную энергию системы (она будет колебаться, но со временем среднее значение останется близким к начальной энергии).

Однако на практике я наблюдал увеличение энергии с течением времени.

Результаты

Вот несколько результатов, которые иллюстрируют проблему. Все симуляции были выполнены с временным шагом dt = 0,001. Облетавшееся на орбиту тело имело массу 1000, а гравитационная постоянная Вселенной была установлена ​​на G = 1.0.

Во всех случаях начальное положение меньшего тела было {0, 0, 1}, а его начальная скорость была {0, 32, 0}. Начальная скорость большего тела была {0,0,0}.

Случай 1 (небольшая масса тела = 0,00001)

Вот траектория меньшего тела:

случай 1 траектория

А вот энергия более 100к шагов.

case 1 energy

Как мы видим, энергия сильно не меняется. Небольшие изменения вероятны из-за неточностей в расчетах.

Случай 1 (небольшая масса тела = 0,001)

Вот траектория орбитального тела:

случай 2 траектория

А вот полная энергия:

case 2 energy

Как мы видим, сейчас система набирает энергию.

Случай 3 (небольшая масса тела = 1)

Вот траектория орбитального тела:

случай 3 траектория

А вот полная энергия:

case 3 energy

Сейчас мы набираем много энергии.

код

Вот исходный код, который выполняет все вычисления:

Код для продвигающегося интегратора:

void Universe::simulation_step()
{
    for(std::size_t i=0; i<get_size(); i++)
    {
        // Verlet step 1: Compute v(t + dt/2) = v(t) + 0.5*dt*a(t)
        const Vector3D<Real> vel_half_step = {
            velocity(i, 0) + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 0),
            velocity(i, 1) + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 1),
            velocity(i, 2) + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 2)
        };

        // Verlet step 2: Compute x(t + dt) = x(t) + v(t + dt/2)*dt
        position(i, 0) += vel_half_step.x*sim_config.timestep;
        position(i, 1) += vel_half_step.y*sim_config.timestep;
        position(i, 2) += vel_half_step.z*sim_config.timestep;

        // Verlet step 3: update forces and update acceleration.
        const Vector3D<Real> forces = compute_net_grawitational_force(i);
        acceleration(i, 0) = forces.x/mass(i);
        acceleration(i, 1) = forces.y/mass(i);
        acceleration(i, 2) = forces.z/mass(i);

        // Verlet step 4: update velocity to the full timestep.
        velocity(i, 0) = vel_half_step.x + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 0);
        velocity(i, 1) = vel_half_step.y + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 1);
        velocity(i, 2) = vel_half_step.z + static_cast<Real>(0.5)*sim_config.timestep*acceleration(i, 2);
    }

    sim_time += sim_config.timestep;
}

Вот код для вычисления чистой гравитационной силы, действующей на тело:

Vector3D<Real> Universe::compute_net_grawitational_force(std::size_t i)
{
    Vector3D<Real> accumulated_force = {0,0,0};
    const Vector3D<Real> r2 = {
            position(i, 0),
            position(i, 1),
            position(i, 2)
    };

    const Real m1 = mass(i);

    for(std::size_t k=0; k<get_size(); k++)
    {
        if(k == i)
            continue;
        
        const Vector3D<Real> distace_vec = {
            r2.x - position(k, 0),
            r2.y - position(k, 1),
            r2.z - position(k, 2),
        };
        
        const Real distance = distace_vec.norm2();

        // Compute term that will be multipled by distance vector.
        const Real a = (-1*sim_config.G*m1*mass(k))/
        (distance*distance*distance);

        // Compute and add new force acting on the body.
        accumulated_force.x += distace_vec.x*a;
        accumulated_force.y += distace_vec.y*a;
        accumulated_force.z += distace_vec.z*a;
    }

    return accumulated_force;
}

Вот код, реализующий norm2 ():

template<typename T>
struct Vector3D
{
    T x;
    T y;
    T z;
    
    T norm2() const
    {
        return sqrt(x*x + y*y + z*z);
    }
};

Наконец, вот код, который вычисляет результаты, нанесенные ранее:

    std::vector<Real> x, y, z, energy;
    x.resize(NSTEPS);
    y.resize(NSTEPS);
    z.resize(NSTEPS);
    energy.resize(NSTEPS);

    for(std::size_t i=0; i<NSTEPS; i++)
    {
        universe.simulation_step();
        const Vector3D<Real> pos1 = 
        {
            universe.get_positions()(0, 0),
            universe.get_positions()(0, 1),
            universe.get_positions()(0, 2)
        };

        const Vector3D<Real> pos2 = 
        {
            universe.get_positions()(1, 0),
            universe.get_positions()(1, 1),
            universe.get_positions()(1, 2)
        };

        x[i] = pos2.x;
        y[i] = pos2.y;
        z[i] = pos2.z;

        // Compute total kinetic energy of the system.
        const Vector3D<Real> vel1 = 
        {
            universe.get_velocities()(0, 0),
            universe.get_velocities()(0, 1),
            universe.get_velocities()(0, 2),
        };
        const Vector3D<Real> vel2 = 
        {
            universe.get_velocities()(1, 0),
            universe.get_velocities()(1, 1),
            universe.get_velocities()(1, 2),
        };

        const Real mass1 = universe.get_masses()(0);
        const Real mass2 = universe.get_masses()(1);
        const Real spd1 = vel1.norm2();
        const Real spd2 = vel2.norm2();

        energy[i] = (spd1*spd1)*mass1*static_cast<float>(0.5);
        energy[i] += (spd2*spd2)*mass2*static_cast<float>(0.5);

        // Compute total potential energy
        const Vector3D<Real> distance_vec = 
        {
            pos1.x - pos2.x,
            pos1.y - pos2.y,
            pos1.z - pos2.z
        };

        const Real G = universe.get_sim_config().G;

        energy[i] += -G*((mass1*mass2)/distance_vec.norm2());
    }

Тип Real - float.

Мои теории

Я новичок, когда дело касается численного интегрирования (поэтому я разместил этот вопрос здесь). Однако вот несколько теорий о том, что может быть неправильным:

  • Когда дело доходит до n ›= 2, в алгоритме Velocity Verlet есть ловушка, и я в нее попал.
  • Где-то в приведенном выше коде есть ошибка реализации, и я ее не вижу.
  • Ошибки, связанные с вычислениями чисел с плавающей запятой, накапливаются из-за небольших перемещений большого тела. (Скорее всего, не тот случай, см. Правку ниже.)
  • Во время попыток отладить это я столкнулся с Energy drift в моделировании молекулярной динамики. Может, вот что здесь происходит?

Не похоже, что орбита разваливается, но это не тот результат, которого я ожидал, и я хочу знать, почему.

Может ли кто-нибудь помочь мне разгадать эту загадку?

Редактировать:

Я проверил двойную точность, и единственное отличие состоит в том, что теперь энергия наименьшей вращающейся массы намного стабильнее.

введите описание изображения здесь

Сейчас наблюдается тенденция к увеличению даже для самой маленькой массы. Это намекает на то, что это не проблема с точностью вычислений.


person Bill2462    schedule 31.03.2021    source источник
comment
Сильно ли изменится результат моделирования, если в расчетах использовать двойную точность?   -  person MatG    schedule 31.03.2021
comment
Нет, это не устраняет проблему. Это только делает линию более гладкой для самой маленькой массы. Фактически после перехода на двойную точность эффект увеличения энергии наблюдается даже для самой маленькой массы.   -  person Bill2462    schedule 31.03.2021
comment
Похоже, установка ненулевой массы вращающегося тела вызывает рост энергии. Рост пропорционален массе на орбите.   -  person Bill2462    schedule 31.03.2021
comment
Я бы проверил этот инвариант: в конфигурации с двумя телами, учитывая два момента моделирования, векторная разность скорости каждого тела всегда должна указывать на другое тело; если есть компонент, касающийся траектории, среднее значение которого не равно нулю, это изменит энергию системы.   -  person MatG    schedule 01.04.2021
comment
Спасибо, MatG! Ваше предложение позволило мне найти проблему и исправить ее.   -  person Bill2462    schedule 02.04.2021
comment
Рад, что вы нашли свой ответ, но я почти разочарован, потому что из любопытства я переписывал с нуля симуляцию n-body на основе ваших фрагментов, чтобы воспроизвести проблему (на самом деле я хотел дать несколько советов по вашему стилю кодирования C ++) .   -  person MatG    schedule 02.04.2021
comment
См., Например, stackoverflow.com/questions/64190036/ для почти идентичной проблемы. Дрейф в таких экспериментальных кодах почти всегда является следствием смешивания обновленных со старыми значениями, в том числе в RK4 и других методах решателя.   -  person Lutz Lehmann    schedule 02.04.2021
comment
Цель проекта - создать простой физический движок со столкновениями, гравитацией и базовой физикой упругого тела за 2 недели и создать короткую презентацию для моих одноклассников. Стиль кода здесь не стоит на первом месте в моем списке приоритетов. Однако все советы относительно моего стиля кода приветствуются, и я постараюсь применить их в будущем.   -  person Bill2462    schedule 02.04.2021
comment
@ bill2462 Не волнуйтесь, в конце концов, это всего лишь инструмент ;-)   -  person MatG    schedule 02.04.2021


Ответы (2)


Я выяснил, в чем дело.

Проблема оказалась в обновлении положения тел по одному. Вычисление ускорения предполагает, что никакое тело не было перемещено между временными шагами, однако обновление одного за другим привело к тому, что некоторые тела имели положение от t, а некоторые от t + dt. Это различие в этой конкретной системе привело к тому, что векторная разность скорости движущегося по орбите тела не была идеально направлена ​​к центру масс. Фактически генерировалась небольшая тангенциальная составляющая, и в систему добавлялась энергия. Ошибка была небольшая, но со временем она накапливалась и становилась заметной.

Я исправил проблему, выполнив каждый этап алгоритма верлета сразу для всех тел. Вот исправленный код интегратора:

    for(std::size_t i=0; i<get_size(); i++)
    {
        position(i, 0) += velocity(i, 0)*sim_config.timestep + acceleration(i, 0)*sim_config.timestep*sim_config.timestep*static_cast<Real>(0.5);
        position(i, 1) += velocity(i, 1)*sim_config.timestep + acceleration(i, 1)*sim_config.timestep*sim_config.timestep*static_cast<Real>(0.5);
        position(i, 2) += velocity(i, 2)*sim_config.timestep + acceleration(i, 2)*sim_config.timestep*sim_config.timestep*static_cast<Real>(0.5);
    }
    
    for(std::size_t i=0; i<get_size(); i++)
    {
        velocity(i, 0) += acceleration(i, 0)*sim_config.timestep*static_cast<Real>(0.5);
        velocity(i, 1) += acceleration(i, 1)*sim_config.timestep*static_cast<Real>(0.5);
        velocity(i, 2) += acceleration(i, 2)*sim_config.timestep*static_cast<Real>(0.5);
    }

    for(std::size_t i=0; i<get_size(); i++)
    {
        const Vector3D<Real> forces = compute_net_grawitational(i);
        acceleration(i, 0) = forces.x/mass(i);
        acceleration(i, 1) = forces.y/mass(i);
        acceleration(i, 2) = forces.z/mass(i);
    }

    for(std::size_t i=0; i<get_size(); i++)
    {
        velocity(i, 0) += acceleration(i, 0)*sim_config.timestep*static_cast<Real>(0.5);
        velocity(i, 1) += acceleration(i, 1)*sim_config.timestep*static_cast<Real>(0.5);
        velocity(i, 2) += acceleration(i, 2)*sim_config.timestep*static_cast<Real>(0.5);

Теперь энергия не увеличивается даже для самого тяжелого вращающегося тела:

введите описание изображения здесь

Энергия все еще дрейфует, однако со временем различия, кажется, усредняются, а изменения незначительны по сравнению с общим значением. График автоматически ранжируется, поэтому изменения кажутся большими, но они находятся в пределах + - 1% от общей энергии, что приемлемо для моего приложения.

person Bill2462    schedule 01.04.2021

Прежде чем OP нашел ответ, я из любопытства работал над воспроизведением проблемы; Я начинал фазу отладки (беглый взгляд на первые данные показывает наличие как минимум одной серьезной ошибки), но думаю, что откажусь от задачи (праздники почти закончились). Перед тем, как удалить код с жесткого диска, я все равно решил положить его сюда для потомков.

#include <cmath>
#include <limits>
#include <iostream>
#include <fstream>
#include <string>
#include <exception>
#include <vector>


//----------------------------------------------------------------------
// Some floating point facilities
//----------------------------------------------------------------------
// Some floating point facilities
constexpr inline bool is_zero(double x) noexcept
{
    return std::fabs(x) < std::numeric_limits<double>::epsilon();
}

constexpr inline double ratio(const double n, const double d) noexcept
{
    return is_zero(d) ? n/std::numeric_limits<double>::epsilon() : n/d;
}


////////////////////////////////////////////////////////////////////////
struct Vector3D ////////////////////////////////////////////////////////
{
    double x, y, z;

    Vector3D() noexcept : x(0.0), y(0.0), z(0.0) {}
    Vector3D(const double X, const double Y, const double Z) noexcept
       : x(X), y(Y), z(Z) {}

    Vector3D operator+=(const Vector3D& other) noexcept
       {
        x+=other.x; y+=other.y; z+=other.z;
        return *this;
       }

    Vector3D operator-() const noexcept
       {
        return Vector3D{-x, -y, -z};
       }

    friend Vector3D operator+(const Vector3D& v1, const Vector3D& v2) noexcept
       {
        return Vector3D{v1.x+v2.x, v1.y+v2.y, v1.z+v2.z};
       }

    friend Vector3D operator-(const Vector3D& v1, const Vector3D& v2) noexcept
       {
        return Vector3D{v1.x-v2.x, v1.y-v2.y, v1.z-v2.z};
       }

    friend Vector3D operator*(double k, const Vector3D& v) noexcept
       {
        return Vector3D{k*v.x, k*v.y, k*v.z};
       }

    friend Vector3D operator/(const Vector3D& v, double k) noexcept
       {
        return Vector3D{v.x/k, v.y/k, v.z/k};
       }

    friend std::ostream& operator<<(std::ostream& os, const Vector3D& v)
       {
        os << v.x << ',' << v.y << ',' << v.z;
        return os;
       }

    double norm2() const noexcept { return x*x + y*y + z*z; }
    double norm() const noexcept { return std::sqrt(norm2()); }
};


////////////////////////////////////////////////////////////////////////
class Body /////////////////////////////////////////////////////////////
{
 public:
    Body(const double m, const Vector3D& pos, const Vector3D& spd) noexcept
       : i_mass(m), i_pos(pos), i_spd(spd) {}

    double mass() const noexcept { return i_mass; }
    const Vector3D& position() const noexcept { return i_pos; }
    const Vector3D& speed() const noexcept { return i_spd; }
    const Vector3D& acceleration() const noexcept { return i_acc; }

    Vector3D distance_from(const Body& other) const noexcept
       {
        return position() - other.position();
       }

    double kinetic_energy() const noexcept
       {// ½ m·V²
        return 0.5 * i_mass * i_spd.norm2();
       }

    Vector3D gravitational_force_on(const Body& other, const double G) const noexcept
       {// G · M·m / d²
        Vector3D disp = distance_from(other);
        double d = disp.norm();
        return ratio(G * mass() * other.mass(), d*d*d) * disp;
       }

    double gravitational_energy_with(const Body& other, const double G) const noexcept
       {// U = -G · mi·mj / d
        double d = distance_from(other).norm();
        return ratio(G * mass() * other.mass(), d);
       }

    void apply_force(const Vector3D& f)
       {// Newton's law: F=ma
        i_acc = f / i_mass;
       }

    void evolve_speed(const double dt) noexcept
       {
        i_spd += dt * i_acc;
       }

    void evolve_position(const double dt) noexcept
       {
        i_pos += dt * i_spd;
       }

 private:
    double i_mass;
    Vector3D i_pos, // Position [<space>]
             i_spd, // Speed [<space>/<time>]
             i_acc; // Acceleration [<space>/<time>²]
};


////////////////////////////////////////////////////////////////////////
class Universe /////////////////////////////////////////////////////////
{
 public:
    Universe(const double g) noexcept : G(g) {}

    void evolve(const double dt) noexcept
       {
        for(Body& body : i_bodies)
           {
            body.evolve_speed(dt/2);
            body.evolve_position(dt);
           }

        for( auto ibody=i_bodies.begin(); ibody!=i_bodies.end(); ++ibody )
           {
            Vector3D f = gravitational_force_on_body(ibody);
            ibody->apply_force(f);
            ibody->evolve_speed(dt/2);
           }
       }

    double kinetic_energy() const noexcept
       {// K = ∑ ½ m·V²
        double Ek = 0.0;
        for( const Body& body : i_bodies ) Ek += body.kinetic_energy();
        return Ek;
       }

    double gravitational_energy() const noexcept
       {// U = ½ ∑ -G · mi·mj / d
        double Eu = 0.0;
        for( auto ibody=i_bodies.begin(); ibody!=i_bodies.end(); ++ibody )
           {// Iterate over all the other bodies
            for( auto ibody2=i_bodies.begin(); ibody2!=ibody; ++ibody2 )
                Eu += ibody->gravitational_energy_with(*ibody2,G);
            for( auto ibody2=ibody+1; ibody2!=i_bodies.end(); ++ibody2 )
                Eu += ibody->gravitational_energy_with(*ibody2,G);
           }
        return Eu/2;
       }

    double total_energy() const noexcept { return kinetic_energy() + gravitational_energy(); }

    Vector3D center_of_mass() const noexcept
       {// U = ∑ m·vpos / M
        Vector3D c;
        double total_mass = 0.0;
        for( const Body& body : i_bodies )
           {
            c += body.mass() * body.position();
            total_mass += body.mass();
           }
        return c/total_mass;
       }

    Vector3D gravitational_force_on_body( std::vector<Body>::const_iterator ibody ) const noexcept
       {// F = ∑ G · m·mi / di²
        Vector3D f;
        // Iterate over all the other bodies
        for( auto ibody2=i_bodies.begin(); ibody2!=ibody; ++ibody2 )
            f += ibody2->gravitational_force_on(*ibody,G);
        for( auto ibody2=ibody+1; ibody2!=i_bodies.end(); ++ibody2 )
            f += ibody2->gravitational_force_on(*ibody,G);
        return f;
       }

    void add_body(const double m, const Vector3D& pos, const Vector3D& spd)
       {
        i_bodies.emplace_back(m,pos,spd);
       }

    const std::vector<Body>& bodies() const noexcept { return i_bodies; }

    const double G; // Gravitational constant

 private:
    std::vector<Body> i_bodies;
};



////////////////////////////////////////////////////////////////////////
class Simulation ///////////////////////////////////////////////////////
{
 public:

    class Data /////////////////////////////////////////////////////////
    {
     public:

        struct Sample ///////////////////////////////////////////////////
        {
         double time;
         std::vector<Body> bodies; // Bodies status
         Vector3D Cm; // Center of mass
         double Ek, // Kinetic energy
                Eu; // Potential energy

         Sample(const double t,
                const std::vector<Body>& bd,
                const Vector3D& cm,
                const double ek,
                const double eu) : time(t),
                                   bodies(bd),
                                   Cm(cm),
                                   Ek(ek),
                                   Eu(eu) {}
        };

        void init(const std::vector<std::string>::size_type N) noexcept
           {
            i_samples.clear();
            i_samples.reserve(N);
           }

        void add(Sample&& s)
           {
            i_samples.push_back(s);
           }

        void save_to_file(std::string fpath) const
           {
            std::ofstream f (fpath, std::ios::out);
            if(!f.is_open()) throw std::runtime_error("Unable to open file " + fpath);
            //f << "time,total-energy\n";
            //for(const Sample& s : i_samples)
            //    f << s.time << ',' << (s.Ek+s.Eu) << '\n';
            f << "time,bodies-xyz-pos\n";
            for(const Sample& s : i_samples)
               {
                f << s.time;
                for(const Body& body : s.bodies)
                    f << ',' << body.position();
                f << '\n';
               }
           }

        const std::vector<Sample>& samples() const noexcept { return i_samples; }

     private:
        std::vector<Sample> i_samples;
    };

    //                                 Total time    Time increment
    void execute(Universe& universe, const double T, const double dt)
       {
        auto N = static_cast<std::size_t>(T/dt + 1);
        i_data.init(N);

        double t = 0.0;
        do {
            universe.evolve(dt);

            i_data.add( Data::Sample(t, universe.bodies(),
                                      universe.center_of_mass(),
                                      universe.kinetic_energy(),
                                      universe.gravitational_energy() ) );
            t += dt;
           }
        while(t<T);
       }

    const Data& data() const noexcept { return i_data; }

 private:
    Data i_data;
};


//----------------------------------------------------------------------
int main()
{
    // Creating a universe with a certain gravitational constant
    Universe universe(1); // Our universe: 6.67408E-11 m³/kg s²
    // Adding bodies (mass, initial position and speed)
    universe.add_body(1000, Vector3D(0,0,0), Vector3D(0,0,0));
    universe.add_body(100, Vector3D(10,0,0), Vector3D(0,10,0));

    // Simulation settings
    Simulation sim;
    const double T = 100; // Total time
    const double dt = 0.001; // Iteration time
    std::cout << "Simulating T=" << T << " dt=" << dt << "...";
    sim.execute(universe, T, dt);
    std::cout << "...Done";

    // Now do something with the simulation data...
    // ...Edit as desired
    //sim.data().save_to_file("data.txt");

   {// Energies
    std::string fname = "energies.txt";
    std::ofstream f (fname, std::ios::out);
    if(!f.is_open()) throw std::runtime_error("Unable to open file " + fname);
    f << "time,kinetic,potential,total\n";
    for(const Simulation::Data::Sample& s : sim.data().samples())
        f << s.time << ',' << s.Ek << ',' << s.Eu << ',' << (s.Ek+s.Eu) << '\n';
   }

   {// Positions of...
    std::size_t idx = 1; // ...Second body
    std::string fname = "body" + std::to_string(idx) + ".txt";
    std::ofstream f (fname, std::ios::out);
    if(!f.is_open()) throw std::runtime_error("Unable to open file " + fname);
    f << "time,body" << idx << ".x,body" << idx << ".y,body" << idx << ".z\n";
    for(const Simulation::Data::Sample& s : sim.data().samples())
        f << s.time << ',' << (s.bodies.begin()+idx)->position() << '\n';
   }
}
person MatG    schedule 05.04.2021