HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Orbital Trajectory simulator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
simulatororbitaltrajectory

Problem

I have written a simple program to do trajectory simulation in the Earth-Moon system, it still has a long way to go I am working on making it more class oriented and am looking into implementing a better system than just the straight time step integration method.

I want to focus on having it more class-oriented. I am unsure where to draw a good line as to how much stuff should be class/function driven and how much should be in the main block. I am also looking for feedback on how to make it faster, as right now when I have the timestamp low it takes quite a while to run. In addition I am trying to find a good way to implement planets, as the way I have them now is not it, I could make a planet class, and initialize all the ones I am going to use, but I would rather just import/use them.

Here is some example output:

Console: (old output)
`Days remaining: 28.84, (96.14% left)
Tics per second: 49286 est time remaining: 00:00:50
Days remaining: 27.69, (92.28% left)
Tics per second: 49555 est time remaining: 00:00:48
Days remaining: 26.53, (88.43% left)
Tics per second: 49653 est time remaining: 00:00:46
Days remaining: 25.37, (84.57% left)
Tics per second: 49770 est time remaining: 00:00:44
Days remaining: 24.21, (80.71% left)
Tics per second: 49777 est time remaining: 00:00:42
Days remaining: 23.06, (76.85% left)
Tics per second: 49789 est time remaining: 00:00:40
Days remaining: 21.90, (72.99% left)
Tics per second: 49844 est time remaining: 00:00:37
Days remaining: 20.74, (69.14% left)
Tics per second: 49901 est time remaining: 00:00:35
Days remaining: 19.58, (65.28% left)
Tics per second: 49796 est time remaining: 00:00:33
Days remaining: 18.43, (61.42% left)
Tics per second: 49725 est time remaining: 00:00:32
Days remaining: 17.27, (57.56% left)
Tics per second: 49682 est time remaining: 00:00:30
Days remaining: 16.11, (53.70% left)
Tics per second: 49632 est time remaining: 00:00:28
Days remaining: 14.95, (49.85% left)
Tics per second: 49645 est time remainin

Solution

Let us start with some code review, before improving the performance, and finish off with a comparision against the original code.

Code review

As a gesture to us reviewing your code, it would have been nice to know which modules where external to a default installation, as I had to plunder a little to install the astropy and jplephem module. However I got thing up and running, and then I looked at code and found the following:

  • Naming is still not entirely snake_case – According to PEP8 the recommendation is to use snake_case, and not abbreviations. Your code breaks both of these, with names like pos, f, endTime, getRelPos(), and so on



  • Function should be named accordingly to what they do – Your function getRelPos() does not get anything, but it does calculate a new position. In other words, update_position(time) would be a better name, as it clearly conveys what is happening.



  • Avoid import's in functions – It's not considered good style to import modules within functions, like you do in plot(). It hides the dependencies, and can be time consuming if the function is called often.



  • Add vertical space – Both between functions, and within a function, you could add blank lines to help your code read better.



  • Add spaces around operators – Instead of self.f-=np.array(...), open it up using self.f -= np.array(...)



  • Use enough parenthesis in formulas – Use enough, but not too many. Some places you can rely on operator precedence to avoid a lot of parenthesis clogging up, like math.sqrt(xdist2 + ydist2 + zdist**2), there is no need to have the power expression in parenthesis.



  • Try to avoid costly functions in top-level of modules – In both earth.py and moon.py you do the SPK.open('de430.bsp') which is >100 MB file. This should be moved into main code, and the top level code of the modules should be within a initialization functions.



Performance issues

There are various issues related to performance which we can address:

  • Ideally simplify the de430.bsp – I don't know how this large file is handled, but besides having it open in just one place (at main), you should look into whether it can be trimmed down to something more relevant for you, and of smaller size. You only seem to use a small part of it, which could possibly be extracted somehow.



-
Do you really need numpy.arrays and numpy.longdouble? – I kind of question if you really need to use this, or if you could have benefitted from using ordinary arrays and standard ints. Especially when I see code where you use the gravity constant with only 3 decimals, 6.674, then using numpy.longdouble's seems inadequate and imprecise...

However, I've left them as is in my refactored code below, but it could be worthwhile to look into simplifications of this.

  • Simpler initialization of numpy arrays – A lot of places you reset your arrays to a triplet of numpy.longdouble zeros. This can easier, and faster, be done using np.zeros(3, dtype=np.longdouble)



  • Simpler multiplication with numpy arrays – Instead of using distinct variables, you multiply the entire vector immediately using self.pos += np.dot(self.vol, self.del_t)



-
Precalculate constant factors in heavy formulas – For example in update(self) you do (((self.f[0] / self.mass) * self.del_t)) / 1000 for each of the f's, when it suffices doing np.dot(self.f, self.VELOCITY_FACTOR), where self.VELOCITY_FACTOR = self.del_t / self.mass / 1000. This removes 2 of 3 multiplications, and that is a time saver.

I used kernprof to profile your code, and found that almost 90% of the time was divided on doing update() and force(), so therefore I spent some time simplifying these amongst others to reduce number of calculations executed.

  • If possibly, avoid math.sqrt – Taking the square root is rather expensive, and if not needed try avoiding it. (You might also here get a precision issue as the is uses normal precision here?). In your code you do d = math.sqrt(...) followed by g_com = (...) / d3. This can be simplifed to one operation of g_com = (...) / d1.5. Still expensive, but not as expensive as the combination of sqrt and **3.



-
Calculate and store only what is needed – In your code you execute the simulation loop 2592000 times with full calculation on the ship, and for each 10th you update the planets position. You store all planets positions, but you only store every 1000th position of the ship, and in addition you only plot every 10th of these again. This concludes to a lot of extraneous movement of the ship not to be used again.

This has an effect on the memory usage, and on execution time to calculate the positions. And in the end, you don't need it to plot the graph you want to plot. Similarily you update a non-moving earth, and store the positions of it, but in the end, you don't use the trajectory of the earth anyways. Waste of memory and time.

Refactored code

I started of simplify the earths.py and moon.py into

Code Snippets

import numpy as np

class Planet(object):

    def __init__(self, kernel, planet_id, relative_id, mass):
        if kernel is None:
            raise ValueError("Need a base kernel, like de430.bsp")

        self._planet_id = planet_id
        self._relative_id = relative_id
        self._kernel = kernel
        self.mass = mass
        self.position = np.zeros(3, dtype=np.longdouble)

        # Initialize position history
        self.trajectory = [[], [], []]


    def log_position(self):
        """Append current position to trajectory."""  

        if self._planet_id != self._relative_id:
            for i in xrange(3):
                self.trajectory[i].append(self.position[i])

    def update_position(self, time):
        """Update the position according to a related position and self movement."""

        if self._planet_id != self._relative_id:
            self.position = (  self._kernel[3, self._planet_id].compute(time) 
                             - self._kernel[3, self._relative_id].compute(time))
import math
import numpy as np

def profile(func):  return func

class Craft(object):

    def __init__(self, delta_t, x=0.0, y=0.0, z=0.0,
                v_x=0.0, v_y=0.0, v_z=0.0, mass=0):

        # Pos is in km
        self.position = np.array([np.longdouble(x),
                                  np.longdouble(y),
                                  np.longdouble(z)])

        # Velocity is in km/s
        self.velocity = np.array([np.longdouble(v_x),
                                  np.longdouble(v_y),
                                  np.longdouble(v_z)])
        # Mass is in kg
        self.mass = mass

        # Delta_t is in days
        self.delta_t = np.longdouble(delta_t) * 86400

        # Initialize trajectory history of posisitons
        self.trajectory = [[np.longdouble(x)],
                           [np.longdouble(y)],
                           [np.longdouble(z)]]

        # Initialize force array
        self.force = np.zeros(3, dtype=np.longdouble)


        # Define some class constants, relative to self.mass
        g_const = np.longdouble(0.00000000006674)
        self.FORCE_FACTOR  = g_const * self.mass / 1000000
        self.VELOCITY_FACTOR = self.delta_t / self.mass / 1000


    @profile
    def force_g(self, body): 
        """Updates the force the body has on this craft."""

        dist = self.position - body.position
        gravity_component = ( self.FORCE_FACTOR * body.mass / 
                              ((dist[0]**2 + dist[1]**2 + dist[2]**2)**1.5) )

        # Update forces array
        self.force -= np.dot(dist, gravity_component)


    @profile
    def update(self):
        """Steps up simulation based on force calculations."""
        # Step up velocity and position
        self.velocity += np.dot(self.force, self.VELOCITY_FACTOR)
        self.position += np.dot(self.velocity, self.delta_t)

        # Reset force profile
        self.force = np.zeros(3, dtype=np.longdouble)


    def log(self):
        for i in range(3):
            self.trajectory[i].append(self.position[i])
import time as t
import math
import sys
from jplephem.spk import SPK
import os
from astropy.time import Time
import numpy as np

#Custum objects live in sub directory
sys.path.append('./Objects/')

# Import Objects
from Objects import Craft
from planets import Planet

import matplotlib.pyplot as plt
# The following is needed for the projection='3d' to work in plot()
from mpl_toolkits.mplot3d import Axes3D


def profile(func): return func

# Generate output for plotting a sphere
def drawSphere(xCenter, yCenter, zCenter, r):
    # Draw sphere
    u, v = np.mgrid[0:2 * np.pi:20j, 0:np.pi:10j]
    x = np.cos(u) * np.sin(v)
    y = np.sin(u) * np.sin(v)
    z = np.cos(v)
    # Shift and scale sphere
    x = r * x + xCenter
    y = r * y + yCenter
    z = r * z + zCenter
    return (x, y, z)

def plot(ship,planets):
    """3d plots earth/moon/ship interaction"""

    # Initialize plot
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.set_xlabel('X Km')
    ax.set_ylabel('Y Km')
    ax.set_zlabel('Z Km')
    ax.set_xlim3d(-500000, 500000)
    ax.set_ylim3d(-500000, 500000)
    ax.set_zlim3d(-500000, 500000)

    ax.plot(xs=ship.trajectory[0][0::10],
            ys=ship.trajectory[1][0::10],
            zs=ship.trajectory[2][0::10],
            zdir='z', label='ys=0, zdir=z')

    # Plot planet trajectory
    for planet in planets:
        ax.plot(xs=planet.trajectory[0],
                ys=planet.trajectory[1],
                zs=planet.trajectory[2],
                zdir='z', label='ys=0, zdir=z')

    # Plot Earth (plot is moon position relative to earth)
    # also plotting to scale
    (xs, ys, zs) = drawSphere(0, 0, 0, 6367.4447)
    ax.plot_wireframe(xs, ys, zs, color="r")

    plt.show()



@profile
def run_simulation(start_time, end_time, step, ship, planets):
    """Runs orbital simulation given ship and planet objects as well as start/stop times"""

    # Calculate moon & planet update rate (1/10th as often as the craft)
    planet_step_rate = int(math.ceil(((end_time - start_time) / step)/100))

    # Initialize positions of planets
    for planet in planets:
        planet.update_position(start_time)
        planet.log_position()

    start = t.time()
    total_time = end_time - start_time
    # Total tics
    total_steps = int((end_time - start_time) / step)

    print("Days in simulation: {:6.2f}, number of steps: {}".format(
            total_time, total_steps))    

    for i, time in enumerate(np.arange(start_time, end_time, step)):

        # Every planet_step_rate update the position estmation
        if (i % planet_step_rate == 0):
            for planet in planets[1:]:
                planet.update_position(time)
                planet.log_position()

        # Update craft velocity and force vectors related to planets
        for planet in planets:
            ship.force_g(planet)

        # Update ship position according to sum of affecting forces
        ship.update()

        # Log the position of the 

Context

StackExchange Code Review Q#112260, answer score: 5

Revisions (0)

No revisions yet.