Search code examples
c++differential-equationsrunge-kutta

Runge Kutta's Method circular motion


I've been asked to solve this differential equation:

(x,y,vx,vy)'=(vx,vy,vy,-vx)

which should return a circular motion with a 2*pi period. I implemented the function:

class FunzioneBase 
{
  public:
    virtual VettoreLineare Eval(double t, const VettoreLineare& v) const = 0; 
};

class Circonferenza: public FunzioneBase
{
  private:
    double _alpha;

  public:
    Circonferenza(double alpha) { _alpha = alpha; };
    void SetAlpha(double alpha) { _alpha = alpha; };
    virtual VettoreLineare Eval(double t, const VettoreLineare& v) const;
};

VettoreLineare Circonferenza::Eval(double t, const VettoreLineare& v) const
{
  VettoreLineare y(4);
  if (v.GetN() != 4) 
  {
    std::cout << "errore" << std::endl;
    return 0;
  };

  y.SetComponent(0, v.GetComponent(2));
  y.SetComponent(1, v.GetComponent(3));
  y.SetComponent(2, pow(pow(v.GetComponent(0), 2.) + pow(v.GetComponent(1), 2.), _alpha) * v.GetComponent(3));
  y.SetComponent(3, - pow(pow(v.GetComponent(0), 2.)  + pow(v.GetComponent(1), 2.), _alpha)) * v.GetComponent(2));

  return y;
};

where _alpha equals to 0. Now, this works just fine with Euler's method: if I integrate this ODE for 2 * pi * 10, given the initial condition (1, 0, 0, -1), with a 0.003 precision, I get that the position is comparable to (1, 0) within a range of 1 ± 0.1, as we should expect. But if I integrate the same ODE with Runge Kutta's Method (with a 0.003 precision, for 2 * pi * 10 seconds) implemented as follows:

class EqDifferenzialeBase 
{
  public:
    virtual VettoreLineare Passo (double t, VettoreLineare& x, double h, FunzioneBase* f) const = 0;
};

class Runge_Kutta: public EqDifferenzialeBase 
{
  public:
    virtual VettoreLineare Passo(double t, VettoreLineare& v, double h,  FunzioneBase* f) const;
};

VettoreLineare Runge_Kutta::Passo(double t, VettoreLineare& v, double h, FunzioneBase* _f) const
{ 
  VettoreLineare k1 = _f->Eval(t, v);
  VettoreLineare k2 = _f->Eval(t + h / 2., v + k1 *(h / 2.));
  VettoreLineare k3 = _f->Eval(t + h / 2., v + k2 * (h / 2.));
  VettoreLineare k4 = _f->Eval(t + h, v + k3 * h);
  VettoreLineare y = v + (k1 + k2 * 2. + k3 * 2. + k4) * (h / 6.);

  return y;
}

the program returns an x position which equals to 0.39 aproximately, when the precision should theorically be, for a 4th order Runge Kutta's method, around 1E-6. I checked and found that the period, with Runge_Kutta's, seems to almost quadruplicate (since in a 2 * pi lapse, x gets from 1 to 0.48), but I don't understand why. This is the content of my main:

VettoreLineare DatiIniziali (4);
Circonferenza* myCirc = new Circonferenza(0);

DatiIniziali.SetComponent(0, 1.);
DatiIniziali.SetComponent(1, 0.);
DatiIniziali.SetComponent(2, 0.);
DatiIniziali.SetComponent(3, -1.);
double passo = 0.003;
Runge_Kutta myKutta;

for(int i = 0; i <= 2. * M_PI / passo; i++)
{
  DatiIniziali = myKutta.Passo(0, DatiIniziali, passo, myCirc);
  cout << DatiIniziali.GetComponent(0) << endl;
};

cout << 1 - DatiIniziali.GetComponent(0) << endl;

Thank you in advance.


Solution

  • Update: One error identified: Always compile with the -Wall option to catch all warnings and automatic code corrections of the compiler. Then you would have found

    fff: In member function ‘virtual VettoreLineare Circonferenza::Eval(double, const VettoreLineare&) const’:
    fff:xxx:114: error: invalid operands of types ‘void’ and ‘double’ to binary ‘operator*’
         y.SetComponent(3, - pow(pow(v.GetComponent(0), 2.)  + pow(v.GetComponent(1), 2.), _alpha)) * v.GetComponent(2));
                                                                                                                      ^
    

    where you are closing too early after _alpha so that the void of SetComponent gets to be a factor.


    Update II: second error identified: In another post of yours the code of the linear vector class is given. There, in contrast to the addition (operator+), the scalar-vector product (operator*(double)) is modifying the calling instance. Thus in computing k2 the components of k1 get multiplied with h/2. But then this modified k1 (and also modified k2 and k3) are used in assembling the result y resulting in some almost completely useless state update.


    Original rapid prototyping: I can tell you that a stripped down bare-bones implementation in python works flawlessly

    import numpy as np
    
    def odeint(f,t0,y0,tf,h):
        '''Classical RK4 with fixed step size, modify h to fit
            the full interval'''
        N = np.ceil( (tf-t0)/h )
        h = (tf-t0)/N
    
        t = t0
        y = np.array(y0)
        for k in range(int(N)):
            k1 = h*np.array(f(t      ,y       ))
            k2 = h*np.array(f(t+0.5*h,y+0.5*k1))
            k3 = h*np.array(f(t+0.5*h,y+0.5*k2))
            k4 = h*np.array(f(t+    h,y+    k3))
            y = y + (k1+2*(k2+k3)+k4)/6
            t = t + h
        return t, y
    
    def odefunc(t,y):
        x,y,vx,vy = y
        return [ vx,vx,vy,-vx ]
    
    pi = 4*np.arctan(1);
    
    print odeint(odefunc, 0, [1,0,0,-1], 2*pi, 0.003)
    

    ends with

    (t, [ x,y,vx,vy]) = (6.2831853071794184,
        [  1.00000000e+00,  -6.76088739e-15,   4.23436503e-12,
        -1.00000000e+00])
    

    as expected. You will need a debugger or intermediate output to find where the computation goes wrong.