├── README.md ├── SolarSystemOrbiter ├── __init__.py ├── htm │ ├── interface.png │ ├── maths.png │ └── progress.png └── sso.py └── setup.py /README.md: -------------------------------------------------------------------------------- 1 | # SolarSystemOrbiter - 2.0 2 | Plot the orbits of the planets in our Solar System and calculate the [Hohmann Transfer Orbits](https://en.wikipedia.org/wiki/Hohmann_transfer_orbit) to transfer your rocket ship from one planet to the other and back. 3 | 4 | ![alt-tag](https://github.com/madoee/SolarSystemOrbiter/blob/master/SolarSystemOrbiter/htm/progress.png?raw=true) 5 | 6 | ### How to install 7 | Download the .zip or clone the repository, and you're done! 8 | The script is written in Python 3, but also Python 2 compatible. It requires the *numpy*, *matplotlib*, and *tkinter* packages. All these packages can be acquired with the pip installer 9 | 10 | `pip install *package*` 11 | 12 | or by executing the install script 13 | 14 | `[sudo] python3 setup.py install` 15 | 16 | ### How to use it 17 | 18 | The script is run using 19 | `python sso.py` 20 | 21 | This interface will then pop up: 22 | 23 | ![alt-tag](https://github.com/madoee/SolarSystemOrbiter/blob/master/SolarSystemOrbiter/htm/interface.png?raw=true) 24 | 25 | On the top you choose the planets you want to include in the simulation. If you want to calculate a Hohmann Transfer Orbit, choose the origin and destination in the lists below. 26 | 27 | For the transfer orbit to be plotted, you have to select the *Plot Hohmann Transfer Orbit* checkbox. Selecting the *Orbit Insertion* checkbox triggers a second acceleration as soon as you have reached your destination (defined by the sphere of influence of the destination planet). The spacecraft now follows the target planet's orbit. 28 | 29 | The *Timestep* and *Integration Time* textboxes set the simulation parameters. A finer (smaller) timestep will lead to more accurate results, while increasing the computation time. The *Suggest Simulation Parameters* button proposes values based on the planets included in the simulation and the parameters of the Hohmann Transfer orbit. 30 | 31 | The *Plot* button triggers the integration of the orbits of the planets and the transfer and opens a plot for you. The *Animate* button shows you the results of the simulation as they are calculated, with additional information superimposed. This animation can be saved to the local directory using the *Save as mp4* button. A file called *SolarSystemSimulation.mp4* is created. 32 | 33 | 34 | ### What it does 35 | The planet and transfer orbits are calculated using the [leap-frog integration scheme](https://en.wikipedia.org/wiki/Leapfrog_integration). Several assumptions are made: in-plane orbits of the planets and the rocket ship around the Sun, only the gravitational field of the Sun is regarded. 36 | The orbits of the planets reflect their actual distances and eccentricities. If a Hohmann Transfer Orbit is calculated, the script sets the eccentricity of destination and origin orbit to 0, as the varying speed of the planets on elliptical orbits messes up the trajectory calculation (will be implemented later). 37 | 38 | Here is the math behind the calculation of the HTM and an exemplary calculation for a Earth-Mars transfer: 39 | ![alt-tag](https://github.com/madoee/SolarSystemOrbiter/blob/master/SolarSystemOrbiter/htm/maths.png?raw=true) 40 | 41 | We disregard any perturbations due to third bodies, non-spherical inhomogeneous planets, solar radiation, ... 42 | In the end, all we do is accelerate our rocket ship at the start and see how its trajectory is influenced by the Sun's gravity field. 43 | 44 | ### To-Do 45 | * Increase number of integration steps only for GAM calculation in SoI 46 | * Include Planet IX 47 | * Include *One-Tangent Burns*. Compare to HTs 48 | * Combine HTs with phasing orbits 49 | * Use eccentric anomaly to calculate HTs for elliptical orbits (fixed orientation between origin and target planet) 50 | * Add real planet's positions for actual dates. Look for possible next date for HT / GAM between planets. 51 | * Long-term goal: Itineraries of Hohmann Transfers and GAMs. Comparison of energies and times required 52 | -------------------------------------------------------------------------------- /SolarSystemOrbiter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmahlke/SolarSystemOrbiter/ff0243eb6cecb62cdeea6bcb9182279a14bcdf27/SolarSystemOrbiter/__init__.py -------------------------------------------------------------------------------- /SolarSystemOrbiter/htm/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmahlke/SolarSystemOrbiter/ff0243eb6cecb62cdeea6bcb9182279a14bcdf27/SolarSystemOrbiter/htm/interface.png -------------------------------------------------------------------------------- /SolarSystemOrbiter/htm/maths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmahlke/SolarSystemOrbiter/ff0243eb6cecb62cdeea6bcb9182279a14bcdf27/SolarSystemOrbiter/htm/maths.png -------------------------------------------------------------------------------- /SolarSystemOrbiter/htm/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmahlke/SolarSystemOrbiter/ff0243eb6cecb62cdeea6bcb9182279a14bcdf27/SolarSystemOrbiter/htm/progress.png -------------------------------------------------------------------------------- /SolarSystemOrbiter/sso.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import matplotlib 3 | matplotlib.use('TkAgg') # This backend is required for the app to run on macOS 4 | import matplotlib.animation as animation 5 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | 9 | try: # Python 3 10 | import tkinter as tk 11 | from tkinter import ttk 12 | from _tkinter import TclError 13 | except ImportError: # Python 2 14 | import Tkinter as tk 15 | from _tkinter import TclError 16 | import ttk 17 | 18 | 19 | ''' 20 | Defines the Simulation Class and the 21 | Application Window with GUI and plotting canvas 22 | ''' 23 | 24 | 25 | class Planet(): 26 | ''' 27 | The planet class contains the physics, the orbital motion of the planet around the central body 28 | ''' 29 | 30 | def __init__(self, a, T, e, m, sim, c): 31 | 32 | # Physical properties 33 | self.semiMajor = a 34 | self.orbitalPeriod = T 35 | self.eccentricity = e 36 | self.mass = m * M * 1e-6 # m is mass in solar mass units 37 | 38 | # Calculate radius of region where planet's gravitational influence on spacecraft 39 | # dominates all other influences 40 | self.sphereOfInfluence = self.semiMajor * np.power(self.mass / M, 2/5) 41 | 42 | # Included in simulation 43 | self.simulate = sim 44 | self.colour = c 45 | self.linestyle = '-' 46 | self.marker = 'o' 47 | 48 | # Simulation quantities 49 | self.eccentricAnomaly = None # later angle in radians 50 | self.position = None # later given as (x, y) coordinate 51 | self.velocity = None # later given as (vx, vy) components 52 | 53 | 54 | def distanceToSun(self): 55 | x, y = self.position 56 | return np.sqrt(x**2 + y**2) 57 | 58 | def getVelocity(self): 59 | x, y = self.velocity 60 | return np.sqrt(x**2 + y**2) 61 | 62 | 63 | def calculateInitialPosition(self): 64 | # Use distance and anomaly to calculate initial positions x0, y0 65 | distance = self.semiMajor * (1. - self.eccentricity) 66 | return (distance*np.sin(self.eccentricAnomaly), -distance*np.cos(self.eccentricAnomaly)) 67 | 68 | 69 | def calculateInitialVelocity(self, delta): 70 | # Calculating the x- and y-components requires the specific timestep of the simulation 71 | # Taylor approximation of velocities at step n=1/2 72 | velocity = np.sqrt(G * M * (2./self.distanceToSun() - 1./self.semiMajor)) 73 | return (velocity * np.cos(self.eccentricAnomaly) - G * M * self.position[0]/self.distanceToSun()**3 * delta/2, 74 | velocity * np.sin(self.eccentricAnomaly) - G * M * self.position[1]/self.distanceToSun()**3 * delta/2) 75 | 76 | def updatePosition(self, delta): 77 | # Return tuple with updated position 78 | return tuple(i + delta * vi for i, vi in zip(self.position, self.velocity)) # for i in x, y 79 | 80 | def updateVelocity(self, delta): 81 | # Return tuple with updated velocity, using leap-frog integration scheme 82 | return tuple(i - (delta * G * M * ri) / self.distanceToSun()**3 for i, ri in zip(self.velocity, self.position)) 83 | 84 | 85 | 86 | class SolarSystemSimulation(): 87 | ''' 88 | This class handles the simulation parameters 89 | ''' 90 | 91 | Planets = {} 92 | 93 | def __init__(self): 94 | 95 | self.delta = gui.stepsize.get() * 2*np.pi / (np.sqrt(G*M) * 365.256) # integration timestep in Earth days 96 | self.steps = round(365.256 * gui.duration.get() / gui.stepsize.get()) # integration time in Earth years 97 | 98 | self.originPlanet = '' # HTO Planets 99 | self.destinationPlanet = '' 100 | 101 | # ------ 102 | # Initialize Planets 103 | # Units of AU and Year 104 | Mercury = Planet(a=0.387, e=0.205, T=0.241, m=0.166014, sim=gui.mercury.get(), c ='Gold') 105 | Venus = Planet(a=0.723, e=0.007, T=0.615, m=2.08106272, sim=gui.venus.get(), c ='Coral') 106 | Earth = Planet(a=1., e=0.017, T=1., m=3.003486962, sim=gui.earth.get(), c ='DarkBlue') 107 | Mars = Planet(a=1.524, e=0.094, T=1.88, m=0.3232371722, sim=gui.mars.get(), c ='Crimson') 108 | Jupiter = Planet(a=5.203, e=0.049, T=11.9, m=954.7919, sim=gui.jupiter.get(), c ='orange') 109 | Saturn = Planet(a=9.58, e=0.057, T=29.5, m=285.885670, sim=gui.saturn.get(), c ='Khaki') 110 | Uranus = Planet(a=19.20, e=0.046, T=84, m=43.66244, sim=gui.uranus.get(), c ='Turquoise') 111 | Neptune = Planet(a=30.06, e=0.011, T=164.8, m=51.51384, sim=gui.neptune.get(), c ='RoyalBlue') 112 | 113 | # For easier interaction between tkinter and class code, place the planet instances in a dict 114 | SolarSystemSimulation.Planets = { 115 | 'Mercury': Mercury, 116 | 'Venus': Venus, 117 | 'Earth': Earth, 118 | 'Mars': Mars, 119 | 'Jupiter': Jupiter, 120 | 'Saturn': Saturn, 121 | 'Uranus': Uranus, 122 | 'Neptune': Neptune 123 | } 124 | 125 | # ------ 126 | # Hohmann Transfer Orbit Parameters 127 | if gui.plotHohmann.get(): 128 | # User has to have selected origin and destination of Hohmann Transfer. If not, 129 | # this function just returns a message and does nothing 130 | try: 131 | self.originPlanet, self.destinationPlanet = gui.origin.get(gui.origin.curselection()), gui.destination.get(gui.destination.curselection()) 132 | except TclError: 133 | print('\n ! You have to select origin and destination planet of the Hohmann Transfer Orbit ! \n') 134 | return None 135 | 136 | # ------ 137 | # If planet is in simulation, determine its initial position and velocity 138 | for key, planet in SolarSystemSimulation.Planets.copy().items(): 139 | if planet.simulate or key in [self.originPlanet, self.destinationPlanet]: 140 | # Randomly place the planets on their respective orbits 141 | planet.eccentricAnomaly = np.random.uniform(0, 2*np.pi) 142 | 143 | planet.position = planet.calculateInitialPosition() 144 | planet.velocity = planet.calculateInitialVelocity(self.delta) 145 | 146 | else: # if planets are not part of the simulation, remove them 147 | SolarSystemSimulation.Planets.pop(key) 148 | 149 | 150 | def timeStep(self): 151 | # Update planet position and velocity after one timestep 152 | for planet in SolarSystemSimulation.Planets.values(): 153 | planet.position = planet.updatePosition(self.delta) 154 | planet.velocity = planet.updateVelocity(self.delta) 155 | 156 | 157 | class Spacecraft(Planet): 158 | 159 | def __init__(self, origin, destination, delta): 160 | 161 | self.origin = origin # Instance of Planet Class 162 | self.destination = destination # Instance of Planet Class 163 | 164 | self.position = origin.position 165 | self.eccentricAnomaly = origin.eccentricAnomaly 166 | self.velocity = self.calculateInitialVelocity(delta) 167 | 168 | self.colour = 'Maroon' 169 | self.linestyle = '--' 170 | self.marker = 'd' 171 | self.simulate = False # This is to exclude HTO from plot limits check 172 | 173 | self.orbitInserted = False 174 | 175 | def calculateInitialVelocity(self, delta): 176 | # Initial velocity is defined by the distance the spacecraft has to travel 177 | dO = self.origin.semiMajor * (1 - self.origin.eccentricity) # Leave at perihelion 178 | dD = self.destination.semiMajor # Arrive at aphelion 179 | 180 | # The velocity of the spacecraft is defined by the HTO parameters 181 | velocity = np.sqrt(G*M/dO) + np.sqrt(G*M/dO) * (np.sqrt(2*dD / (dD + dO)) - 1) 182 | return (velocity*np.cos(self.eccentricAnomaly) - G*M*self.origin.position[0]/self.origin.distanceToSun()**3 * delta/2, 183 | velocity*np.sin(self.eccentricAnomaly) - G*M*self.origin.position[1]/self.origin.distanceToSun()**3 * delta/2) 184 | 185 | def performOrbitInsertion(self, Sim): 186 | if not Sim.Planets['HTO'].orbitInserted: 187 | # Adjusts the spacecraft's velocity if it has not been done yet 188 | semiMajorOrigin = Sim.Planets[Sim.originPlanet].semiMajor 189 | semiMajorDestination = Sim.Planets[Sim.destinationPlanet].semiMajor 190 | 191 | requiredAcceleration = np.sqrt(G*M/semiMajorDestination) * (1 - np.sqrt(2 * semiMajorOrigin / (semiMajorOrigin + semiMajorDestination))) # x-component 192 | # Acceleration is split up into x- and y-component 193 | Sim.Planets['HTO'].velocity = (Sim.Planets['HTO'].velocity[0] + requiredAcceleration*(Sim.Planets['HTO'].velocity[0]/Sim.Planets['HTO'].getVelocity()), 194 | Sim.Planets['HTO'].velocity[1] + requiredAcceleration*(Sim.Planets['HTO'].velocity[1]/Sim.Planets['HTO'].getVelocity())) 195 | Sim.Planets['HTO'].orbitInserted = True 196 | 197 | class App: 198 | # ------ 199 | # Set up control window 200 | 201 | def __init__(self, master): 202 | self.master = master 203 | 204 | # ------ 205 | # Top left: Quit button 206 | self.quit = ttk.Button(master, text='Quit', command=quit) 207 | self.quit.grid(column=0, row=0, sticky='EW') 208 | 209 | # ------ 210 | # Upper Panel: Select Planets to Plot 211 | self.planet_frame = ttk.LabelFrame(master, text='Choose Planets to Plot') 212 | self.planet_frame.grid(row=1, columnspan=5, sticky='EW') 213 | 214 | # Planets 215 | self.mercury = tk.BooleanVar() 216 | self.venus = tk.BooleanVar() 217 | self.earth = tk.BooleanVar() 218 | self.mars = tk.BooleanVar() 219 | self.jupiter = tk.BooleanVar() 220 | self.saturn = tk.BooleanVar() 221 | self.uranus = tk.BooleanVar() 222 | self.neptune = tk.BooleanVar() 223 | 224 | self.plot_mercury = ttk.Checkbutton(self.planet_frame, text='Mercury', variable=self.mercury) 225 | self.plot_mercury.grid(row=1, column=0, sticky='EW', padx=5, pady=5) 226 | self.plot_venus = ttk.Checkbutton(self.planet_frame, text='Venus', variable=self.venus) 227 | self.plot_venus.grid(row=2, column=0, sticky='EW', padx=5, pady=5) 228 | self.plot_earth = ttk.Checkbutton(self.planet_frame, text='Earth', variable=self.earth) 229 | self.plot_earth.grid(row=3, column=0, sticky='EW', padx=5, pady=5) 230 | self.plot_mars = ttk.Checkbutton(self.planet_frame, text='Mars', variable=self.mars) 231 | self.plot_mars.grid(row=4, column=0, sticky='EW', padx=5, pady=5) 232 | self.plot_jupiter = ttk.Checkbutton(self.planet_frame, text='Jupiter', variable=self.jupiter) 233 | self.plot_jupiter.grid(row=1, column=1, sticky='EW', padx=5, pady=5) 234 | self.plot_saturn = ttk.Checkbutton(self.planet_frame, text='Saturn', variable=self.saturn) 235 | self.plot_saturn.grid(row=2, column=1, sticky='EW', padx=5, pady=5) 236 | self.plot_uranus = ttk.Checkbutton(self.planet_frame, text='Uranus', variable=self.uranus) 237 | self.plot_uranus.grid(row=3, column=1, sticky='EW', padx=5, pady=5) 238 | self.plot_neptune = ttk.Checkbutton(self.planet_frame, text='Neptune', variable=self.neptune) 239 | self.plot_neptune.grid(row=4, column=1, sticky='EW', padx=5, pady=5) 240 | 241 | # ------ 242 | # Lower Panel: Hohmann Transfer Orbit selection 243 | self.transfer_frame = ttk.LabelFrame(master, text='Choose Origin and Destination') 244 | self.transfer_frame.grid(row=5, columnspan=5, sticky='EW') 245 | 246 | self.origin = tk.Listbox(self.transfer_frame, exportselection=0) 247 | self.origin.grid(row=6, column=0, sticky='EW') 248 | for planet in planets: 249 | self.origin.insert(0, planet) 250 | 251 | self.destination = tk.Listbox(self.transfer_frame, exportselection=0) 252 | self.destination.grid(row=6, column=2, sticky='EW') 253 | for planet in planets: 254 | self.destination.insert(0, planet) 255 | 256 | self.plotHohmann = tk.BooleanVar() 257 | self.orbitInsertion = tk.BooleanVar() 258 | 259 | self.plotHohmann_check = ttk.Checkbutton(self.transfer_frame, text='Plot Hohmann Transfer Orbit', variable=self.plotHohmann) 260 | self.plotHohmann_check.grid(row=7, column=0, sticky='EW', padx=5, pady=5) 261 | 262 | self.orbitInsertion_check = ttk.Checkbutton(self.transfer_frame, text='Orbit Insertion', variable=self.orbitInsertion) 263 | self.orbitInsertion_check.grid(row=7, column=2, sticky='EW', padx=5, pady=5) 264 | 265 | self.suggestionButton = ttk.Button(master, text='Suggest Simulation Parameters', command=self.suggestSimParameters) 266 | self.suggestionButton.grid(column=1, row=9, sticky='EW') 267 | 268 | 269 | # ------ 270 | # Other settings 271 | 272 | self.stepsize = tk.DoubleVar() # Integration timestep in Earth days 273 | tk.Entry(self.transfer_frame, textvariable=self.stepsize).grid(row=10, column=0, sticky='EW') 274 | tk.Label(self.transfer_frame, text='Timestep in Earth days').grid(row=10, column=2, sticky='E') 275 | 276 | self.duration = tk.DoubleVar() 277 | tk.Entry(self.transfer_frame, textvariable=self.duration).grid(row=11, column=0, sticky='EW') 278 | tk.Label(self.transfer_frame, text='Integration Time in Earth Years').grid(row=11, column=2, sticky='E') 279 | 280 | self.plot_button = ttk.Button(master, text='Plot', command=self.plot) 281 | self.plot_button.grid(column=1, row=12, sticky='EW') 282 | 283 | self.animate_button = ttk.Button(master, text='Animate', command=self.simulation_animation) 284 | self.animate_button.grid(column=2, row=12, sticky='EW') 285 | 286 | self.movie_button = ttk.Button(master, text='Save as mp4', command=self.movie) 287 | self.movie_button.grid(column=2, row=13, sticky='EW') 288 | 289 | # ------ 290 | # Set-up initial scenario for easy use 291 | self.plotHohmann.set(True) 292 | self.orbitInsertion.set(True) 293 | self.mercury.set(True) 294 | self.venus.set(True) 295 | self.earth.set(True) 296 | self.mars.set(True) 297 | self.jupiter.set(True) 298 | self.origin.select_set(2) 299 | self.destination.select_set(3) 300 | self.duration.set(0.72) 301 | self.stepsize.set(1.0) 302 | 303 | 304 | def plot(self): 305 | # ------ 306 | # Start simulation 307 | Sim = SolarSystemSimulation() 308 | 309 | # ------ 310 | # Create new window to plot the simulation 311 | sim_window = tk.Toplevel() 312 | sim_window.title('SolarSystemSimulation') 313 | sim_window.focus_force() # Auto-focus on window 314 | 315 | # Add interrupt button 316 | killswitch = ttk.Button(sim_window, text="Close Simulation", command=sim_window.destroy) 317 | killswitch.pack() 318 | 319 | fig, ax, elapsedTime = self.setupPlot(Sim) 320 | 321 | # Check if HTO is plotted 322 | if gui.plotHohmann.get(): 323 | Sim, travelTime, transferDistanceAU = self.hohmannTransfer(Sim) 324 | 325 | # ------ 326 | # Add Information to Plot 327 | info = self.addInformation(ax) 328 | info['elapsedTime'].set_text('Elapsed Time: {:.2f} Earth years'.format(gui.duration.get())) 329 | 330 | if gui.plotHohmann.get(): 331 | info['originDestination'].set_text('%s \u2192 %s' % (Sim.originPlanet, Sim.destinationPlanet)) 332 | info['transferTime'].set_text('Transfer Time: %.2f Earth Years' % travelTime) 333 | info['transferDistance'].set_text('Transfer Distance: %.1f AU' % transferDistanceAU) 334 | 335 | # ------ 336 | # Create plotting canvas 337 | canvas = FigureCanvasTkAgg(fig, sim_window) 338 | canvas.show() 339 | canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) 340 | 341 | # Add matplotlib toolbar to the plot 342 | toolbar = NavigationToolbar2TkAgg(canvas, sim_window) 343 | toolbar.update() 344 | canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) 345 | 346 | # ------ 347 | # Simulate and plot 348 | 349 | # For each planet in simulation, calculate trajectories 350 | planetTrajectories = {} 351 | for key, planet in Sim.Planets.items(): 352 | planetTrajectories[key] = [planet.position] 353 | 354 | sphereOfInfluence = Sim.Planets[Sim.destinationPlanet].sphereOfInfluence # in AU 355 | 356 | for i in range(Sim.steps): 357 | Sim.timeStep() 358 | for key, planet in Sim.Planets.items(): 359 | planetTrajectories[key].append(planet.position) 360 | 361 | # If orbit insertion is performed, check if spacecraft has arrived at planet 362 | if gui.plotHohmann.get(): 363 | if gui.orbitInsertion.get(): 364 | distanceToDestination = np.sqrt((Sim.Planets['HTO'].position[0]-Sim.Planets[Sim.destinationPlanet].position[0])**2 + 365 | (Sim.Planets['HTO'].position[1]-Sim.Planets[Sim.destinationPlanet].position[1])**2) 366 | 367 | if distanceToDestination < sphereOfInfluence: 368 | Sim.Planets['HTO'].performOrbitInsertion(Sim) 369 | 370 | # Plot final trajectories 371 | for key, planet in Sim.Planets.items(): 372 | ax.plot(*zip(*planetTrajectories[key]), ls=planet.linestyle, c=planet.colour, lw=0.5) 373 | ax.plot(*planetTrajectories[key][-1], marker=planet.marker, c=planet.colour) 374 | 375 | # Add Sun 376 | sun, = ax.plot((0, 0), c='orange', marker='o', ls='') 377 | 378 | canvas.draw() 379 | 380 | 381 | def simulation_animation(self): 382 | # ------ 383 | # Start simulation 384 | Sim = SolarSystemSimulation() 385 | 386 | # ------ 387 | # Create new window to plot the simulation 388 | sim_window = tk.Toplevel() 389 | sim_window.title('SolarSystemSimulation') 390 | sim_window.focus_force() # Auto-focus on window 391 | 392 | # Check if HTO is plotted 393 | if gui.plotHohmann.get(): 394 | Sim, travelTime, transferDistanceAU = self.hohmannTransfer(Sim) 395 | 396 | fig, ax, elapsedTime = self.setupPlot(Sim) 397 | 398 | # ------ 399 | # Create plotting canvas 400 | canvas = FigureCanvasTkAgg(fig, sim_window) 401 | canvas.show() 402 | canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) 403 | 404 | # Add interrupt button 405 | killswitch = ttk.Button(sim_window, text="Close Simulation", command=sim_window.destroy) 406 | killswitch.pack() 407 | 408 | info = self.addInformation(ax) 409 | 410 | # For each planet in simulation, add an empty line and store the instance in dict 411 | planetTrajectories = {} 412 | for key, planet in Sim.Planets.items(): 413 | line, = ax.plot([], [], ls=planet.linestyle, c=planet.colour, lw=0.5) 414 | sphere, = ax.plot([], [], marker=planet.marker, c=planet.colour) 415 | planetTrajectories[key] = (line, sphere) 416 | 417 | # Add Sun 418 | sun, = ax.plot([], [], c='orange', marker='o', ls='') 419 | 420 | def init(): 421 | # Initialize the animation 422 | elapsedTime.set_text('') 423 | 424 | # If HTO is plotted, include information 425 | if gui.plotHohmann.get(): 426 | info['originDestination'].set_text('%s \u2192 %s' % (Sim.originPlanet, Sim.destinationPlanet)) 427 | info['transferTime'].set_text('Transfer Time: %.2f Earth Years' % travelTime) 428 | info['transferDistance'].set_text('Transfer Distance: %.1f AU' % transferDistanceAU) 429 | info['destinationDistance'].set_text('Distance to Destination:') 430 | info['transferStatus'].set_text('Transfer Status: in progress..') 431 | 432 | trajectories = [] 433 | for line, sphere in planetTrajectories.values(): 434 | line.set_data([], []) 435 | trajectories.append(line) 436 | sphere.set_data([], []) 437 | trajectories.append(sphere) 438 | 439 | sun.set_data([], []) 440 | trajectories.append(sun) 441 | 442 | return trajectories 443 | 444 | def animate(i): 445 | # Animate the simulation 446 | 447 | # ------ 448 | # Update the planet positions 449 | Sim.timeStep() 450 | 451 | # Update the planet trajectories 452 | trajectories = [] # lines in plot 453 | for key, planet in Sim.Planets.items(): 454 | # Append new position to line of trajectory 455 | planetTrajectories[key][0].set_data(tuple(x + [xi] for x, xi in zip(planetTrajectories[key][0].get_data(), planet.position))) 456 | trajectories.append(planetTrajectories[key][0]) 457 | 458 | # Move planet sphere to current trajectory 459 | planetTrajectories[key][1].set_data(planet.position) 460 | trajectories.append(planetTrajectories[key][1]) 461 | 462 | sun.set_data([0], [0]) 463 | trajectories.append(sun) 464 | 465 | elapsedTimeNumber = i * gui.stepsize.get() / 365.256 466 | info['elapsedTime'].set_text('Elapsed Time: {:.2f} Earth Years'.format(elapsedTimeNumber)) 467 | trajectories.append(info['elapsedTime']) 468 | 469 | # HTO updates 470 | if gui.plotHohmann.get(): 471 | 472 | xSpacecraft, ySpacecraft = Sim.Planets['HTO'].position 473 | 474 | distancesToSpacecraft = {} 475 | for key, planet in Sim.Planets.items(): 476 | if key != 'HTO': 477 | distancesToSpacecraft[key] = np.sqrt((xSpacecraft-planet.position[0])**2 + 478 | (ySpacecraft-planet.position[1])**2) 479 | 480 | # Give distance to target planet 481 | info['destinationDistance'].set_text('Distance to Destination: %.1fAU' % distancesToSpacecraft[Sim.destinationPlanet]) 482 | trajectories.append(info['destinationDistance']) 483 | 484 | # See if spacecraft has arrived in sphere of influence of planet 485 | sphereOfInfluence = Sim.Planets[Sim.destinationPlanet].sphereOfInfluence # in AU 486 | if distancesToSpacecraft[Sim.destinationPlanet] <= sphereOfInfluence: 487 | # The HTO succeeded 488 | info['transferStatus'].set_text('Transfer Status: \u2714 completed') 489 | trajectories.append(info['transferStatus']) 490 | # If orbit insertion is true and has not yet been performed, perform second acceleration 491 | if gui.orbitInsertion.get(): 492 | Sim.Planets['HTO'].performOrbitInsertion(Sim) 493 | 494 | return trajectories 495 | 496 | ani = animation.FuncAnimation(fig, animate, frames=Sim.steps, interval=0, blit=True, init_func=init, repeat=False) 497 | canvas.draw() 498 | 499 | 500 | def movie(self): 501 | # ------ 502 | # Start simulation 503 | Sim = SolarSystemSimulation() 504 | 505 | fig, ax, elapsedTime = self.setupPlot(Sim) 506 | 507 | # ------ 508 | # Hohmann Transfer Orbit 509 | 510 | # Check if HTO is plotted 511 | if gui.plotHohmann.get(): 512 | Sim, travelTime, transferDistanceAU = self.hohmannTransfer(Sim) 513 | 514 | # ------ 515 | # Add Information to Plot 516 | info = self.addInformation(ax) 517 | 518 | # For each planet in simulation, add an empty line and store the instance in dict 519 | planetTrajectories = {} 520 | for key, planet in Sim.Planets.items(): 521 | line, = ax.plot([], [], ls=planet.linestyle, c=planet.colour, lw=0.5) 522 | sphere, = ax.plot([], [], marker=planet.marker, c=planet.colour) 523 | planetTrajectories[key] = (line, sphere) 524 | 525 | # Add Sun 526 | sun, = ax.plot([], [], c='orange', marker='o', ls='') 527 | 528 | def init(): 529 | # Initialize the animation 530 | trajectories = [] 531 | for line, sphere in planetTrajectories.values(): 532 | line.set_data([], []) 533 | trajectories.append(line) 534 | sphere.set_data([], []) 535 | trajectories.append(sphere) 536 | elapsedTime.set_text('') 537 | 538 | sun.set_data([], []) 539 | trajectories.append(sun) 540 | 541 | # If HTO is plotted, include information 542 | if gui.plotHohmann.get(): 543 | info['originDestination'].set_text('%s \u2192 %s' % (Sim.originPlanet, Sim.destinationPlanet)) 544 | info['transferTime'].set_text('Transfer Time: %.2f Earth Years' % travelTime) 545 | info['transferDistance'].set_text('Transfer Distance: %.1f AU' % transferDistanceAU) 546 | info['destinationDistance'].set_text('Distance to Destination:') 547 | info['transferStatus'].set_text('Transfer Status: in progress..') 548 | 549 | return trajectories 550 | 551 | def animate(i): 552 | # Animate the simulation 553 | 554 | # ------ 555 | # Update the planet positions 556 | Sim.timeStep() 557 | 558 | # Update the planet trajectories 559 | trajectories = [] # lines in plot 560 | for key, planet in Sim.Planets.items(): 561 | # Append new position to line of trajectory 562 | planetTrajectories[key][0].set_data(tuple(x + [xi] for x, xi in zip(planetTrajectories[key][0].get_data(), planet.position))) 563 | trajectories.append(planetTrajectories[key][0]) 564 | 565 | # Move planet sphere to current trajectory 566 | planetTrajectories[key][1].set_data(planet.position) 567 | trajectories.append(planetTrajectories[key][1]) 568 | 569 | sun.set_data([0], [0]) 570 | trajectories.append(sun) 571 | 572 | elapsedTimeNumber = i * gui.stepsize.get() / 365.256 573 | info['elapsedTime'].set_text('Elapsed Time: {:.2f} Earth years'.format(elapsedTimeNumber)) 574 | trajectories.append(info['elapsedTime']) 575 | 576 | # HTO updates 577 | if gui.plotHohmann.get(): 578 | # Find planet closest to spacecraft 579 | xSpacecraft, ySpacecraft = Sim.Planets['HTO'].position 580 | 581 | distancesToSpacecraft = {} 582 | 583 | for key, planet in Sim.Planets.items(): 584 | if key != 'HTO': 585 | distancesToSpacecraft[key] = np.sqrt((xSpacecraft-planet.position[0])**2 + 586 | (ySpacecraft-planet.position[1])**2) 587 | #closestPlanetText = min(distancesToSpacecraft, key=distancesToSpacecraft.get) 588 | #closestPlanet.set_text('Closest Planet: %s' % closestPlanetText) 589 | 590 | # Give distance to target planet 591 | info['destinationDistance'].set_text('Distance to Destination: %.1fAU' % distancesToSpacecraft[Sim.destinationPlanet]) 592 | 593 | #trajectories.append(closestPlanet) 594 | trajectories.append(info['destinationDistance']) 595 | 596 | # See if spacecraft has arrived in sphere of influence of planet 597 | sphereOfInfluence = Sim.Planets[Sim.destinationPlanet].sphereOfInfluence # in AU 598 | 599 | if distancesToSpacecraft[Sim.destinationPlanet] <= sphereOfInfluence: 600 | # The HTO succeeded 601 | info['transferStatus'].set_text('Transfer Status: \u2714 completed') 602 | trajectories.append(info['transferStatus']) 603 | # If orbit insertion is true and has not yet been performed, perform second acceleration 604 | if gui.orbitInsertion.get(): 605 | Sim.Planets['HTO'].performOrbitInsertion(Sim) 606 | 607 | return trajectories 608 | 609 | ani = animation.FuncAnimation(fig, animate, frames=Sim.steps, interval=0, blit=True, init_func=init) 610 | ani.save('SolarSystemSimulation.mp4', fps=60, extra_args=['-vcodec', 'libx264']) 611 | return 0 612 | 613 | 614 | def setupPlot(self, Sim): 615 | # ------ 616 | # Plot set-up 617 | fig = plt.figure(figsize=(8, 8)) 618 | # Determine plot limits by finding planet with largest semi-major axis included in simulation 619 | lim = 0.4 620 | for planet in Sim.Planets.values(): 621 | if planet.simulate: 622 | if planet.semiMajor > lim: 623 | lim = planet.semiMajor 624 | lim *= 1.2 625 | ax = fig.add_subplot(111, xlim=(-lim, lim), ylim=(-lim, lim), aspect='equal', autoscale_on=False) 626 | #ax.set(xlabel='x / AU', ylabel='y / AU') 627 | elapsedTime = ax.text(0.65, 0.95, '', transform=ax.transAxes) 628 | 629 | # Move axis to centre 630 | ax.spines['left'].set_position('center') 631 | ax.spines['bottom'].set_position('center') 632 | 633 | # Eliminate upper and right axes 634 | ax.spines['right'].set_color('none') 635 | ax.spines['top'].set_color('none') 636 | 637 | # Show ticks in the left and lower axes only 638 | ax.xaxis.set_ticks_position('bottom') 639 | ax.yaxis.set_ticks_position('left') 640 | 641 | # Make axes more subtle 642 | ax.spines['left'].set_color('none') 643 | ax.spines['bottom'].set_color('none') 644 | ax.tick_params(axis='x', colors='none') 645 | ax.tick_params(axis='y', colors='none') 646 | ax.xaxis.label.set_color('none') 647 | ax.yaxis.label.set_color('none') 648 | 649 | plt.tight_layout() 650 | return fig, ax, elapsedTime 651 | 652 | 653 | def addInformation(self, ax): 654 | info = {} 655 | # Upper part shows basic information 656 | info['originDestination'] = ax.text(0.45, 0.95, '', transform=ax.transAxes) 657 | info['transferTime'] = ax.text(0.01, 0.93, '', transform=ax.transAxes) 658 | info['transferDistance'] = ax.text(0.01, 0.91, '', transform=ax.transAxes) 659 | info['elapsedTime'] = ax.text(0.65, 0.93, '', transform=ax.transAxes) 660 | info['destinationDistance'] = ax.text(0.65, 0.91, '', transform=ax.transAxes) 661 | info['transferStatus'] = ax.text(0.65, 0.89, '', transform=ax.transAxes) 662 | return info 663 | 664 | 665 | def hohmannTransfer(self, Sim): 666 | # The initial position and velocities of the origin and destination planet have to be adjusted 667 | # and their eccentricities set to zero 668 | for i, planet in enumerate([Sim.Planets[Sim.originPlanet], Sim.Planets[Sim.destinationPlanet]]): 669 | planet.eccentricity = 0 670 | # Planets need angular offset in order for the spacecraft to arrive at the correct time 671 | if i == 0: 672 | # The destination planet has to have an angular offset with respect to the origin planet 673 | # in order for the spacecraft to arrive at the destination planet at the correct time 674 | # The offset depends on the travel time. Using Kepler's third law in units of 1 AU and 1 Earth year: T^2/a^3=const 675 | dO = planet.semiMajor * (1 - planet.eccentricity) # Leave at perihelion 676 | dD = Sim.Planets[Sim.destinationPlanet].semiMajor # Arrive at aphelion 677 | 678 | semiMajorHTO = (dO + dD) / 2 679 | travelTime = np.sqrt(semiMajorHTO**3) / 2 # Only half-way required 680 | # Calculate required angular offset 681 | offset = travelTime/Sim.Planets[Sim.destinationPlanet].orbitalPeriod * 360. 682 | planet.eccentricAnomaly = np.radians(offset + 180) 683 | 684 | elif i ==1: 685 | planet.eccentricAnomaly = 0 686 | planet.position = planet.calculateInitialPosition() 687 | planet.velocity = planet.calculateInitialVelocity(Sim.delta) 688 | 689 | # ------ 690 | # The HTO is simulated using the spacecraft class. 691 | # Initial velocity and position determine the trajectory, therefore 692 | # we can use the same Sim.timeStep function to calculate the orbit 693 | HTO = Spacecraft(origin=Sim.Planets[Sim.originPlanet], destination=Sim.Planets[Sim.destinationPlanet], delta=Sim.delta) 694 | # Now all left to do is to regard spacecraft as fake planet 695 | # and append to list of simulated planets 696 | Sim.Planets['HTO'] = HTO 697 | return Sim, travelTime, semiMajorHTO 698 | 699 | 700 | def suggestSimParameters(self): 701 | # ------ 702 | # Initialize simulation 703 | Sim = SolarSystemSimulation() 704 | 705 | # Suggest timestep based on planet with smallest orbit included in simulation 706 | smallestOrbit = min([planet.orbitalPeriod for planet in Sim.Planets.values()]) 707 | gui.stepsize.set(round(smallestOrbit * 365.263 / 100, 2)) 708 | 709 | if gui.plotHohmann.get(): 710 | # Set duration of simulation to 110% of the HTO travelTime 711 | dO = Sim.Planets[Sim.originPlanet].semiMajor * (1 - Sim.Planets[Sim.originPlanet].eccentricity) # Leave at perihelion 712 | dD = Sim.Planets[Sim.destinationPlanet].semiMajor # Arrive at aphelion 713 | 714 | semiMajorHTO = (dO + dD) / 2 715 | travelTime = np.sqrt(semiMajorHTO**3) / 2 # Only half-way required 716 | gui.duration.set(round(travelTime * 1.1, 2)) 717 | 718 | else: 719 | # Set to one orbital period of planet with largest orbiatl period in simulation 720 | gui.duration.set(round(max([planet.orbitalPeriod for planet in Sim.Planets.values()]), 2)) 721 | 722 | 723 | if __name__ == '__main__': 724 | 725 | # Some definitions 726 | planets = ['Neptune', 'Uranus', 'Saturn', 'Jupiter', 'Mars', 'Earth', 'Venus', 'Mercury'] 727 | M = 1.998e30 # Solar Mass 728 | G = 6.67e-11 # Gravitational Constant 729 | 730 | # ------ 731 | # Start GUI 732 | root = tk.Tk() 733 | root.wm_title('SolarSystemOrbiter') 734 | gui = App(root) 735 | root.mainloop() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | 7 | 8 | setup( 9 | name="SolarSystemOrbiter", 10 | url="https://github.com/madoee/SolarSystemOrbiter", 11 | author="Max Mahlke", 12 | version="2.0", 13 | packages=['SolarSystemOrbiter'], 14 | author_email='m_mahlke@yahoo.de', 15 | description="Plot orbits of planets and calculate Hohmann Tranfer Orbits", 16 | long_description=open("README.md").read(), 17 | install_requires=[ 18 | "matplotlib", 19 | "numpy", 20 | ] 21 | ) 22 | --------------------------------------------------------------------------------