Plotting Orbits with Python Library Matplotlib

What tools were used?

Python is a very high level language and rather easy to understand; the topic of coding orbits in Python was, on the other hand, much more difficult. Thankfully, [name]'s videos were invaluable in supporting this challenge, and we ended up with some good results! Using Matplotlib, a Python library that is made for plotting graphs from data, and some other maths-based Python libraries, we were able to effectively create a set of data points of a planet's position in the two body problem


Explaing the code

Full credit to Alfonso Gonzalez for the code - although I have slightly adapted and added in my own data and some functionality, the videos were instrumental in aiding the development of this code. Take a look at his videos by using the research link. Throughout many stages in development, I had lots of files on the go and different logic used throughout the code, but the current stage of the code is what I will write about here. It makes use of two files, one with all the functions (such as calculating the steps of the orbit, displaying the orbit, etc), and the other with main logic of program. This made it much easier to read and write the code by adding a level of abstraction to the main file, and hopefully it will be easier to explain as well. I also used a third file to contain all the data about different orbiting bodies (denoted by 'OB') and central bodies ('CB'), where a body can be both an OB and CB, for example the Earth. However, it is important to know that this code only simulates the two-body problem so, for example, having the Moon orbit the Earth would not give a very valid result because the mass of the Moon is comparable to that of the Earth. This allows you to easily modify the code to go from having Venus orbit the Sun to Halley's Comet orbiting the Sun - by just changing one line of code. I also added in the functionality of the colour of the orbit plotted relating to the velocity of the orbit, allowing you to see the effect of gravity even more!

The following is the main logic of the program:


    import numpy as np
    import matplotlib.pyplot as plt
    from scipy.integrate import ode
    from sympy import *
    from mpl_toolkits.mplot3d import Axes3D
    import planetarydatafile as pd
    from OrbitPropagator import OrbitPropagater as OP   # importing the data file
    plt.style.use('dark_background')

    # define the central body
    cb = pd.sun
    ob = pd.venus

    if __name__ == '__main__':

        # initial conditions for orbit (position and velocity) which are obtained from the data file
        r_mag_1 = ob['startPosition']  # distance away from central body that the orbit is
        v_mag_1 = ob['startVelocity']

        # initial position and velocity vectors
        #     this assumes position and velocity vectors are at right angles to each other initially,
        #     which varies in accuracy depending on the eccentricity of the orbit
        r0_1 = [r_mag_1,0,0]    # position as a vector coordinate
        v0_1 = [0,v_mag_1,0]    # starting velocity at right angle to position 
                                #(this is true for all circular orbits, so close to true for the orbits we are modelling)

        # timespan
        tspan_1 = 500 *(3600 * 24.0) # in seconds (x * days)

        #number of steps
        num_of_steps = 1000

        # timestep (calculated using the above values)
        dt_1 = int(tspan_1/num_of_steps)
        #dt_1 = 10 * (24*60*60) # seconds, (x * days)

        # calling functions that are in our custom orbit propogator file, to both propogate then show the orbit
        op_1 = OP(r0_1,v0_1,tspan_1,dt_1,cb=cb,ob=ob)
        op_1.propogate_orbit()
        op_1.plot_3d(show_plot=True)
    
The following is the more complex details of fuctions called in the main program:

    import numpy as np
    import matplotlib.pyplot as plt
    from scipy.integrate import ode

    import planetarydatafile as pd

    class OrbitPropagater:
        def __init__(self,r0,v0,tspan,dt,cb=pd.earth,ob=None):
            self.r0 = r0
            self.v0 = v0
            self.tspan = tspan
            self.dt = dt
            self.cb = cb
            self.ob = ob

            # =======================
            # DEFINE NECESSARY VARIABLES FOR THE ORBIT
            # =======================
            
            # total number of steps
            self.n_steps = int(np.ceil(self.tspan/self.dt))

            # initialise arrays
            self.ys = np.zeros((self.n_steps,6)) # an array contianin 6 sub arrays (for each position and velocity) of 0s for each time step
            self.ts = np.zeros((self.n_steps,1)) # one array of 

            # initial conditions
            self.y0 = self.r0 + self.v0
            self.ys[0] = np.array(self.y0)
            self.step = 1

            # solver - to solve the differential equation of the orbit and get values for the position at each timestep
            self.solver = ode(self.diffy_q)
            self.solver.set_integrator('lsoda')
            self.solver.set_initial_value(self.y0,0)

        def propogate_orbit(self):

            # propogate orbit
            while self.solver.successful() and self.step < self.n_steps:
                self.solver.integrate(self.solver.t+self.dt)
                self.ts[self.step] = self.solver.t 
                self.ys[self.step] = self.solver.y 
                self.step+=1

            self.rs = self.ys[:,:3]
            self.vs = self.ys[:,3:]
        
        def diffy_q(self,t,y):

            #unpack equation
            rx,ry,rz,vx,vy,vz = y
            r = np.array([rx,ry,rz]) # defining position as array, so it can be used as a vector

            # finding the norm (magnitude) of the vector
            norm_r = np.linalg.norm(r)

            # two body acceleration, uses equation to determine the acceleration of the orbiting body
            ax,ay,az = -r*self.cb['mu']/norm_r**3

            # return derivatives of position (velocity) and velocity (acceleration)
            return[vx,vy,vz,ax,ay,az]

        def plot_3d(self, show_plot=False, save_plot = False, title = None):

            # =======================
            # PREPARE (screen, figure, axis, variables, etc)
            # =======================

            # 3D pLot - screen that will appear
            fig=plt.figure(figsize=(10,10))
                # the actual axis that will appear
                # 111 - represents one plot on one figure (useful later when you may want to have multiple plots on one figure)
            ax=fig.add_subplot(111, projection='3d')

            # variables
            r_plot = self.cb['radius']

            # =======================
            # PLOT TRAJECTORY
            # =======================

            # plot trajectory and starting point. rs[:,i] - plots all rows of the ith column, i.e all the x coordinaties, all the y coordinates, all the z coordinates
            ax.plot(self.rs[:,0],self.rs[:,1],self.rs[:,2], 'w', zorder=10)
            # starting point, put points in a list otherwise it plots a line with 1 point (which is nothing). 'wo' means white (w) and point (o), w* uses a * to denote a star symbol
            ax.plot([self.rs[0,0]],[self.rs[0,1]],[self.rs[0,2]],'w*',zorder=10)

            # =======================
            # PLOT CENTRAL BODY
            # =======================

            _u, _v=np.mgrid[0:2*np.pi:20j,0:np.pi:10j]
            _x=r_plot*np.cos(_u)*np.sin(_v)
            _y=r_plot*np.sin(_u)*np.sin(_v)
            _z=r_plot*np.cos(_v)

            # this plots the surface of the central body, cmap denotes the colour and zorder makes it go behind all other plotted items (e.g. the orbits)
            ax.plot_surface(_x,_y,_z, cmap='Blues', zorder = 0)

            # =======================
            # PLOT SUPPORTING ITEMS
            # =======================

            # x,y,z arrows
            unit=r_plot*2.0 # length of arrows
            x,y,z=[[0,0,0], [0,0,0], [0,0,0]] # where the arrows start
            u,v,w=[[unit,0,0], [0,unit,0],[0,0,unit]] # where the arrows end
            # quiver command creates arrows. x,y,z (start), u,v,w (end)
            ax.quiver(x,y,z,u,v,w, color='w')

            # check for custom axes limits
            max_val=np.max(np.abs(self.rs))

            # define axis limits (range)
            ax.set_xlim([-max_val, max_val])
            ax.set_ylim([-max_val,max_val])
            ax.set_zlim([-max_val,max_val])
            # set labels and title
            ax.set_xlabel('X (km)'); ax.set_ylabel('Y (km)'); ax.set_zlabel('Z (km)');
            ax.set_aspect ('equal')

            if title:
                ax.set_title(title)
            else:
                if self.ob:
                    ax.set_title(('Orbit of ' + self.ob['name'] + ' around '+self.cb['name']))
                else:
                    ax.set_title(('Orbit around '+self.cb['name']))
            # creates a legend
            plt.legend(['Trajectory', 'Starting Position'])
            
            if show_plot:
                # show the plot
                plt.show()
            
            if save_plot:
                plt.savefig(title+'.png',dpi=300)
    

Research links