Demo entry 6352839

NEA

   

Submitted by anonymous on Mar 26, 2017 at 20:35
Language: Python. Code size: 43.3 kB.

#######################     Toby Hillery AQA NEA project    ###################
#######################  Gravitation Mechanics Simulation   ###################
###############################################################################

# the coding conventions that I have used are as follows:
#       Classes - CamelCase
#       Variables - CamelCase
#       Functions - lowercase_separated_by_underscored
#       Constants - UPPERCASE_SEPARATED_BY_UNDERSCORED


# below are all the imports needed for the program to run

#forces python to produce a real number from integer division
#i.e so instead of 1/2 rounding down to 0 (the nearest integer) it would equal 0.5
from __future__ import division

#imports the VPython library
from visual import *

#needed for creating random planets
import random

#import the wxpython library
import wx

#import accurate value of pi, and indicies handling
from math import pi,pow,isinf



########################################## Cosmic Objects Class  ##############################################

class CosmicObject:
    # A class to handle both the graphical Objects, but also the functions
    # required to calculate the gravitational attractions and mechanics.

    # class variable of G (gravitational constant)
    # value is common to all instances, may be changed by the user
    G = 0.5

    def __init__(self,Mass,Radius,Colour,Position,Velocity,Density):
        #create the visual Object
        self.Object = sphere(mass=Mass,color =Colour, pos=Position, radius=Radius, v= vector(Velocity), density=Density, make_trail=True)
        self.Object.trail_object.color = Colour
        

    def calculate_acceleration(self,pos):
        # calculates the net force acting upon a single Object due to all other Object
        # pos is passed as a parameter, is a 3D vector, and is the current position
        # at the current stage of approximation

        # variable to hold the net force
        ForceTotal = vector(0,0,0)

        for key in Planets.Dict:
            other = Planets.Dict[key]
            # if statement remove the current Object and non-existant Objects from calculation
            if other != self and other.Object.visible == True:
                
                #taking the absolute value of the difference in vector positions
                distance = magnitude(pos - other.Object.pos)
                
                #subtracting vectors but taking normalised form (returns a unit vector)
                force_direction = normalise(pos - other.Object.pos)
                
                if distance < 0.75*(self.Object.radius + other.Object.radius):
                    #then Objects have collided fully
                    self.on_collision(other)

                #calculate the force acting on the body
                try:   #possibility of underflow error with small mass, close distance
                    force_magnitude = (-CosmicObject.G*self.Object.mass*other.Object.mass) / distance**2
                    # test for rounding to infinity
                    if isinf(force_magnitude):
                        raise OverflowError
                    
                except ZeroDivisionError:
                    #excepts underflow also
                    #error when small mass, massive distance, python rounds small numbers to 0
                    #since force is effectively negligible, can ignore
                    pass

                except OverflowError as e:
                    raise   # raise error to parent function
                    
                    
                #combine magnitude and direction to create vector
                force_vector = force_direction * force_magnitude
                ForceTotal += force_vector
             
        #newtons second law
        acceleration = ForceTotal/self.Object.mass
   
        return (acceleration)

    def initial_derivative(self,InitialPos):
        # calculate acceleration using initial conditions
        try:
            acceleration = self.calculate_acceleration(InitialPos)
        except OverflowError as e:
            raise     #raise error to parent function 
        
        #dp/dt = v     dv/dt = a
        dp = self.Object.v
        dv = acceleration
        return(dp,dv)


    def next_derivative(self, InitialPos, InitialV, dp, dv, dt):
        # the main step of the RK4 algorithm, see analysis section for detailed explanation
        # takes initial conditions, as well as derivatives, and returns new derivatives

        #dp and dv are from previous derivative, increment using dt
        #in form of equation of line y=mx+c
        new_pos = InitialPos + dp * dt
        new_v = InitialV + dv * dt

        #calculate acceleration at this stage of the approximation
        try:
            acceleration = self.calculate_acceleration(new_pos)
        except OverflowError as e:
            raise   #raise error to parent function

        #dp is actually dp/dt, same with dv
        dp = new_v
        dv = acceleration

        return (dp,dv)
        

    def update_position(self,dt):
        #take defensive copies of properties so that they arent accidentally altered
        InitialPos = self.Object.pos
        InitialV = self.Object.v

        #the 4 steps of the runge kutta algorithm
        try:
            k1_dp, k1_dv = self.initial_derivative(InitialPos)
            k2_dp, k2_dv = self.next_derivative(InitialPos, InitialV, k1_dp, k1_dv, dt*0.5)
            k3_dp, k3_dv = self.next_derivative(InitialPos, InitialV, k2_dp, k2_dv, dt*0.5)
            k4_dp, k4_dv = self.next_derivative(InitialPos, InitialV, k3_dp, k3_dv, dt)
        except OverflowError as e:
            raise   #raise error to parent function
        
        #weighted mean of the 4 steps
        dp = (k1_dp + 2*(k2_dp + k3_dp) + k4_dp)/ 6
        dv = (k1_dv + 2*(k2_dv + k3_dv) + k4_dv)/ 6

        #update the position and velocity for that time period
        self.Object.pos += dp*dt
        self.Object.v += dv*dt
        

    def on_collision(self,other):
        # called when a collision is detected, merges objects and handles altered properties
        
        # update all the properties using a weighted average
        self.Object.color = tuple(self.resultant(other.Object.mass,vector(self.Object.color), vector(other.Object.color)))
        self.Object.pos = self.resultant(other.Object.mass,self.Object.pos,other.Object.pos)
        self.Object.density = self.resultant(other.Object.mass,self.Object.density, other.Object.density)
        self.Object.v = self.resultant(other.Object.mass, self.Object.v, other.Object.v)
        
        # update mass
        self.Object.mass += other.Object.mass

        # make the new planet bigger by using weighted density and mass
        self.radius_from_mass()

        # change name to reflect changes
        selfkey = self.key_by_value()
        otherkey = other.key_by_value()
        name = selfkey + "|" + otherkey
        Planets.Dict[name] = Planets.Dict.pop(selfkey)
        
        # set the other planet to no longer exist
        other.delete_object("")


    def resultant(self,othermass, selfvalue, othervalue):
        # performs a mean calculation using mass weighted values
        # takes the two values as input, return the average
        
        #weight value according to their mass
        selfweightedvalue = self.Object.mass * selfvalue
        otherweightedvalue = othermass * othervalue
        totalmass = self.Object.mass + othermass

        #find average between two weighted values
        value = (selfweightedvalue + otherweightedvalue) / totalmass
        return (value)
        
    
    def radius_from_mass(self):
        #use volume = mass/density
        #since weighted averages have already been calculated, can use self here
        mass = self.Object.mass
        density = self.Object.density
        
        try:  #possility of division by zero
            #substitute into rearranged formula
            RadiusCubed = (0.75 * mass) / (pi * density)
            self.Object.radius = pow(RadiusCubed,1/3)
    
        except ZeroDivisionError:
            pass #if the density is small emough to cause zero error
            #then no other Object will affect it, so keep things the same
        finally:
            return
              

    def mass_from_radius(self):
        density = 10
        #derived from mass = density * volume
        self.Object.mass = density * pow(self.Object.radius,3) * (4*pi/3)


    def delete_object(self,key):
        # add the object corresponding to passed key to the delete pile
        # if no key is passed, self object will be used
        self.Object.visible = False
        if key == "":    #then no key is passed
            key = self.key_by_value()
        Planets.ToDelete.append(key)
        

    def key_by_value(self):
        # returns the dict key of the self object
        for name in Planets.Dict:
            if Planets.Dict[name] == self:
                return(name)
    
        
        

#################################################  Create Window Class ##########################

class MyFrame(wx.Frame):

    def __init__(self,parent,xsize, ysize, TITLE):
        super(MyFrame,self).__init__(parent, title=TITLE, size=(xsize,ysize), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
        self.init_UI()


    def init_UI(self):
        #more individual inits
        self.init_widgits()
        self.init_menu()
        self.init_panel()
    
        #finally show everything
        self.Show()


    def init_widgits(self):
        # any widgets that dont belong to a main section are handled here
        self.statusbar = self.CreateStatusBar()
        self.statusbar.SetStatusText("Ready to roll")

#---------------------------------------- Create Menu Section ---------------------------

    def init_menu(self):
        #creates the menu bar upon which the menu is created
        self.MenuBar = wx.MenuBar()

        #create individual menus to go on the menu bar
        FileMenu = wx.Menu()

        #add options to each menu, using a standard ID adds in icon and keyboard shortcut
        QuitOption = wx.MenuItem(FileMenu,wx.ID_EXIT, "&Quit\tCtrl+Q", "Quit the application")
        ResetOption = wx.MenuItem(FileMenu, wx.ID_REVERT, "&Reset\tCtrl+R", "Reset the solar system to its initial state")
        NewOption = wx.MenuItem(FileMenu, wx.ID_NEW, "&New\tCtrl+N", "Create a new solar system")
        SaveOption = wx.MenuItem(FileMenu, wx.ID_SAVE, "&Save\tCtrl+S", "Save this solar system")
        LoadOption = wx.MenuItem(FileMenu, wx.ID_OPEN, "&Load\tCtrl+L", "Load a solar system from file")
        FileMenu.AppendItem(QuitOption)
        FileMenu.AppendItem(ResetOption)
        FileMenu.AppendItem(NewOption)
        FileMenu.AppendItem(SaveOption)
        FileMenu.AppendItem(LoadOption)
    
        #add individual menus to menu bar
        self.MenuBar.Append(FileMenu, "&File")       

        #adds the Menu bar to the parent frame
        self.SetMenuBar(self.MenuBar) 
        
        #a menu option should be bound to its function
        #wx.EVT_MENU is the event handler
        self.Bind(wx.EVT_MENU, self.on_quit, QuitOption)
        self.Bind(wx.EVT_MENU, self.on_new, NewOption)
        self.Bind(wx.EVT_MENU, self.on_reset, ResetOption)
        self.Bind(wx.EVT_MENU, self.on_save, SaveOption)
        self.Bind(wx.EVT_MENU, self.on_load, LoadOption)

            
    def on_quit(self,e):
        quit()

    def on_new(self,e):
        self.new_simulation()

    def new_simulation(self):
        Planets.clear_dict(Planets.Dict)
        Planets.clear_dict(Planets.Copy)
        #update the combo box to reflect changes
        self.update_combobox()
        #clear remaining entry
        self.clear_entry
        
#-------------------------------------------------------- Save/Load section ------------------

            
    def on_save(self,e):
        #first ask the user for a file name
        SaveWindow = wx.FileDialog(self, "Save simulation as... ","", "", "*.sim", wx.SAVE|wx.OVERWRITE_PROMPT)
        Confirm = SaveWindow.ShowModal()    #returns result of which button clicked: okay or cancel
        FileName = SaveWindow.GetPath()     #returns the file name that the user chose, with location
        SaveWindow.Destroy()                #closes the window down once the user has chosen
        
        if Confirm == wx.ID_CANCEL:     #then user chose to cancel, so escape function
            return False
        
        elif Confirm == wx.ID_OK:
            #user has confirmed that they want to save, and specified a name
            SaveFile = open(FileName, "w")
            
            for key in Planets.Copy.keys():
                SaveList = []
                Object = Planets.Copy[key]
                #writing everything to a list first makes it easier to write to file
                SaveList.append(key)
                SaveList.append(Object.Object.mass)
                SaveList.append(Object.Object.pos)
                SaveList.append(Object.Object.radius)
                SaveList.append(Object.Object.v)
                SaveList.append(Object.Object.color)
                
                for item in SaveList:   #iterate through things to write
                    SaveFile.write(str(item))
                    SaveFile.write("\t")    #\t indicates gap between properties
                SaveFile.write("\n")        #\n indicates end of Object

            SaveFile.close()                

            

    def on_load(self,e):

        #first ask the user for a file name
        LoadWindow = wx.FileDialog(self, "Load simulation","", "", "*.sim", wx.OPEN)
        Confirm = LoadWindow.ShowModal()    #returns result of which button clicked: okay or cancel
        File = LoadWindow.GetPath()     #returns the file name that the user chose, with location
        LoadWindow.Destroy()                #closes the window down once the user has chosen
        
        if Confirm == wx.ID_CANCEL:
            return False
        
        elif Confirm == wx.ID_OK:
            # take defensive copies of copy dictionary in case of failure
            CopiedDict = dict(Planets.Copy)
            
            #clear old Objects
            Planets.clear_dict(Planets.Dict)
            Planets.clear_dict(Planets.Copy)
    
            #"File" is pathname returned from system window
            LoadFile = open(File,"r")

            try:
                #each Object is separated by a \n, or end of line
                #therefore each line represent an Object
                for line in LoadFile:
                    Details = line.split("\t")  #\t separates properties
                    Details.pop(len(Details)-1) #remove the \n from the end
                    
                    Name = Details[0]
                    Mass = float(Details[1])

                    Position = self.string_to_list(Details[2])
                    
                    Radius = float(Details[3])
                    
                    Velocity = self.string_to_list(Details[4])
                    
                    Colour = tuple(self.string_to_list(Details[5]))
                    
                    #find the density from mass and radius
                    Density = (3*Mass) / (4*pi*Radius**3)

                    #use the data read in to create Objects, one for main, one for copy
                    Object = CosmicObject(Mass,Radius,Colour,Position,Velocity,Density)
                    ObjectCopy = CosmicObject(Mass,Radius,Colour,Position,Velocity,Density)
                    ObjectCopy.Object.visible = False
                    Planets.Dict[Name] = Object
                    Planets.Copy[Name] = ObjectCopy
                    
            except:
                # if the file is corrupt or tampered with, then catch errors

                Message = "The file that you selected is corrupt; the simulation will not reset"
                wx.MessageBox(Message, "Corrupt File", wx.OK|wx.ICON_ERROR)
                
                # revert to defensive copy
                Planets.Copy = CopiedDict
                Planets.load_copy()
                self.clear_entry()
                
            finally:
                LoadFile.close()
                #update the combo box to reflect changes
                self.update_combobox()
                #clear the old entry
                self.clear_entry()


    def string_to_list(self,Input):
        #takes string eg "<255,255,0>", returns [255,255,0]
        Input = Input[1:-1] #removes <> or ()
        List = Input.split(",")
        for i in range(0,len(List)):
            List[i] = (float(List[i]))
        return (List)
        
    
#--------------------------------------------------------- Objects Panel Section --------------

    def init_panel(self):
        
        #create Object section
        self.ObjectPanel = wx.Panel(self)

        #create some Objects to go in this panel
        #order doesnt matter here, they will be added later
        Line = wx.StaticLine(self.ObjectPanel)
        self.ResetButton = wx.Button(self.ObjectPanel, label="Reset Simulation")
        self.SubmitButton = wx.Button(self.ObjectPanel, label="Submit")
        self.SubmitButton.Disable()
        self.CheckButton = wx.Button(self.ObjectPanel, label="Check Details")
        self.PlayButton = wx.Button(self.ObjectPanel, label="Play")
        self.PlayButton.SetBackgroundColour("GREEN")
        
        #use a combo box to display all the Objects   
        self.ObjectInput = wx.ComboBox(self.ObjectPanel,style=wx.CB_READONLY)
        self.update_combobox()

        #creates the title bar to go at the top
        TitleBox = wx.BoxSizer(wx.HORIZONTAL)
        TitleBox.Add(self.ObjectInput, proportion = 10,flag=wx.ALL|wx.EXPAND)
        TitleBox.Add(self.PlayButton , proportion=1)
        
        #creates the grid 
        Grid = wx.FlexGridSizer(rows=7, cols=3, hgap=15, vgap=15)
        PositionGrid = wx.GridSizer(1,3,0,10)
        VelocityGrid = wx.GridSizer(1,3,0,10)
        ColourGrid = wx.GridSizer(1,3,0,10)

        #create the text labels
        Name = wx.StaticText(self.ObjectPanel, label="Name")
        Mass = wx.StaticText(self.ObjectPanel, label="Mass")
        Colour = wx.StaticText(self.ObjectPanel, label="Colour  (RGB)")
        Radius = wx.StaticText(self.ObjectPanel, label="Radius")
        Position = wx.StaticText(self.ObjectPanel, label="Position (XYZ)")
        Velocity = wx.StaticText(self.ObjectPanel, label="Velocity (XYZ)")

        #to show entry
        self.ShowName = wx.StaticText(self.ObjectPanel,label="")
        self.ShowMass = wx.StaticText(self.ObjectPanel,label="")
        self.ShowRadius = wx.StaticText(self.ObjectPanel,label="")
        self.ShowColour = wx.Panel(self.ObjectPanel, style=wx.SUNKEN_BORDER)
        self.ShowPosition = wx.StaticText(self.ObjectPanel,label="")
        self.ShowVelocity = wx.StaticText(self.ObjectPanel,label="")
        
        #create the methods  of entering data
        self.EnterName = wx.TextCtrl(self.ObjectPanel)
        self.EnterMass = wx.TextCtrl(self.ObjectPanel)
        self.EnterRadius = wx.TextCtrl(self.ObjectPanel)
        self.EnterColourR = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterColourG = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterColourB = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterPositionX = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterPositionY = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterPositionZ = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterVelocityX = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterVelocityY = wx.TextCtrl(self.ObjectPanel,size=(30,25))
        self.EnterVelocityZ = wx.TextCtrl(self.ObjectPanel,size=(30,25))

        #add in a slider to define the value of G
        GLabel = wx.StaticText(self.ObjectPanel, label = "Slide to change Gravitational Constant")
        self.GSlider = wx.Slider(self.ObjectPanel, value = 50, minValue = 0, maxValue= 100)
                           
        #individual grids
        PositionGrid.Add(self.EnterPositionX)
        PositionGrid.Add(self.EnterPositionY)
        PositionGrid.Add(self.EnterPositionZ)
        
        VelocityGrid.Add(self.EnterVelocityX)
        VelocityGrid.Add(self.EnterVelocityY)
        VelocityGrid.Add(self.EnterVelocityZ)

        ColourGrid.Add(self.EnterColourR)
        ColourGrid.Add(self.EnterColourG)
        ColourGrid.Add(self.EnterColourB)
        
        #add everything to the grid
        Grid.AddMany([
            (Name), (self.EnterName), (self.ShowName),
            (Mass), (self.EnterMass), (self.ShowMass),
            (Colour), (ColourGrid), (self.ShowColour,1,wx.ALL|wx.EXPAND),
            (Radius), (self.EnterRadius), (self.ShowRadius),
            (Position), (PositionGrid), (self.ShowPosition),
            (Velocity), (VelocityGrid), (self.ShowVelocity),
            (self.ResetButton,1,wx.ALIGN_LEFT), (self.CheckButton,1,wx.ALIGN_CENTER), (self.SubmitButton,1,wx.ALIGN_RIGHT)
            ])
        
        #create sizer
        Box = wx.BoxSizer(wx.VERTICAL)
        
        # add individual things to sizer
        Box.Add(TitleBox)
        Box.Add(Line, flag=wx.EXPAND)
        Box.Add(Grid, proportion =1, flag=wx.ALL|wx.EXPAND, border=15)
        Box.Add(GLabel)
        Box.Add(self.GSlider, flag=wx.EXPAND)

        #attach the sizer to the main panel
        self.ObjectPanel.SetSizer(Box)

        #bindings
        self.ResetButton.Bind(wx.EVT_BUTTON, self.on_reset)
        self.SubmitButton.Bind(wx.EVT_BUTTON, self.create_cosmic)
        self.CheckButton.Bind(wx.EVT_BUTTON, self.on_check_details)
        self.PlayButton.Bind(wx.EVT_BUTTON, self.on_play)
        self.ObjectInput.Bind(wx.EVT_COMBOBOX, self.on_input_selection)
        self.GSlider.Bind(wx.EVT_SCROLL, self.on_slider)


    def on_slider(self,e):
        # when the user moves the slider to change G.
        Slider = e.GetEventObject()
        CosmicObject.G = Slider.GetValue()


    def on_input_selection(self,e):
        Input = e.GetString()
        if Input != "-- Create New --":
            #edit an Object
            Object = Planets.Dict.get(Input)
    
            Name = Input
            Mass = str(Object.Object.mass)
            Radius = str(Object.Object.radius)
            
            Position = Object.Object.pos
            Velocity = Object.Object.v

            #unpack tuple, then change from 0-1 to 0-255
            R,G,B = Object.Object.color
            R = R*255
            G = G*255
            B = B*255
            

            self.EnterName.SetValue(Name)
            self.EnterMass.SetValue(Mass)
            self.EnterPositionX.SetValue(str(Position.x))
            self.EnterPositionY.SetValue(str(Position.y))
            self.EnterPositionZ.SetValue(str(Position.z))
            self.EnterColourR.SetValue(str(R))
            self.EnterColourG.SetValue(str(G))
            self.EnterColourB.SetValue(str(B))
            self.EnterVelocityX.SetValue(str(Velocity.x))
            self.EnterVelocityY.SetValue(str(Velocity.y))
            self.EnterVelocityZ.SetValue(str(Velocity.z))
            self.EnterRadius.SetValue(Radius)

    def on_play(self,e):
        self.play_pause()

    def play_pause(self):
        if self.Play == True:   #then pause
            self.Play = False
            self.PlayButton.SetLabel("Play")
            self.PlayButton.SetBackgroundColour("GREEN")
            
            #handling the deletion of redundant Objects
            Planets.clear_deleted_objects()

            #update the combo box to reflect changes
            self.update_combobox()
                
            self.enable_interface()  #re-allow user interaction
            
        elif self.Play == False:    #then play
            self.Play = True
            self.PlayButton.SetLabel("Pause")
            self.PlayButton.SetBackgroundColour("RED")
            self.disable_interface()
        #refresh the button to change the colour
        self.PlayButton.Refresh()


    def update_combobox(self):
        #update the input box by wiping and re appending
        self.ObjectInput.Clear()
        self.ObjectInput.Append("-- Create New --")
        self.ObjectInput.Value = "-- Create New --"
        SortedList = sort_handler(Planets.Dict)
        for name in SortedList:
            self.ObjectInput.Append(name)

        
    def disable_interface(self):
        self.SubmitButton.Disable()
        self.CheckButton.Disable()
        self.ResetButton.Disable()
        self.ObjectInput.Disable()
        self.MenuBar.EnableTop(0,False)
        self.GSlider.Disable()


    def enable_interface(self):
        self.CheckButton.Enable()
        self.ResetButton.Enable()
        self.ObjectInput.Enable()
        self.MenuBar.EnableTop(0,True)
        self.GSlider.Enable()


    def on_reset(self,e):
        Planets.load_copy()
        self.clear_entry()
        
        
   
################################################### CHECK DETAILS #####################################       

    def on_check_details(self,e):
        Count = self.check_details()
        if Count == 6:
            self.SubmitButton.Enable()
        else:
            self.SubmitButton.Disable()

       
    def check_details(self):
        #check every input, if acceptable, +1 Count
        Count = 0

        #Name
        if self.check_name():
            Count += 1

        #Mass
        if self.check_value(self.EnterMass.GetValue(),self.ShowMass):
            Count += 1

        #Radius
        if self.check_value(self.EnterRadius.GetValue(), self.ShowRadius):
            Count += 1
            
        #position
        X = self.EnterPositionX.GetValue()
        Y = self.EnterPositionY.GetValue()
        Z = self.EnterPositionZ.GetValue()
        if self.check_vector(X,Y,Z,self.ShowPosition):
            Count += 1
            
        #velocity
        X = self.EnterVelocityX.GetValue()
        Y = self.EnterVelocityY.GetValue()
        Z = self.EnterVelocityZ.GetValue()
        if self.check_vector(X,Y,Z,self.ShowVelocity):
            Count += 1
            
        #Colour
        if self.test_colour() == True:
            Count += 1

        return (Count)

    
    def check_name(self):
        #as long as not blank, its okay
        Name = self.EnterName.GetValue()
        if Name != "":
            self.ShowName.SetLabel(Name)
            return True
        else:
            self.ShowName.SetLabel("Missing")


    def check_vector(self,X,Y,Z,ShowObject):
        try:
            
            if X != "" and Y !="" and Z != "": #Ie not blank
                try:    #if cant be converted to float, then invalid
                    Vector = vector(float(X),float(Y),float(Z))
                except:
                    raise InvalidInput()
            else:
                raise EmptyInput()
            
            ShowObject.SetLabel(str(Vector))
            return True
        
        except InvalidInput:
            ShowObject.SetLabel("Invalid Input")
        except EmptyInput:
            ShowObject.SetLabel("Missing")


    def check_value(self, Value, ShowObject):
        try:
            
            if Value != "":
                try:    #catches none floats
                    Float = float(Value)
                except:
                    raise InvalidInput
                
                if Float <= 0: #catches negatives
                     raise InvalidInput
                else:
                    ShowObject.SetLabel(str(Float))
                    return True
                
            else:
                raise EmptyInput

        except InvalidInput:
            ShowObject.SetLabel("Invalid Input")
        except EmptyInput:
            ShowObject.SetLabel("Missing")
            

    def check_float_range(self,Value,Min,Max):
        if Value != "":
            try:    #catches none floats
                Float = float(Value)
            except:
                return False
            
            if Float <= Max and Float >= Min:
                return (Float)
            else:
                return False
            
        else:
            return False


    def test_colour(self):
        #will contain false if invalid, or float value if valid
        R = self.check_float_range(self.EnterColourR.GetValue(),0,255) 
        G = self.check_float_range(self.EnterColourG.GetValue(),0,255)
        B = self.check_float_range(self.EnterColourB.GetValue(),0,255)

        #   "is not" is necessary as 0 will evaluate to false otherwise
        if R is not False and G is not False and B is not False:
            RGB = (R,G,B);  #; signifies tuple
            self.ShowColour.SetBackgroundColour(RGB)
            self.ShowColour.Refresh()
            return True
        else:
            self.ShowColour.SetBackgroundColour(wx.NullColour)
            self.ShowColour.Refresh()
            return False

    
    def create_cosmic(self,e):
        if self.check_details() == 6: #then all checks passed
            #first delete old Object
            Input = self.ObjectInput.GetValue()
            if Input != "-- Create New --":
                #then the old Object needs to be deleted
                Old = Planets.Dict.get(Input)
                Old.delete_object(Input)
                
                # force delete Object now, rather than wait for auto delete
                Planets.clear_deleted_objects()
                
            Name = self.EnterName.GetValue()
            Mass = float(self.EnterMass.GetValue())
            Radius = float(self.EnterRadius.GetValue())
            
            X = self.EnterPositionX.GetValue()
            Y = self.EnterPositionY.GetValue()
            Z = self.EnterPositionZ.GetValue()
            Position = vector(X,Y,Z)
            
            X = self.EnterVelocityX.GetValue()
            Y = self.EnterVelocityY.GetValue()
            Z = self.EnterVelocityZ.GetValue()
            Velocity = vector(X,Y,Z)
            
            #find the density from mass and radius
            Density = (3*Mass) / (4*pi*Radius**3)

            R = float(self.EnterColourR.GetValue())
            G = float(self.EnterColourG.GetValue())
            B = float(self.EnterColourB.GetValue())
            #scale from 0-255 to 0-1, then make into tuple
            RGB = (R/255,G/255,B/255);

            Object = CosmicObject(Mass,Radius,RGB,Position,Velocity,Density)
            Planets.Dict[Name] = Object
            self.ObjectInput.Append(Name)

            #also add Object to initial copy
            ObjectCopy = CosmicObject(Mass,Radius,RGB,Position,Velocity,Density)
            ObjectCopy.Object.visible = False
            Planets.Copy[Name] = ObjectCopy
            
            #wipe the entire panel and then rewrite in order to reset
            self.clear_entry()
            self.SubmitButton.Disable()
            self.update_combobox()
             
        else:
            self.SubmitButton.Disable()


    def clear_entry(self):
        # just clear everything
        # there's no convenient way to do it
        # just have to manually call widgits
        self.EnterName.SetValue("")
        self.EnterRadius.SetValue("")
        self.EnterPositionX.SetValue("")
        self.EnterPositionY.SetValue("")
        self.EnterPositionZ.SetValue("")
        self.EnterMass.SetValue("")
        self.EnterVelocityX.SetValue("")
        self.EnterVelocityY.SetValue("")
        self.EnterVelocityZ.SetValue("")
        self.EnterColourR.SetValue("")
        self.EnterColourG.SetValue("")
        self.EnterColourB.SetValue("")

        self.ShowName.SetLabel("")
        self.ShowRadius.SetLabel("")
        self.ShowMass.SetLabel("")
        self.ShowPosition.SetLabel("")
        self.ShowVelocity.SetLabel("")
        self.ShowColour.SetBackgroundColour(wx.NullColour)
        self.ShowColour.Refresh()
        
    

        
########################################## Objects class ################################       

class Objects():
    
    def __init__(self):
        self.Dict = {}      # holds all the objects of the simulation
        self.ToDelete = []     # holds objects that are waiting to be deleted 
        self.Copy = {}      # a copy of self.Dict, holds initial conditions for resets
        self.Deleted = []       # keeps a record of all the objects that have been deleted


    def spawn_random(self,number):
        # called at the start of the program to populate simulation with random objects
        # number is the number of random objects needed
        
        for i in range (0,number):
            radius = 2
            colour=(random.uniform(0,1),random.uniform(0,1),random.uniform(0,1))
            mass = 100
            #pick random positions and initial velocity
            position = (random.uniform(-100,100),random.uniform(-100,100))
            velocity = (random.uniform(-1,1), random.uniform(-1,1))

            #find the density from mass and radius
            Density = (3*mass) / (4*pi*radius**3)
            
            key = str(i)
            self.Dict[key] = CosmicObject(mass,radius,colour,position,velocity, Density)
            # needs to be added to both to ensure two distinct objects are stored
            # i.e. copy dict needs to be independant of main dict
            self.Copy[key] = CosmicObject(mass,radius,colour,position,velocity, Density)
            
        for key in self.Copy:
            # effectively set all the copy objects to not exist
            self.Copy[key].Object.visible = False


    def clear_dict(self,Dict):
        # takes a dictionary as input and deletes the contents, then the dict itself
        for key in Dict.keys():
            #make them invisible, then delete reference
            Object = Dict[key]
            Object.Object.visible = False
            Object.Object.trail_object.visible = False
            del Dict[key]

        if Dict == self.Dict:
            #iterate through the remaining trails of deleted Objects
            for item in self.Deleted:
                item.Object.trail_object.visible = False
            self.Deleted = []

            
    def load_copy(self):
        # wipes the main dict, then repopulates with values in copy dict
        
        # first clear the old Objects
        self.clear_dict(self.Dict)
        
        # repopulate the dictionary with new Objects
        # using their initial values
        for key in self.Copy:
            CopyObject = Planets.Copy[key]
            mass = CopyObject.Object.mass
            radius = CopyObject.Object.radius
            position = CopyObject.Object.pos
            colour = CopyObject.Object.color
            velocity = CopyObject.Object.v
            density = CopyObject.Object.density

            # create new object rather than use reference to avoid conflicts and maintain independance
            NewObject = CosmicObject(mass,radius,colour,position,velocity, density)
            self.Dict[key] = NewObject

        
    def clear_deleted_objects(self):
        for item in self.ToDelete:
            #keep note of deleted Objects so trails can be deleted 
            self.Deleted.append(self.Dict[item])
            del self.Dict[item]
        self.ToDelete = []
        
########################################## VECTOR CALCULATIONS ###########################################

def magnitude(a_vector):
    #returns the magnitude of a vector (its size) as a scalar
    return (a_vector.x**2 + a_vector.y**2 + a_vector.z**2) **0.5

def normalise(a_vector):
    #returns the normalised form of a vector, which is the unit vector representing direction
    return a_vector/magnitude(a_vector)

######################################### EXCEPTIONS #####################################################

class InvalidInput(Exception):
    def __init__(self):
        Exception.__init__(self,"The data you inputted was invalid")


class EmptyInput(Exception):
    def __init__(self):
        Exception.__init__(self, "No data was entered")


class OverflowError(Exception):
    # custom override of built in exception to add error messages and
    # better handling, so program doesnt quit.
    
    def __init__(self):
        # call parent init funtion first
        super(OverflowError, self).__init__("An overflow error occured")

        Message = "The simulation has generated a number that is too large to handle, and so this simulation is redundant. Therefore a new, blank simulation will now be loaded."
        
        wx.MessageBox(Message, "Overflow Error", wx.OK|wx.ICON_ERROR)
        ControlWindow.play_pause()
        ControlWindow.new_simulation()
        
        
        
########################################  CIRCULAR QUEUE STRUCTURE ##########################################

class CircularQueue:
    # a class to handle the circular queue structure used to show trails
    # designed and implemented in iteration 3, but replaced by in built function
    # due to performance benefits(see documentation).
    # Code is included here for those who may wish to use it instead of the in built
    # function due to increased customisability.
    
    def __init__(self,parent):
        # parent class must be passed so that the trail Object can be bound to the parent
        self.__size = 10
        self.__pointer = 0
        self.__array = []  # use a list to act like an array
        self.__counter = 0
        self.__interval = 10
        for item in range(0,self.__size):
            # generate the required number of points, add to array
            point = sphere(pos=(parent.Object.pos), color=(parent.Object.color),opacity=1, radius=parent.Object.radius)
            self.__array.append(point)
        

    def enqueue(self,position):
        # position is passed as a parameter so that function acts through interface only
        # function called after a cosmic Object's position is incremented
        if self.__counter == self.__interval:
            if self.__pointer == self.__size-1:
                # when gets back end, return to start
                self.__pointer -= self.__size

            # simply overwrites the oldest position with the new one
            point = self.__array[self.__pointer]
            point.pos = position
    
            # makes the point visible
            point.opacity = 0.5
            
            # increment pointer to next trail point
            self.__pointer += 1
            
            # reset counter to 0
            self.__counter = 0
            
        # increment counter
        self.__counter += 1 

######################################## merge SORTING ALGORITHMS ##############################################

def merge(Left, Right):
    # takes two halves from the conquer step,
    # return a sorted result as a list.

    LeftPos = 0
    RightPos = 0
    Output = []
    # loops whilst there are still more than one element left in each half
    while LeftPos < len(Left) and RightPos < len(Right):
        # Use Evaluate function to try convert to float
        LeftItem = Evaluate(Left[LeftPos])
        RightItem = Evaluate(Right[RightPos])

        # performs the comparison, smallest key is added
        # position counter is then incremented
        if LeftItem < RightItem:
            Output.append(str(LeftItem))
            LeftPos += 1
        else:
            Output.append(str(RightItem))
            RightPos += 1

    #remaining Object can be added
    if LeftPos < len(Left):
        Output.extend(Left[LeftPos:])

    if RightPos < len(Right):
        Output.extend(Right[RightPos:])
    return Output


def Evaluate(Value):
    # takes a value, and return either the integer form,
    # if suitable.
    try:
        Value = int(Value)
    finally:
        # none integer keys can be kept as string
        # so no action is needed
        return Value
        
        
def merge_sort(List):
    if len(List) <= 1:
        # base case has been reached
        return List

    # use recursive calling to continually split list into halves
    middle = len(List) // 2
    Left = merge_sort(List[:middle])
    Right = merge_sort(List[middle:])

    # by here, all lists have been split, so combination
    # process can begin.
    return merge(Left, Right)


def sort_handler(Dict):
    # takes a dictionary, picks out the keys
    # and returns the keys in alphabetical order
    ObjectList = []
    for key in Dict:
        ObjectList.append(key)
        
    # uses a merge sort to order keys
    SortedList = merge_sort(ObjectList)
    return (SortedList)
        
                   
    

            
    

########################################## START OF PROGRAM ###############################################

Graphics = display( title = "Graphics Window",
                    x=0, y=0,
                    width=600, height=600
                    )

#creates 20 planets
Planets = Objects()
Planets.spawn_random(10)

#needs to be after the creation of Objects
Graphics.autoscale = False

ControlWindow = MyFrame(None,400,450,"Solar System Simulation")

dt = 0.03
ControlWindow.Play = False

while True:
    # the execution speed of the program must be specified
    rate(100)
    
    if ControlWindow.Play == True:

        for key in Planets.Dict:
            
            Planet = Planets.Dict[key]
            # discount invisible Objects waiting to be cleared
            if Planet.Object.visible == True:
                
                try:
                    Planet.update_position(dt)
                except OverflowError as e:
                    # now that error has reached top of stack
                    # break out of iteration over dictionary
                    # must use break to avoid dictionary iteration error
                    break
                    

        
##############################################################################################



        
       

This snippet took 0.05 seconds to highlight.

Back to the Entry List or Home.

Delete this entry (admin only).