Professional Documents
Culture Documents
import time
import random
import numpy
import math
from datetime import datetime
import csv
#Creating and opening file that will store data produced by simulation
filename = datetime.now().strftime("%d_%m_%Y_%H_%M_%S")+".csv"
file = open(filename,"w")
#Gets user input of very large or very small numbers using scientific notation
#Message explains what value that program is asking for
def getSciNotation(message):
print(message)
invalid = True
while invalid:
orderedPair = input("Enter an ordered pair \"a,b\" for the number a*10^b:
").split(',')
try:
orderedPair = tuple(orderedPair)
orderedPair = (float(orderedPair[0]),float(orderedPair[1]))
if len(orderedPair) == 2:
answer = orderedPair[0] * 10**(orderedPair[1])
if not math.isinf(answer): #Very large numbers could be rounded to
infinity, which would be bad
invalid = False
return answer
except:
pass
#This function takes the current time and the positions and velocities of all the
heavenly bodies and saves them to the file created and opened earlier in a new line
using csv
def saveData(time,bodies,file):
line = ""
line += str(time)+"," #records time
for body in bodies:
line += str(body.getPosition()[0]) + ","
line += str(body.getPosition()[1]) + ","
line += str(body.getVelocity()[0]) + ","
line += str(body.getVelocity()[1]) + ","
line += "\n"
file.write(line)
class Body:
def __init__(self,name,color,position,velocity,mass,radius):
self.color = color
self.position = numpy.array(position)
self.velocity = numpy.array(velocity)
self.mass = mass
self.radius = radius
self.energyPerMass = 0
self.name = name
#pastPositions is a record of where the body has been, but with positions
rounded to the nearest pixel (using the DISTANCE_SCALE). A new position is only
added to pastPositions
#if it is in a different pixel. The advantage of having a list of rounded
positions instead of a list of pixels is that if the DISTANCE_SCALE changes (from a
body going off screen), pastPositions can be
#used to accurately draw the body's path with the new DISTANCE_SCALE,
something which would be harder if pixels were stored directly.
self.pastPositions = []
#As mentioned above, a new position is only added to pastPositions if that
position is in a different pixel. The pixels which the body has already covered are
stored in pixelsAlreadyCovered so that
#a new position can be added to pastPositions only when that position is in
a different pixel.
self.pixelsAlreadyCovered = []
self.nameTag = font.render(self.name,True,self.color,(0,0,0)) # to display
the name of the body
self.nameRect = self.nameTag.get_rect()
self.nameRect.center =
(int(self.position[0]/DISTANCE_SCALE),int(self.position[1]/DISTANCE_SCALE)-10)
def getPosition(self):
return self.position
def setPosition(self,position):
self.position = position
def getVelocity(self):
return self.velocity
def setVelocity(self,velocity):
self.velocity = velocity
def getMass(self):
return self.mass
def getRadius(self):
return self.radius
#Later, mechanical energy per mass is used to adjust the speeds of the different
bodies in accordance with conservation of energy
def getEnergyPerMass(self):
return self.energyPerMass
def setEnergyPerMass(self,energyPerMass):
self.energyPerMass = energyPerMass
def getName(self):
return self.name
def setName(self,name):
self.name = name
#Finds the gravitational potential energy of the body divided by its mass
def findGPEPerMass(self,bodies):
answer = 0
for otherbody in bodies:
if not distTwoPoints(self.position,otherbody.getPosition()) == 0:
distance = distTwoPoints(self.position,otherbody.getPosition())
answer -= G*otherbody.getMass()/distance #GPE equation based on
Newton's Law of Universal Gravitation
return answer
def findEnergyPerMass(self,bodies):
answer = 0
answer += 0.5*numpy.linalg.norm(self.getVelocity())**2
answer += self.findGPEPerMass(bodies)
#This summed up the GPE divided by the mass and the kinetic energy divided
by the mass
return answer
def drawBody(self,screen):
#When drawing the bodies, SCREEN_WIDTH/2 and SCREEN_HEIGHT/2 are added to
the pixel values. This is because
#(0,0) in pygame refers to the top left of the screen (positive y direction
is down), so adding those values
#moves the origin used by the position variables to the center of the
screen
x = int(self.position[0]/DISTANCE_SCALE+SCREEN_WIDTH/2)
y = int(self.position[1]/DISTANCE_SCALE+SCREEN_HEIGHT/2)
pygame.draw.circle(screen,self.color,(x,y),int(self.radius/RADIUS_SCALE))
def calculateAcceleration(self,bodies):
acceleration = numpy.array([0,0])
for body in bodies:
if not distTwoPoints(self.position,body.getPosition()) == 0: #Doesn't
calculate gravitational attraction on body from itself
#Newton's Law of Universal Gravitation combined with his Second Law
distance = distTwoPoints(self.position,body.getPosition())
r_vector = diffTwoPoints(body.getPosition(),self.position)
accelContribution =
numpy.multiply(G*body.getMass()/(distance**3),r_vector)
acceleration = acceleration + accelContribution #sums up
contributions from all the bodies
return acceleration
#This function adds the body's position (having been rounded to the nearest
pixel) to the pastPositions list and adds the pixel covered to
pixelsAlreadyCovered, but only if the pixel hasn't already been covered
def recordPosition(self):
roundedPosition =
numpy.array([DISTANCE_SCALE*int(self.position[0]/(DISTANCE_SCALE)),DISTANCE_SCALE*i
nt(self.position[1]/(DISTANCE_SCALE))])
pixelCovered =
(int(self.position[0]/DISTANCE_SCALE),int(self.position[1]/DISTANCE_SCALE))
if not pixelCovered in self.pixelsAlreadyCovered:
self.pastPositions.append(roundedPosition)
self.pixelsAlreadyCovered.append(pixelCovered)
#This function is used when the DISTANCE_SCALE changes. A pixel which was once
covered by the body's path may no longer be covered
def clearPixelsAlreadyCovered(self):
self.pixelsAlreadyCovered = []
#Writes the first row of the csv file so that it can be interpreted as a
spreadsheet
firstRow = "time,"
for body in bodies:
firstRow += body.getName() + " x pos,"
firstRow += body.getName() + " y pos,"
firstRow += body.getName() + " x vel,"
firstRow += body.getName() + " y vel,"
firstRow += "\n"
file.write(firstRow)
#The program should be able to display systems of very different sizes, so this
adjusts the DISTANCE_SCALE such that every body in the system is within a quarter
of the screen width horizontally and a quarter of the screen height vertically of
the origin
scaledWell = False
while not scaledWell: #loop that runs until the conditions are met
scaledWell = True
for body in bodies:
if not (body.getPosition()[0]/DISTANCE_SCALE > -SCREEN_WIDTH/4 and
body.getPosition()[0]/DISTANCE_SCALE < SCREEN_WIDTH/4 and body.getPosition()
[1]/DISTANCE_SCALE > -SCREEN_HEIGHT/4 and body.getPosition()[1]/DISTANCE_SCALE <
SCREEN_HEIGHT/4):
scaledWell = False
if scaledWell == False:
DISTANCE_SCALE = DISTANCE_SCALE*10 #increases DISTANCE_SCALE if conditions
aren't met
print("Distance scale adjusted to "+str(DISTANCE_SCALE)+"meters per pixel")
elif (body.getPosition()[0]/DISTANCE_SCALE > -SCREEN_WIDTH/40 and
body.getPosition()[0]/DISTANCE_SCALE < SCREEN_WIDTH/40 and body.getPosition()
[1]/DISTANCE_SCALE > -SCREEN_HEIGHT/40 and body.getPosition()[1]/DISTANCE_SCALE <
SCREEN_HEIGHT/40):
DISTANCE_SCALE = DISTANCE_SCALE/10 #If the bodies are all very close
together close to the origin, DISTANCE_SCALE is reduced
print("Distance scale adjusted to "+str(DISTANCE_SCALE)+"meters per pixel")
#calculate initial total mechanicalenergy of each body divided by the body's mass
for body in bodies:
body.setEnergyPerMass(body.findEnergyPerMass(bodies))
screen = pygame.display.set_mode([SCREEN_WIDTH,SCREEN_HEIGHT])
lastShownPositions = [] #used in determining whether the screen needs to be
updated, which is only when the bodies have moved enough from their last animated
position for the difference to be visible
while running: #main simulation loop, which contains the animation code as well
for event in pygame.event.get(): #makes the program exit the simulation loop if
the X in the top-right corner of the window is clicked
if event.type == pygame.QUIT:
running = False
#Adjusts time scale. If the time scale is too fast, the bodies will essentially
"skip around" too much and the results won't be realistic (we need a small delta-t)
#So the program adjust the time scale such that no body's delta-v during the
time interval used is more than 1% its prior velocity (if its prior velocity is at
least 100 m/s)
previousTimeScale = TIME_SCALE
TIME_SCALE = INIT_TIME_SCALE #Uses 10^6 seconds as a baseline delta-t
for body in bodies:
oldspeed = numpy.linalg.norm(body.getVelocity())
if oldspeed>100:
tooFast=True
while tooFast:
accelmag = numpy.linalg.norm(body.calculateAcceleration(bodies))
if TIME_SCALE*accelmag/oldspeed >= 10**(-2): #Ensure delta v is
less than one percent of previous velocity
TIME_SCALE = TIME_SCALE/10
else:
tooFast = False
if TIME_SCALE != previousTimeScale:
print("Changed time scale to: "+str(TIME_SCALE)+" seconds per frame from
"+str(previousTimeScale)) #notifies user of change to time scale
simTime += TIME_SCALE #updates counter of time that has passed since start of
simulation
for body in bodies:
#change velocities such that new velocity equals old velocity plus
(acceleration times delta-t)
body.setVelocity(body.getVelocity()
+numpy.multiply(body.calculateAcceleration(bodies),TIME_SCALE))
#changes positions so that new position equals old position plus (velocity
times delta-t)
body.setPosition(body.getPosition()
+numpy.multiply(body.getVelocity(),TIME_SCALE))
#Because only a conservative force (gravity) acts on the bodies, their
total mechanical energy must stay the same. If it hasn't, something went a bit
wrong, so the program
#adjusts their speeds to maintain conservation of energy. The formulae are
easier to work with if one takes total mechanical energy divided by mass, and the
masses are also constant, so this is fine to use
newEPM = body.findEnergyPerMass(bodies)
if not newEPM == body.getEnergyPerMass():
correctSpeed = (2*(body.getEnergyPerMass()-
body.findGPEPerMass(bodies)))**0.5 #If the total mechanical energy divided by the
mass isn't the same, it calculates what the speed should be to make it the same
if not math.isnan(correctSpeed): #for some reason there was a NaN error
before, so this avoids it
uncorrectedSpeed = numpy.linalg.norm(body.getVelocity())
correctionFactor = correctSpeed/uncorrectedSpeed #Figures out by
what factor the speed must change to maintain conservation of energy
body.setVelocity(numpy.multiply(body.getVelocity(),correctionFactor)) #multiplies
the velocity by that factor
body.recordPosition() #this function is explained where it is defined
#Rounds positions to nearest pixel. Then, if the pixels occupied by the bodies
are different from the last time the screen was updated, the screen will be updated
to show the movement
currentPositions = []
for body in bodies:
currentPositions += (int(body.getPosition()
[0]/DISTANCE_SCALE),int(body.getPosition()[1]/DISTANCE_SCALE))