# Exercise: 2D orbital mechanics simulation (python)

Just a little disclaimer beforehand: I have never studied astronomy or any exact sciences for that matter (not even IT), so I am trying to fill this gap by self-education. Astronomy is one of the areas that has captured my attention and my idea of self-education is head on applied approach. So, straight to the point - this is orbital simulation model that I am casually working on when I have time/mood. My major goal is to create complete solar system in motion and ability to plan spacecraft launches to other planets.

You are all free to pick this project up at any point and have fun experimenting!

update!!! (Nov10)

• velocity is now proper deltaV and giving additional motion now calculates sum vector of velocity
• you can place as many static objects as you like, on every time unit object in motion checks for gravity vectors from all sources (and checks for collision)
• greatly improved the performance of calculations
• a fix to account for interactive mod in matplotlib. Looks like that this is default option only for ipython. Regular python3 requires that statement explicitly.

Basically it is now possible to "launch" a spacecraft from the surface of the Earth and plot a mission to the Moon by making deltaV vector corrections via giveMotion(). Next in line is trying to implement global time variable to enable simultaneous motion e.g. Moon orbits Earth while spacecraft tries out a gravity assist maneuver.

Comments and suggestions for improvements are always welcome!

Done in Python3 with matplotlib library

import matplotlib.pyplot as plt
import math
plt.ion()

G = 6.673e-11  # gravity constant
gridArea = [0, 200, 0, 200]  # margins of the coordinate grid
gridScale = 1000000  # 1 unit of grid equals 1000000m or 1000km

plt.clf()  # clear plot area
plt.axis(gridArea)  # create new coordinate grid
plt.grid(b="on")  # place grid

class Object:
_instances = []
def __init__(self, name, position, radius, mass):
self.name = name
self.position = position
self.mass = mass
self.placeObject()
self.velocity = 0
Object._instances.append(self)

def placeObject(self):
plt.show()

def giveMotion(self, deltaV, motionDirection, time):
if self.velocity != 0:
self.velocity = math.sqrt((x_comp**2)+(y_comp**2))

if x_comp > 0 and y_comp > 0:  # calculate degrees depending on the coordinate quadrant
self.motionDirection = math.degrees(math.asin(abs(x_comp)/self.velocity))  # update motion direction
elif x_comp > 0 and y_comp < 0:
self.motionDirection = math.degrees(math.asin(abs(y_comp)/self.velocity)) + 90
elif x_comp < 0 and y_comp < 0:
self.motionDirection = math.degrees(math.asin(abs(x_comp)/self.velocity)) + 180
else:
self.motionDirection = math.degrees(math.asin(abs(y_comp)/self.velocity)) + 270

else:
self.velocity = self.velocity + deltaV  # in m/s
self.motionDirection = motionDirection  # degrees
self.time = time  # in seconds
self.vectorUpdate()

def vectorUpdate(self):
self.placeObject()
data = []

for t in range(self.time):
motionForce = self.mass * self.velocity  # F = m * v
x_net = 0
y_net = 0
for x in [y for y in Object._instances if y is not self]:
distance = math.sqrt(((self.position[0]-x.position[0])**2) +
(self.position[1]-x.position[1])**2)
gravityForce = G*(self.mass * x.mass)/((distance*gridScale)**2)

x_pos = self.position[0] - x.position[0]
y_pos = self.position[1] - x.position[1]

if x_pos <= 0 and y_pos > 0:  # calculate degrees depending on the coordinate quadrant
gravityDirection = math.degrees(math.asin(abs(y_pos)/distance))+90

elif x_pos > 0 and y_pos >= 0:
gravityDirection = math.degrees(math.asin(abs(x_pos)/distance))+180

elif x_pos >= 0 and y_pos < 0:
gravityDirection = math.degrees(math.asin(abs(y_pos)/distance))+270

else:
gravityDirection = math.degrees(math.asin(abs(x_pos)/distance))

x_gF = gravityForce * math.sin(math.radians(gravityDirection))  # x component of vector
y_gF = gravityForce * math.cos(math.radians(gravityDirection))  # y component of vector

x_net += x_gF
y_net += y_gF

x_net += x_mF
y_net += y_mF
netForce = math.sqrt((x_net**2)+(y_net**2))

if x_net > 0 and y_net > 0:  # calculate degrees depending on the coordinate quadrant
self.motionDirection = math.degrees(math.asin(abs(x_net)/netForce))  # update motion direction
elif x_net > 0 and y_net < 0:
self.motionDirection = math.degrees(math.asin(abs(y_net)/netForce)) + 90
elif x_net < 0 and y_net < 0:
self.motionDirection = math.degrees(math.asin(abs(x_net)/netForce)) + 180
else:
self.motionDirection = math.degrees(math.asin(abs(y_net)/netForce)) + 270

self.velocity = netForce/self.mass  # update velocity
traveled = self.velocity/gridScale  # grid distance traveled per 1 sec
self.position[1] + math.cos(math.radians(self.motionDirection))*traveled)  # update pos
data.append([self.position[0], self.position[1]])

collision = 0
for x in [y for y in Object._instances if y is not self]:
if (self.position[0] - x.position[0])**2 + (self.position[1] - x.position[1])**2 <= x.radius**2:
collision = 1
break
if collision != 0:
print("Collision!")
break

plt.plot([x[0] for x in data], [x[1] for x in data])

Earth = Object(name="Earth", position=(50.0, 50.0), radius=6.371, mass=5.972e24)
Moon = Object(name="Moon", position=(100.0, 100.0), radius=1.737, mass = 7.347e22)  # position not to real scale
Craft = Object(name="SpaceCraft", position=(49.0, 40.0), radius=1, mass=1.0e4)

Craft.giveMotion(deltaV=8500.0, motionDirection=100, time=130000)
Craft.giveMotion(deltaV=2000.0, motionDirection=90, time=60000)
plt.show(block=True)


How it works

It all boils down to two things:

1. Creating object like Earth = Object(name="Earth", position=(50.0, 50.0), radius=6.371, mass=5.972e24) with parameters of position on grid (1 unit of grid is 1000km by default but this can be changed too), radius in grid units and mass in kg.
2. Giving object some deltaV such as Craft.giveMotion(deltaV=8500.0, motionDirection=100, time=130000) obviously it requires Craft = Object(...) to be created in the first place as mentioned in previous point. Parameters here are deltaV in m/s (note that for now acceleration is instantaneous), motionDirection is direction of deltaV in degrees (from current position imagine 360 degree circle around object, so direction is a point on that circle) and finally parameter time is how many seconds after the deltaV push trajectory of the object will be monitored. Subsequent giveMotion() start off from last position of previous giveMotion().

Questions:

1. Is this a valid algorithm to calculate orbits?
2. What are the obvious improvements to be made?
3. I have been considering "timeScale" variable that will optimize calculations, as it might not be necessary to recalculate vectors and positions for every second. Any thoughts on how it should be implemented or is it generally a good idea? (loss of accuracy vs improved performance)

Basically my aim is to start a discussion on the topic and see where it leads. And, if possible, learn (or even better - teach) something new and interesting.

Feel free to experiment!

Try using:

Earth = Object(name="Earth", position=(50.0, 100.0), radius=6.371, mass=5.972e24)
Moon = Object(name="Moon", position=(434.0, 100.0), radius=1.737, mass = 7.347e22)
Craft = Object(name="SpaceCraft", position=(43.0, 100.0), radius=1, mass=1.0e4)

Craft.giveMotion(deltaV=10575.0, motionDirection=180, time=322000)
Craft.giveMotion(deltaV=400.0, motionDirection=180, time=50000)


With two burns - one prograde at Earth orbit and one retrograde at Moon orbit I achieved stable Moon orbit. Are these close to theoreticaly expected values?

Suggested exercise: Try it in 3 burns - stable Earth orbit from Earth surface, prograde burn to reach Moon, retrograde burn to stabilize orbit around Moon. Then try to minimize deltaV.

Note: I plan to update the code with extensive comments for those not familiar with python3 syntax.

• A very good idea for self-educating! Would it be possible to summarise your formulae for those of us who are not familiar with Python syntax? – user2449 Oct 28 '14 at 20:35
• Sure, I guess. I will make more extensive comments in the code for those that want to pick it up and summarize the general logic in the question itself. – statespace Oct 28 '14 at 21:06
• Off the top of my head: consider using a vector for velocity instead of treating speed and direction differently. Where you say "F = m * v" do you mean "F = m*a"? You're assuming the Earth doesn't move because it's so much heavier than asteroid? Consider looking at github.com/barrycarter/bcapps/blob/master/bc-grav-sim.pl – barrycarter Oct 29 '14 at 0:44
• You can give motion to any object, including Earth. For test purposes I included only object -> Earth relation in main loop. It can easily be converted that every object relates to all other objects that are created. And every object can have their own motion vector. Reason why I didn't do it - very slow calculations even for 1 object. I hope that scaling time units should help alot but I am still not sure how to do it right. – statespace Oct 29 '14 at 5:50
• OK. A thought: do the simulation for two real objects (eg, Earth/Moon or Earth/Sun) and compare your results to ssd.jpl.nasa.gov/?horizons for accuracy? It won't be perfect because of perturbations by other sources, but it will give you some idea of accuracy? – barrycarter Oct 29 '14 at 16:49

Let the two bodies involved have masses $m_1, m_2$. Start with Newton's second law $$F = ma$$ where $a$ is acceleration. The gravitational force on body 2 from body 1 is given by

$$F_{21} = \frac{G m_1 m_2}{|r_{21}|^3}r_{21}$$

where $r_{21}$ is the relative position vector for the two bodies in question. The force on body 1 from body two is of course $F_{12}=-F_{21}$ since $r_{12}=-r_{21}$. If we denote the positions by $(x_1,y_1)$ and $(x_2,y_2)$, then

$$r_{21} = \begin{pmatrix} x_1-x_2 \\ y_1-y_2 \end{pmatrix}.$$

and

$$|r| = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}.$$ Then Newton's law $a=F/m$ becomes

\begin{align} x_1''(t) & = \frac{G m_2 (x_2-x_1)}{|r|^3} \\ y_1''(t) & = \frac{G m_2 (y_2-y_1)}{|r|^3} \\ x_2''(t) & = \frac{G m_1 (x_1-x_2)}{|r|^3} \\ y_2''(t) & = \frac{G m_1 (y_1-y_2)}{|r|^3}. \end{align}

Together with the initial positions and velocities, this system of ordinary differential equations (ODEs) comprises an initial value problem. The usual approach is to write this as a first-order system of 8 equations and apply a Runge-Kutta or multistep method to solve them.

If you apply something simple like forward Euler or backward Euler, you will see the Earth spiral out to infinity or in toward the sun, respectively, but that is an effect of the numerical errors. If you use a more accurate method, like the classical 4th-order Runge-Kutta method, you'll find that it stays close to a true orbit for a while but still eventually goes off to infinity. The right approach is to use a symplectic method, which will keep the Earth in the correct orbit -- although its phase will still be off due to numerical errors.

For the 2-body problem it's possible to derive a simpler system by basing your coordinate system around the center of mass. But I think the formulation above is clearer if this is new to you.

• This will take some time to digest. – statespace Oct 30 '14 at 20:31
• Still digesting. Too many unknown words for me but somehow I feel that at some point I will get there. For now my own algorithm is good enough for things to simply work. But when I will plug in simultaneous motion - I will be forced to get to the literature and read about proper algorithms. Given that limitations of modern hardware are much looser, I can afford to fool around with simple equations. Afraid not for long though. – statespace Nov 10 '14 at 20:17
• Indeed the symplectic methods are by far the most accurate but I think it is difficult for someone with no background in science to implement them. Instead you can use the very simple Euler method along with the Feynman correction. I don't think that you need something more complex than that for self-education purposes. – chrispap Nov 21 '14 at 10:43