patternpythonMinor
Orbital Trajectory simulator
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
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
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
Performance issues
There are various issues related to performance which we can address:
-
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,
However, I've left them as is in my refactored code below, but it could be worthwhile to look into simplifications of this.
-
Precalculate constant factors in heavy formulas – For example in
I used kernprof to profile your code, and found that almost 90% of the time was divided on doing
-
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
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 usesnake_case, and not abbreviations. Your code breaks both of these, with names likepos,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 usingself.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.pyandmoon.pyyou do theSPK.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 dod = math.sqrt(...)followed byg_com = (...) / d3. This can be simplifed to one operation ofg_com = (...) / d1.5. Still expensive, but not as expensive as the combination ofsqrtand**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 intoCode 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.