3

The problem

I implemented velocity verlet algorithm to compute trajectories of 2 bodies interacting with each other gravitationally (newtonian gravity only). Orbiting smaller body has a very small mass, body that is in the center of the orbit has a large mass.

In theory Velocity Verlet should not change total energy of the system (it will oscilate but over time the average will remain close to the initial energy).

However in practice I observed increase of energy over time.

Results

Here are some results which illustrate the problem. All simulations were performed with a timestep dt=0.001. Orbited body had a mass of 1000 and gravitational constant of the universe was set to G=1.0

In all cases smaller body initial position was {0, 0, 1} and it's initial velocity was {0, 32, 0}. Initial velocity of larger body was {0,0,0}.

Case 1 (small body mass = 0.00001)

Here is the trajectory of the smaller body:

case 1 trajectory

And here is energy over 100k steps.

case 1 energy

As we can see the energy does not change by a lot. Small changes are likely due to inaccuracies in the calculations.

Case 1 (small body mass = 0.001)

Here is trajectory of the orbiting body:

case 2 trajectory

And here is total energy:

case 2 energy

As we can see now system is gaining energy.

Case 3 (small body mass = 1)

Here is trajectory of the orbiting body:

case 3 trajectory

And here is total energy:

case 3 energy

Now we are gaining a lot of energy.

code

Here is the source code that is performing all calculations:

Code for advancing integrator:

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;
}

Here is code for computing net gravitational force acting on the body:

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;
}

Here is code that implements norm2():

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

Finally here is code that computes results plotted previously:

    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());
    }

Type Real is float.

My theories

I'm a beginner when it comes to numerical integration (that's why I posted this question here). However here are some theories about what might be wrong:

  • There is some pitfall in the Velocity Verlet algorithm when it comes to n>=2 and I've fallen into it.
  • There is implementation error somewhere in the above code and I don't see it.
  • Errors due to floating point number calculations accumulate due to small movements of the large body. (Likely not the case see edit below.)
  • During attempts to debug this I've come across Energy drift in molecular dynamics simulation. Maybe this is what is happening here?

It doesn't seem like the orbit is falling apart but it is not the result that I expected and I want to know why.

Can someone help me solve this mystery?

Edit:

I have tested double precision and only change is that now the energy of the smallest orbiting mass is much more stable.

enter image description here

Now increasing trend can be seen even for the smallest mass. This hints that it is not a problem with precision of calculations.

Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
Bill2462
  • 81
  • 10
  • 1
    Does the simulation result change a lot if you use double precision in the calculations? – MatG Mar 31 '21 at 19:26
  • No it does not eliminate the problem. It only makes the line smoother for the smallest mass. Actually after changing to double precision the effect of growing energy is observable even for the smallest mass. – Bill2462 Mar 31 '21 at 19:35
  • Look's like setting non zero mass of the orbiting body causes the energy to grow. Growth is proportional to the orbiting mass. – Bill2462 Mar 31 '21 at 20:14
  • 1
    I would check this invariant: in the two body configuration, considering two simulation instants, the vectorial difference of the speed of each body should always point toward the other body; if there is a component tangent to the trajectory whose average is not null, that would change the energy of the system. – MatG Apr 01 '21 at 06:17
  • 1
    Thank you MatG! Your suggestion allowed me to find the problem and fix it. – Bill2462 Apr 01 '21 at 21:27
  • 1
    Glad you found your answer, but I'm almost disappointed because out of curiosity I was rewriting from scratch a n-body simulation based on your snippets to reproduce the problem (in reality I was eager to give some advices on your c++ coding style). – MatG Apr 02 '21 at 08:15
  • 1
    See for instance https://stackoverflow.com/questions/64190036/how-can-i-update-c-class-members-with-a-function/64192329#64192329 for an almost identical problem. Drift in such experimental codes is almost always a consequence of mixing updated with old values, also in RK4 and other solver methods. – Lutz Lehmann Apr 02 '21 at 09:48
  • The goal of the project is to create a simple physics engine with collisions, gravity and basic elastic body physics in 2 weeks and create a short presentation for my classmates. Code style is not exactly on top of my priority list here. However all advises regarding my code style are welcome and I will try to apply them in the future. – Bill2462 Apr 02 '21 at 11:06
  • @bill2462 Don't worry, in the end it's just a tool ;-) – MatG Apr 02 '21 at 17:34

2 Answers2

3

I found out what was wrong.

What turned out to be a problem was updating the position of bodies one by one. Computation of acceleration assumes that no body was moved between the timesteps however updating one by one resulted in some bodies having position from t and some from t + dt. That difference in this specific system caused vectorial difference of the orbiting body speed to not be ideally pointing towards the center of mass. In effect a small tangential component was generated and energy was being added to the system. The error was small but over time it accumulated and was visible.

I fixed the problem by performing each stage of verlet algorithm on all bodies at once. Here is revised code for the integrator:

    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);

Now the energy is not increasing even for the heaviest orbiting body:

enter image description here

The energy is still drifting however over time the differences seem to average out and changes are small relative to the total value. Plot is auto ranged so changes seem large but they are within +- 1% of total energy which is acceptable for my application.

Bill2462
  • 81
  • 10
0

Before OP found its answer I was working on reproducing the problem, out of curiosity; I was starting the debug phase (a quick peek to the first data reveals the presence of at least one major bug), but I think I'll drop the task (holidays are almost finished). Before deleting the code from my hard disk I thought to put it here anyway, for posterity.

#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';
   }
}
MatG
  • 574
  • 2
  • 7
  • 19