2

I am currently trying to get a two body problem to work, that I can then upgrade to more planets, but it is not working. It is outputting me impossible positions. Does anyone know what is causing that?

This is the code I use:

day = 60*60*24
# Constants
G = 6.67408e-11
dt = 0.1*day
au = 1.496e11
t = 0


class CelBody:

    def __init__(self, id, name, x0, y0, z0, vx0, vy0, vz0, mass, vector, ax0, ay0, az0, totalforcex, totalforcey, totalforcez):
        self.ax0 = ax0
        self.ay0 = ay0
        self.az0 = az0

        self.ax = self.ax0
        self.ay = self.ay0
        self.az = self.az0

        # Constants of nature
        # Universal constant of gravitation
        self.G = 6.67408e-11
        # Name of the body (string)
        self.id = id
        self.name = name
        # Initial position of the body (au)
        self.x0 = x0
        self.y0 = y0
        self.z0 = z0
        # Position (au). Set to initial value.
        self.x = self.x0
        self.y = self.y0
        self.z = self.z0
        # Initial velocity of the body (au/s)
        self.vx0 = vx0
        self.vy0 = vy0
        self.vz0 = vz0
        # Velocity (au/s). Set to initial value.
        self.vx = self.vx0
        self.vy = self.vy0
        self.vz = self.vz0
        # Mass of the body (kg)
        self.M = mass
        # Short name
        self.vector = vector

        self.totalforcex = totalforcex
        self.totalforcey = totalforcey
        self.totalforcez = totalforcez

# All Celestial Bodies

forcex = 0
forcey = 0
forcez = 0

Bodies = [
    CelBody(0, 'Sun', 1, 1, 1, 0, 0, 0, 1.989e30, 'sun', 0, 0, 0, 0, 0, 0),
    CelBody(1, 'Mercury', 1*au, 1, 1, 0, 29780, 0, 3.3e23, 'earth', 0, 0, 0, 0, 0, 0),
    ]

leftover_bin = []
templistx = []
templisty = []
templistz = []

for v in range(365242):
    for n in range(len(Bodies)):
        #Need to initialize the bodies

        planetinit = Bodies[n]

        for x in range(len(Bodies)):
            # Temporary lists and initial conditions
            planet = Bodies[x]

            if (planet == planetinit):
                pass

            else:
                rx = Bodies[x].x - Bodies[n].x
                ry = Bodies[x].y - Bodies[n].y
                rz = Bodies[x].z - Bodies[n].z

                r3 = (rx**2+ry**2+rz**2)**1.5
                gravconst = G*Bodies[n].M*Bodies[x].M
                fx = -gravconst*rx/r3
                fy = -gravconst*ry/r3
                fz = -gravconst*rz/r3


                # Make a temporary list of the total forces and then add them to get the resulting force
                templistx.append(fx)
                templisty.append(fy)
                templistz.append(fz)

        forcex = sum(templistx)
        forcey = sum(templisty)
        forcez = sum(templistz)
        templistx.clear()
        templisty.clear()
        templistz.clear()

        x = int(Bodies[n].x) + int(Bodies[n].vx) * dt
        y = int(Bodies[n].y) + int(Bodies[n].vx) * dt
        z = int(Bodies[n].z) + int(Bodies[n].vz) * dt

        Bodies[n].x = x
        Bodies[n].y = y
        Bodies[n].z = z

        vx = int(Bodies[n].vx) + forcex/int(Bodies[n].M)*dt
        vy = int(Bodies[n].vy) + forcey/int(Bodies[n].M)*dt
        vz = int(Bodies[n].vz) + forcez/int(Bodies[n].M)*dt

        Bodies[n].vx = vx
        Bodies[n].vy = vy
        Bodies[n].vz = vz

        t += dt




print(Bodies[0].name)
print(Bodies[0].x)
print(Bodies[0].y)
print(Bodies[0].z)


print(Bodies[1].name)
print(Bodies[1].x)
print(Bodies[1].y)
print(Bodies[1].z)

It should output something like the coordinates here, but then also a z coordinate: coordinate 1 (41.147123353981485, -2812171.2728945166) coordinate 2 (150013715707.77917, 2374319765.821534)

But it outputs the following:

Sun 0.0, 0.0, 0.0

Earth 149600000000.0, 0.0, 0.0

Note: The problem is probably in the for loops or in the rounding of the sum of the arrays but I am not sure.

Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
Ian Ronk
  • 55
  • 2
  • 16
  • Try breaking this down into functions with clear objectives. You have one massive code dump, but it looks like you have two bodies loops, and you are re-initializing inside one of them, and you are applying state changes to the body in the body loop (so you are applying future state of one body to past state of another body). You need to break it into clear functions so you can debug it and hand-check the numbers after ONE step (instead of the whole thing running). – Kenny Ostrom Dec 17 '18 at 14:57
  • It does not output what you say it does. – beauxq Dec 17 '18 at 15:19
  • What does it output then? – Ian Ronk Dec 17 '18 at 15:26
  • fundamentally, you need a fixed state which you use in all computations, then return the next state (at each time step). There may be other errors, but this is a common mistake. – Kenny Ostrom Dec 17 '18 at 15:41
  • You really need to post the correct answer (not just say this is wrong). What this outputs is Sun 1.0 1.0 1.0 Mercury 102089793491840.0 101940193491841.0 1.0 – Kenny Ostrom Dec 17 '18 at 15:50
  • also get rid of unused variables: vector, ax0, ay0, az0, totalforcex, totalforcey, totalforcez – Kenny Ostrom Dec 17 '18 at 19:07
  • Other posts on why it is a bad idea to use Euler to simulate planets (or any other system with conserved quantities): [https://stackoverflow.com/q/53645649/3088138](Cannot get RK4 to solve for position of orbiting body), [Initial value problem for a system of ODEs solver](https://stackoverflow.com/q/53692643/3088138). Always a good read: the ["Moving stars around" project](http://www.artcompsci.org/msa/web/vol_1/v1_web/v1_web.html). The next best thing you can do is to use the Verlet method as discussed [here](https://stackoverflow.com/q/53056901/3088138). – Lutz Lehmann Dec 17 '18 at 22:56

2 Answers2

3

picture - 1000 words

enter image description here

The direct errors in your code are

  • You compute the force in the wrong direction, it should be rx = b[n].x-b[x].x etc. or you need to remove the minus sign some lines later.

  • Your computation in single coordinates invites copy-paste errors as in

    x = int(Bodies[n].x) + int(Bodies[n].vx) * dt
    y = int(Bodies[n].y) + int(Bodies[n].vx) * dt
    z = int(Bodies[n].z) + int(Bodies[n].vz) * dt
    

    where in the y coordinate you still use vx. The intermediate rounding to integer values makes no sense, it only reduces the accuracy somewhat.


I changed your code to use numpy arrays as vectors, separated the acceleration computation from the Euler updates, removed the non-sensical rounding to integer values during the numerical simulation, removed unused variables and fields, removed intermediate variables for the force/acceleration computations to directly update the acceleration field, changed the loop to use the time to notice when a year (or 10) has passed (your code iterates over 100 years in 0.1day increments, was that intended?), ... and added Venus to the bodies and added code to produce images, result see above.

This spiraling is typical for the Euler method. You can easily improve that pattern by changing the Euler update to the symplectic Euler update, which means to update the velocity first and compute the position with the new velocity. This gives, with everything else the same, the image

enter image description here

day = 60*60*24
# Constants
G = 6.67408e-11
au = 1.496e11

class CelBody(object):
    # Constants of nature
    # Universal constant of gravitation
    def __init__(self, id, name, x0, v0, mass, color, lw):
        # Name of the body (string)
        self.id = id
        self.name = name
        # Mass of the body (kg)
        self.M = mass
        # Initial position of the body (au)
        self.x0 = np.asarray(x0, dtype=float)
        # Position (au). Set to initial value.
        self.x = self.x0.copy()
        # Initial velocity of the body (au/s)
        self.v0 = np.asarray(v0, dtype=float)
        # Velocity (au/s). Set to initial value.
        self.v = self.v0.copy()
        self.a = np.zeros([3], dtype=float)
        self.color = color
        self.lw = lw

# All Celestial Bodies

t = 0
dt = 0.1*day

Bodies = [
    CelBody(0, 'Sun', [0, 0, 0], [0, 0, 0], 1.989e30, 'yellow', 10),
    CelBody(1, 'Earth', [-1*au, 0, 0], [0, 29783, 0], 5.9742e24, 'blue', 3),
    CelBody(2, 'Venus', [0, 0.723 * au, 0], [ 35020, 0, 0], 4.8685e24, 'red', 2),
    ]

paths = [ [ b.x[:2].copy() ] for b in Bodies]

# loop over ten astronomical years
v = 0
while t < 10*365.242*day:
    # compute forces/accelerations
    for body in Bodies:
        body.a *= 0
        for other in Bodies:
            # no force on itself
            if (body == other): continue # jump to next loop
            rx = body.x - other.x
            r3 = sum(rx**2)**1.5
            body.a += -G*other.M*rx/r3

    for n, planet in enumerate(Bodies):
        # use the symplectic Euler method for better conservation of the constants of motion
        planet.v += planet.a*dt
        planet.x += planet.v*dt
        paths[n].append( planet.x[:2].copy() )
        #print("%10s x:%53s v:%53s"%(planet.name,planet.x, planet.v))
    if t > v:
        print("t=%f"%t)
        for b in Bodies: print("%10s %s"%(b.name,b.x))
        v += 30.5*day
    t += dt

plt.figure(figsize=(8,8))
for n, planet in enumerate(Bodies): 
    px, py=np.array(paths[n]).T; 
    plt.plot(px, py, color=planet.color, lw=planet.lw)
plt.show()
Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
1

I think the core of your problem is that you are not thinking of it as a state engine.

Imagine "Bodies" is a completely unchangable value that determines the state of the system at one point in time:

bodies_at_time_0 = ((sun, position, velocity, mass), (earth, position, velocity, mass))

You get the next state like so:

bodies_at_time_1 = apply_euler_method_for_one_tick( bodies_at_time_0 )

Thus your "Bodies" is completely fixed at one time, and you compute a whole new "Bodies" for the next time. Inside the computation you ALWAYS use the data in the input, which is where they are now. What you are doing is moving some things, and then computing where to move other things based on the wrong number (because you already moved other stuff).

Once you make sure your function uses the input state, and returns an output state, you can break it down much more easily:

# advance all bodies one time interval, using their frozen state 
def compute(bodies):
    new_bodies = []
    for body in bodies:
        new_bodies.append(compute_one_body(body, bodies))
    return new_bodies

# figure out where one body will move to, return its new state
def compute_one_body(start, bodies):
    end = math stuff using the fixed state in bodies
    return end

# MAIN
bodies = initial_state
for timepoint in whatever:
    bodies = compute(bodies)

I like to use tuples for this sort of thing, to avoid accidentally changing a list in some other scope (because lists are mutable).

Kenny Ostrom
  • 5,639
  • 2
  • 21
  • 30
  • And how would I add in the euler method with forces and how do I calculate the forces with this method? – Ian Ronk Dec 17 '18 at 16:20
  • I didn't see anything wrong with that part at first glance, so I assume they are right. It looked like you know the math, but not the code. – Kenny Ostrom Dec 17 '18 at 19:03
  • Yeah that is why I want to know how to implement this method, because I now have defined a function for calculating the force and I will create one for calculating the position per body and put this in the compute function, would you approve with that? – Ian Ronk Dec 17 '18 at 19:17
  • Yes, if you give them the right inputs. You can rename "compute" and "compute_one_body". I just named them that so we wouldn't be distracted by implementation details. They are actually supposed to be the euler method functions. The whole point of this answer is GENERATE THE NEXT STATE from the current state. Don't mix and match different planets from different times. – Kenny Ostrom Dec 17 '18 at 19:55
  • I am trying to put in my new code, I have tried to implement it, but I need to call up the planets and connect all of the functions with each other. I am not succeeding in editing the code to show my current code – Ian Ronk Dec 17 '18 at 20:04