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.radius = radius # in grid values
    self.mass = mass
    self.placeObject()
    self.velocity = 0
    Object._instances.append(self)

    def placeObject(self):
    drawObject = plt.Circle(self.position, radius=self.radius, fill=False, color="black")
    plt.gca().add_patch(drawObject)
    plt.show()

    def giveMotion(self, deltaV, motionDirection, time):
    if self.velocity != 0:
    x_comp = math.sin(math.radians(self.motionDirection))*self.velocity
    y_comp = math.cos(math.radians(self.motionDirection))*self.velocity
    x_comp += math.sin(math.radians(motionDirection))*deltaV
    y_comp += math.cos(math.radians(motionDirection))*deltaV
    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_mF = motionForce * math.sin(math.radians(self.motionDirection))
    y_mF = motionForce * math.cos(math.radians(self.motionDirection))
    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 = (self.position[0] + math.sin(math.radians(self.motionDirection))*traveled,
    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?

    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.

    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 https://github.com/barrycarter/bcapps/blob/master/bc-grav-sim.pl

    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.

    OK. A thought: do the simulation for two real objects (eg, Earth/Moon or Earth/Sun) and compare your results to http://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?

    Awesome, thanks. That link alone was worth it to start the discussion. So, first step - I will try to re-write the code so that objects check all other objects for gravity force vector. By the way, most pressing issue of time scaling is still here. And it would be interesting to check how results improve or get worse by changing time scale parameter.

    Oh, and F=m*v is really that. Assumption is that by giving motion to the object first of all it goes to absolute halt and by that velocity equals to acceleration. Like instant speed from 0 -> whatever value, and goes on indefinitely with that speed if no other forces act upon the object.

  • 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 spirals inward (as long as the time step is small enough) or goes to infinity (if the time step is too large). 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.

    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.

    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.

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM