-
Notifications
You must be signed in to change notification settings - Fork 51
/
nbody.py
192 lines (146 loc) · 4.78 KB
/
nbody.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import numpy as np
import matplotlib.pyplot as plt
"""
Create Your Own N-body Simulation (With Python)
Philip Mocz (2020) Princeton Univeristy, @PMocz
Simulate orbits of stars interacting due to gravity
Code calculates pairwise forces according to Newton's Law of Gravity
"""
def getAcc( pos, mass, G, softening ):
"""
Calculate the acceleration on each particle due to Newton's Law
pos is an N x 3 matrix of positions
mass is an N x 1 vector of masses
G is Newton's Gravitational constant
softening is the softening length
a is N x 3 matrix of accelerations
"""
# positions r = [x,y,z] for all particles
x = pos[:,0:1]
y = pos[:,1:2]
z = pos[:,2:3]
# matrix that stores all pairwise particle separations: r_j - r_i
dx = x.T - x
dy = y.T - y
dz = z.T - z
# matrix that stores 1/r^3 for all particle pairwise particle separations
inv_r3 = (dx**2 + dy**2 + dz**2 + softening**2)
inv_r3[inv_r3>0] = inv_r3[inv_r3>0]**(-1.5)
ax = G * (dx * inv_r3) @ mass
ay = G * (dy * inv_r3) @ mass
az = G * (dz * inv_r3) @ mass
# pack together the acceleration components
a = np.hstack((ax,ay,az))
return a
def getEnergy( pos, vel, mass, G ):
"""
Get kinetic energy (KE) and potential energy (PE) of simulation
pos is N x 3 matrix of positions
vel is N x 3 matrix of velocities
mass is an N x 1 vector of masses
G is Newton's Gravitational constant
KE is the kinetic energy of the system
PE is the potential energy of the system
"""
# Kinetic Energy:
KE = 0.5 * np.sum(np.sum( mass * vel**2 ))
# Potential Energy:
# positions r = [x,y,z] for all particles
x = pos[:,0:1]
y = pos[:,1:2]
z = pos[:,2:3]
# matrix that stores all pairwise particle separations: r_j - r_i
dx = x.T - x
dy = y.T - y
dz = z.T - z
# matrix that stores 1/r for all particle pairwise particle separations
inv_r = np.sqrt(dx**2 + dy**2 + dz**2)
inv_r[inv_r>0] = 1.0/inv_r[inv_r>0]
# sum over upper triangle, to count each interaction only once
PE = G * np.sum(np.sum(np.triu(-(mass*mass.T)*inv_r,1)))
return KE, PE;
def main():
""" N-body simulation """
# Simulation parameters
N = 100 # Number of particles
t = 0 # current time of the simulation
tEnd = 10.0 # time at which simulation ends
dt = 0.01 # timestep
softening = 0.1 # softening length
G = 1.0 # Newton's Gravitational Constant
plotRealTime = True # switch on for plotting as the simulation goes along
# Generate Initial Conditions
np.random.seed(17) # set the random number generator seed
mass = 20.0*np.ones((N,1))/N # total mass of particles is 20
pos = np.random.randn(N,3) # randomly selected positions and velocities
vel = np.random.randn(N,3)
# Convert to Center-of-Mass frame
vel -= np.mean(mass * vel,0) / np.mean(mass)
# calculate initial gravitational accelerations
acc = getAcc( pos, mass, G, softening )
# calculate initial energy of system
KE, PE = getEnergy( pos, vel, mass, G )
# number of timesteps
Nt = int(np.ceil(tEnd/dt))
# save energies, particle orbits for plotting trails
pos_save = np.zeros((N,3,Nt+1))
pos_save[:,:,0] = pos
KE_save = np.zeros(Nt+1)
KE_save[0] = KE
PE_save = np.zeros(Nt+1)
PE_save[0] = PE
t_all = np.arange(Nt+1)*dt
# prep figure
fig = plt.figure(figsize=(4,5), dpi=80)
grid = plt.GridSpec(3, 1, wspace=0.0, hspace=0.3)
ax1 = plt.subplot(grid[0:2,0])
ax2 = plt.subplot(grid[2,0])
# Simulation Main Loop
for i in range(Nt):
# (1/2) kick
vel += acc * dt/2.0
# drift
pos += vel * dt
# update accelerations
acc = getAcc( pos, mass, G, softening )
# (1/2) kick
vel += acc * dt/2.0
# update time
t += dt
# get energy of system
KE, PE = getEnergy( pos, vel, mass, G )
# save energies, positions for plotting trail
pos_save[:,:,i+1] = pos
KE_save[i+1] = KE
PE_save[i+1] = PE
# plot in real time
if plotRealTime or (i == Nt-1):
plt.sca(ax1)
plt.cla()
xx = pos_save[:,0,max(i-50,0):i+1]
yy = pos_save[:,1,max(i-50,0):i+1]
plt.scatter(xx,yy,s=1,color=[.7,.7,1])
plt.scatter(pos[:,0],pos[:,1],s=10,color='blue')
ax1.set(xlim=(-2, 2), ylim=(-2, 2))
ax1.set_aspect('equal', 'box')
ax1.set_xticks([-2,-1,0,1,2])
ax1.set_yticks([-2,-1,0,1,2])
plt.sca(ax2)
plt.cla()
plt.scatter(t_all,KE_save,color='red',s=1,label='KE' if i == Nt-1 else "")
plt.scatter(t_all,PE_save,color='blue',s=1,label='PE' if i == Nt-1 else "")
plt.scatter(t_all,KE_save+PE_save,color='black',s=1,label='Etot' if i == Nt-1 else "")
ax2.set(xlim=(0, tEnd), ylim=(-300, 300))
ax2.set_aspect(0.007)
plt.pause(0.001)
# add labels/legend
plt.sca(ax2)
plt.xlabel('time')
plt.ylabel('energy')
ax2.legend(loc='upper right')
# Save figure
plt.savefig('nbody.png',dpi=240)
plt.show()
return 0
if __name__== "__main__":
main()