1

I am trying to solve for the position of a body orbiting a much more massive body, using the idealization that the much more massive body doesn't move. I am trying to solve for the position in cartesian coordinates using 4th order Runge-Kutta in python.

Here is my code:

dt = .1
t = np.arange(0,10,dt)

vx = np.zeros(len(t))
vy = np.zeros(len(t))
x = np.zeros(len(t))
y = np.zeros(len(t))

vx[0] = 10 #initial x velocity
vy[0] = 10 #initial y velocity
x[0] = 10 #initial x position
y[0] = 0 #initial y position

M = 20

def fx(x,y,t): #x acceleration
     return -G*M*x/((x**2+y**2)**(3/2))

def fy(x,y,t): #y acceleration
     return -G*M*y/((x**2+y**2)**(3/2))

def rkx(x,y,t,dt): #runge-kutta for x

     kx1 = dt * fx(x,y,t)
     mx1 = dt * x
     kx2 = dt * fx(x + .5*kx1, y + .5*kx1, t + .5*dt)
     mx2 = dt * (x + kx1/2)
     kx3 = dt * fx(x + .5*kx2, y + .5*kx2, t + .5*dt)
     mx3 = dt * (x + kx2/2)
     kx4 = dt * fx(x + kx3, y + x3, t + dt)
     mx4 = dt * (x + kx3)

     return (kx1 + 2*kx2 + 2*kx3 + kx4)/6
     return (mx1 + 2*mx2 + 2*mx3 + mx4)/6

 def rky(x,y,t,dt): #runge-kutta for y

     ky1 = dt * fy(x,y,t)
     my1 = dt * y
     ky2 = dt * fy(x + .5*ky1, y + .5*ky1, t + .5*dt)
     my2 = dt * (y + ky1/2)
     ky3 = dt * fy(x + .5*ky2, y + .5*ky2, t + .5*dt)
     my3 = dt * (y + ky2/2)
     ky4 = dt * fy(x + ky3, y + ky3, t + dt)
     my4 = dt * (y + ky3)

     return (ky1 + 2*ky2 + 2*ky3 + ky4)/6
     return (my1 + 2*my2 + 2*my3 + my4)/6

for n in range(1,len(t)): #solve using RK4 functions
    vx[n] = vx[n-1] + fx(x[n-1],y[n-1],t[n-1])*dt
    vy[n] = vy[n-1] + fy(x[n-1],y[n-1],t[n-1])*dt
    x[n] = x[n-1] + vx[n-1]*dt
    y[n] = y[n-1] + vy[n-1]*dt

Originally, no matter which way I tweaked the code, I was getting an error on my for loop, either "object of type 'float' has no len()" (I didn't understand what float python could be referring to), or "setting an array element with a sequence" (I also didn't understand what sequence it meant). I've managed to get rid of the errors, but my results are just wrong. I get vx and vy arrays of 10s, an x array of integers from 10. to 109., and a y array of integers from 0. to 99.

I suspect there are issues with fx(x,y,t) and fy(x,y,t) or with the way I have coded the runge-kutta functions to go with fx and fy, because I've used the same runge-kutta code for other functions and it works fine.

I greatly appreciate any help in figuring out why my code isn't working. Thank you.

Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
corgiworld
  • 37
  • 1
  • 7
  • 1
    You cannot use two **return** statement in your function. If you want to return more than one value from a **function** you can put them in `list` or `tuple`. For example, suppose you want to return `a` and `b` from your function `myfunc(*args, **kwargs)` you can do `return (a, b)`. Then when you call `myfunc`, you can do `value1, value2 = myfunc(*args, **kwargs)`. You can apply this to the functions `**rkx** and **rky** – eapetcho Dec 06 '18 at 07:12

2 Answers2

4

Physics

The Newton law gives you a second order ODE u''=F(u) with u=[x,y]. Using v=[x',y'] you get the first order system

u' = v
v' = F(u)

which is 4-dimensional and has to be solved using a 4 dimensional state. The only reduction available is to use the Kepler laws which allow to reduce the system to a scalar order one ODE for the angle. But that is not the task here.

But to get the scales correct, for a circular orbit of radius R with angular velocity w one gets the identity w^2*R^3=G*M which implies that the speed along the orbit is w*R=sqrt(G*M/R) and period T=2*pi*sqrt(R^3/(G*M)). With the data given, R ~ 10, w ~ 1, thus G*M ~ 1000 for a close-to-circular orbit, so with M=20 this would require G between 50 and 200, with an orbital period of about 2*pi ~ 6. The time span of 10 could represent one half to about 2 or 3 orbits.

Euler method

You correctly implemented the Euler method to calculate values in the last loop of your code. That it may look un-physical can be because the Euler method continuously increases the orbit, as it moves to the outside of convex trajectories following the tangent. In your implementation this outward spiral can be seen for G=100.

enter image description here

This can be reduced in effect by choosing a smaller step size, such as dt=0.001.

enter image description here

You should select the integration time to be a good part of a full orbit to get a presentable result, with above parameters you get about 2 loops, which is good.

RK4 implementation

You made several errors. Somehow you lost the velocities, the position updates should be based on the velocities.

Then you should have halted at fx(x + .5*kx1, y + .5*kx1, t + .5*dt) to reconsider your approach as that is inconsistent with any naming convention. The consistent, correct variant is

fx(x + .5*kx1, y + .5*ky1, t + .5*dt) 

which shows that you can not decouple the integration of a coupled system, as you need the y updates alongside the x updates. Further, the function values are the accelerations, thus update the velocities. The position updates use the velocities of the current state. Thus the step should start as

 kx1 = dt * fx(x,y,t) # vx update
 mx1 = dt * vx        # x update
 ky1 = dt * fy(x,y,t) # vy update
 my1 = dt * vy        # y update

 kx2 = dt * fx(x + 0.5*mx1, y + 0.5*my1, t + 0.5*dt)
 mx2 = dt * (vx + 0.5*kx1)
 ky2 = dt * fy(x + 0.5*mx1, y + 0.5*my1, t + 0.5*dt)
 my2 = dt * (vy + 0.5*ky1)

etc.

However, as you see, this already starts to become unwieldy. Assemble the state into a vector and use a vector valued function for the system equations

M, G = 20, 100
def orbitsys(u):
     x,y,vx,vy = u
     r = np.hypot(x,y)
     f = G*M/r**3
     return np.array([vx, vy, -f*x, -f*y]);

Then you can use a cook-book implementation of the Euler or Runge-Kutta step

def Eulerstep(f,u,dt): return u+dt*f(u)

def RK4step(f,u,dt):
    k1 = dt*f(u)
    k2 = dt*f(u+0.5*k1)
    k3 = dt*f(u+0.5*k2)
    k4 = dt*f(u+k3)
    return u + (k1+2*k2+2*k3+k4)/6

and combine them into an integration loop

def Eulerintegrate(f, y0, tspan):
    y = np.zeros([len(tspan),len(y0)])
    y[0,:]=y0
    for k in range(1, len(tspan)):
        y[k,:] = Eulerstep(f, y[k-1], tspan[k]-tspan[k-1])
    return y


def RK4integrate(f, y0, tspan):
    y = np.zeros([len(tspan),len(y0)])
    y[0,:]=y0
    for k in range(1, len(tspan)):
        y[k,:] = RK4step(f, y[k-1], tspan[k]-tspan[k-1])
    return y

and invoke them with your given problem

dt = .1
t = np.arange(0,10,dt)
y0 = np.array([10, 0.0, 10, 10])

sol_euler = Eulerintegrate(orbitsys, y0, t)
x,y,vx,vy = sol_euler.T
plt.plot(x,y)

sol_RK4 = RK4integrate(orbitsys, y0, t)
x,y,vx,vy = sol_RK4.T
plt.plot(x,y)
Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
  • Thanks for your help, LutzL. Looking through my RK code now I can see the several mistakes you're talking about. I fixed them and now it to works. – corgiworld Dec 06 '18 at 23:47
  • I think using backward Euler you can get rid of the increasing orbit problem. But anyway RK4 is a better method in terms of error introduced when integrating. –  Dec 07 '18 at 15:40
  • 1
    @SembeiNorimaki : Then you get a decreasing orbit problem, as implicit Euler behaves like explicit Euler backwards in time. Alternating explicit and implicit steps gives the midpoint method which is symplectic. Any variant of Verlet integration will also give better results. – Lutz Lehmann Dec 07 '18 at 15:45
  • @LutzL could you please post the complete code? I'm trying to integrate your solution into OPs code, but I'm having difficulties. A minimal code that includes this solution to run would be very appreciated. Thanks –  Dec 13 '18 at 12:47
  • @SembeiNorimaki : done, for the vector-oriented version. To produce the Euler plots above, just take the OP code and add `G=100`, one can cut out the RK4 related procedures as they do nothing. Executing `plt.grid(); plt.show()` at the end of the new code should show similar plots. – Lutz Lehmann Dec 13 '18 at 14:01
1

You are not using rkx, rky functions anywhere! There are two return at the end of function definition you should use return [(kx1 + 2*kx2 + 2*kx3 + kx4)/6, (mx1 + 2*mx2 + 2*mx3 + mx4)/6] (as pointed out by @eapetcho). Also, your implementation of Runge-Kutta is not clear to me.

You have dv/dt so you solve for v and then update r accordingly.

for n in range(1,len(t)): #solve using RK4 functions
    vx[n] = vx[n-1] + rkx(vx[n-1],vy[n-1],t[n-1])*dt
    vy[n] = vy[n-1] + rky(vx[n-1],vy[n-1],t[n-1])*dt
    x[n] = x[n-1] + vx[n-1]*dt
    y[n] = y[n-1] + vy[n-1]*dt

Here is my version of the code

import numpy as np

#constants
G=1
M=1
h=0.1

#initiating variables
rt = np.arange(0,10,h)
vx = np.zeros(len(rt))
vy = np.zeros(len(rt))
rx = np.zeros(len(rt))
ry = np.zeros(len(rt))

#initial conditions
vx[0] = 10 #initial x velocity
vy[0] = 10 #initial y velocity
rx[0] = 10 #initial x position
ry[0] = 0 #initial y position

def fx(x,y): #x acceleration
     return -G*M*x/((x**2+y**2)**(3/2))

def fy(x,y): #y acceleration
     return -G*M*y/((x**2+y**2)**(3/2))

def rk4(xj, yj):
    k0 = h*fx(xj, yj)
    l0 = h*fx(xj, yj)

    k1 = h*fx(xj + 0.5*k0 , yj + 0.5*l0)
    l1 = h*fy(xj + 0.5*k0 , yj + 0.5*l0)

    k2 = h*fx(xj + 0.5*k1 , yj + 0.5*l1)
    l2 = h*fy(xj + 0.5*k1 , yj + 0.5*l1)

    k3 = h*fx(xj + k2, yj + l2)
    l3 = h*fy(xj + k2, yj + l2)

    xj1 = xj + (1/6)*(k0 + 2*k1 + 2*k2 + k3)
    yj1 = yj + (1/6)*(l0 + 2*l1 + 2*l2 + l3)
    return (xj1, yj1)

for t in range(1,len(rt)):
    nv = rk4(vx[t-1],vy[t-1])
    [vx[t],vy[t]] = nv
    rx[t] = rx[t-1] + vx[t-1]*h
    ry[t] = ry[t-1] + vy[t-1]*h

I suspect there are issues with fx(x,y,t) and fy(x,y,t)

This is the case, I just checked my code for fx=3 and fy=y and I got a nice trajectory.

Here is the ry vs rx plot:

fx=3, fy=y trajectory

Cheesebread
  • 91
  • 1
  • 10
  • The Newton law for the planetary motion is a second order DE, you need the 4 state variables, 2 for position and 2 for velocity. – Lutz Lehmann Dec 06 '18 at 10:54
  • Your "correction" just replaced one error with another. The ODE is `u''=F(u)` where `u=[x,y]`. With `v=[vx,vy]` the first order system is `u'=v; v'=F(u)` which can not be de-coupled into an independent 2-dimensional system (at least not without invoking the Kepler laws). – Lutz Lehmann Dec 06 '18 at 11:30
  • Sir @LutzL, The edit 'correction' was not made based on your suggestion, I was merely trying to correct OP's implementation in the last loop. I couldn't understand what were you trying to point at in your first comment as I was just thinking about the code. I didn't pay attention to the physics behind the question. Your answer is much more insightful both in terms of the physics and implementation. It also points out the difference between the two numerical methods. – Cheesebread Dec 06 '18 at 12:24
  • Except that the last loop was correct as is, only not the method that was claimed in title and question. In principle your correction makes for a valid code, for a system `u'=v; v'=F(v);` like in ballistics with air friction. But even then the position integration is only order 1 correct. – Lutz Lehmann Dec 06 '18 at 13:06
  • Thank you, SamD97. I realize now that I wasn't using my RK functions at all...how silly. I implemented them for real this time and got them to work. – corgiworld Dec 06 '18 at 23:49