Skip to content

Commit 7fe7295

Browse files
authored
Add algorithm for N-body simulation
1 parent 1367811 commit 7fe7295

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed

other/n-body_simulation.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
"""
2+
In physics and astronomy, a gravitational N-body simulation is a simulation of a
3+
dynamical system of particles under the influence of gravity. The system
4+
consists of a number of bodies, each of which exerts a gravitational force on all
5+
other bodies. These forces are calculated using Newton's law of universal
6+
gravitation. The Euler method is used at each time-step to calculate the change in
7+
velocity and position brought about by these forces. Softening is used to prevent
8+
numerical divergences when a particle comes too close to another (and the force
9+
goes to infinity).
10+
(Description adapted from https://en.wikipedia.org/wiki/N-body_simulation )
11+
(See also http://www.shodor.org/refdesk/Resources/Algorithms/EulersMethod/ )
12+
"""
13+
14+
15+
from __future__ import annotations
16+
17+
import random
18+
19+
from matplotlib import animation # type: ignore
20+
from matplotlib import pyplot as plt
21+
22+
23+
class Body:
24+
def __init__(
25+
self: Body,
26+
position_x: float,
27+
position_y: float,
28+
velocity_x: float,
29+
velocity_y: float,
30+
mass: float = 1,
31+
size: float = 1,
32+
color: str = "blue",
33+
) -> None:
34+
"""
35+
The parameters "size" & "color" are not relevant for the simulation itself,
36+
they are only used for plotting.
37+
"""
38+
self.position_x = position_x
39+
self.position_y = position_y
40+
self.velocity_x = velocity_x
41+
self.velocity_y = velocity_y
42+
self.mass = mass
43+
self.size = size
44+
self.color = color
45+
46+
def update_velocity(
47+
self: Body, force_x: float, force_y: float, delta_time: float
48+
) -> None:
49+
"""
50+
Euler algorithm for velocity
51+
"""
52+
self.velocity_x += force_x * delta_time
53+
self.velocity_y += force_y * delta_time
54+
55+
def update_position(self: Body, delta_time: float) -> None:
56+
"""
57+
Euler algorithm for position
58+
"""
59+
self.position_x += self.velocity_x * delta_time
60+
self.position_y += self.velocity_y * delta_time
61+
62+
63+
class BodySystem:
64+
"""
65+
This class is used to hold the bodies, the gravitation constant, the time
66+
factor and the softening factor. The time factor is used to control the speed
67+
of the simulation. The softening factor is used for softening, a numerical
68+
trick for N-body simulations to prevent numerical divergences when two bodies
69+
get too close to each other.
70+
"""
71+
72+
def __init__(
73+
self: BodySystem,
74+
bodies: list[Body],
75+
gravitation_constant: float = 1,
76+
time_factor: float = 1,
77+
softening_factor: float = 0,
78+
) -> None:
79+
self.bodies = bodies
80+
self.gravitation_constant = gravitation_constant
81+
self.time_factor = time_factor
82+
self.softening_factor = softening_factor
83+
84+
def update_system(self: BodySystem, delta_time: float) -> None:
85+
"""
86+
For each body, loop through all other bodies to calculate the total
87+
force they exert on it. Use that force to update the body's velocity.
88+
"""
89+
for body1 in self.bodies:
90+
force_x = 0.0
91+
force_y = 0.0
92+
for body2 in self.bodies:
93+
if body1 != body2:
94+
dif_x = body2.position_x - body1.position_x
95+
dif_y = body2.position_y - body1.position_y
96+
97+
# Calculation of the distance using Pythagoras's theorem
98+
# Extra factor due to the softening technique
99+
distance = (dif_x ** 2 + dif_y ** 2 + self.softening_factor) ** (
100+
1 / 2
101+
)
102+
103+
# Newton's law of universal gravitation.
104+
force_x += (
105+
self.gravitation_constant * body2.mass * dif_x / distance ** 3
106+
)
107+
force_y += (
108+
self.gravitation_constant * body2.mass * dif_y / distance ** 3
109+
)
110+
111+
# Update the body's velocity once all the force components have been added
112+
body1.update_velocity(force_x, force_y, delta_time * self.time_factor)
113+
114+
# Update the positions only after all the velocities have been updated
115+
for body in self.bodies:
116+
body.update_position(delta_time * self.time_factor)
117+
118+
119+
def plot(
120+
title: str,
121+
bodySystem: BodySystem,
122+
x_start: float = -1,
123+
x_end: float = 1,
124+
y_start: float = -1,
125+
y_end: float = 1,
126+
) -> None:
127+
INTERVAL = 20 # Frame rate of the animation
128+
129+
fig = plt.figure()
130+
fig.canvas.set_window_title(title)
131+
132+
# Set section to be plotted
133+
ax = plt.axes(xlim=(x_start, x_end), ylim=(y_start, y_end))
134+
135+
# Each body is drawn as a patch by the plt-function
136+
patches = []
137+
for body in bodySystem.bodies:
138+
patches.append(
139+
plt.Circle((body.position_x, body.position_y), body.size, fc=body.color)
140+
)
141+
142+
# Function called once at the start of the animation
143+
def init() -> list[patches.Circle]: # type: ignore
144+
axes = plt.gca()
145+
axes.set_aspect("equal")
146+
147+
for patch in patches:
148+
ax.add_patch(patch)
149+
return patches
150+
151+
# Function called at each step of the animation
152+
def animate(i: int) -> list[patches.Circle]: # type: ignore
153+
# Update the positions of the bodies
154+
bodySystem.update_system(INTERVAL)
155+
156+
# Update the positions of the patches
157+
for patch, body in zip(patches, bodySystem.bodies):
158+
patch.center = (body.position_x, body.position_y)
159+
return patches
160+
161+
anim = animation.FuncAnimation( # noqa: F841
162+
fig, animate, init_func=init, interval=INTERVAL, blit=True
163+
)
164+
165+
plt.show()
166+
167+
168+
if __name__ == "__main__":
169+
# Example 1: figure-8 solution to the 3-body-problem
170+
position_x = 0.9700436
171+
position_y = -0.24308753
172+
velocity_x = 0.466203685
173+
velocity_y = 0.43236573
174+
175+
bodies1 = [
176+
Body(position_x, position_y, velocity_x, velocity_y, size=0.2, color="red"),
177+
Body(-position_x, -position_y, velocity_x, velocity_y, size=0.2, color="green"),
178+
Body(0, 0, -2 * velocity_x, -2 * velocity_y, size=0.2, color="blue"),
179+
]
180+
body_system1 = BodySystem(bodies1, time_factor=0.003)
181+
plot("Figure-8 solution to the 3-body-problem", body_system1, -2, 2, -2, 2)
182+
183+
# Example 2: Moon's orbit around the earth
184+
moon_mass = 7.3476e22
185+
earth_mass = 5.972e24
186+
velocity_dif = 1022
187+
earth_moon_distance = 384399000
188+
gravitation_constant = 6.674e-11
189+
190+
# Calculation of the respective velocities so that total impulse is zero,
191+
# i.e. the two bodies together don't move
192+
moon_velocity = earth_mass * velocity_dif / (earth_mass + moon_mass)
193+
earth_velocity = moon_velocity - velocity_dif
194+
195+
moon = Body(-earth_moon_distance, 0, 0, moon_velocity, moon_mass, 10000000, "grey")
196+
earth = Body(0, 0, 0, earth_velocity, earth_mass, 50000000, "blue")
197+
body_system2 = BodySystem([earth, moon], gravitation_constant, time_factor=1000)
198+
plot(
199+
"Moon's orbit around the earth",
200+
body_system2,
201+
-430000000,
202+
430000000,
203+
-430000000,
204+
430000000,
205+
)
206+
207+
# Example 3: Random system with many bodies
208+
bodies = []
209+
for i in range(10):
210+
velocity_x = random.uniform(-0.5, 0.5)
211+
velocity_y = random.uniform(-0.5, 0.5)
212+
213+
# Bodies are created pairwise with opposite velocities so that the
214+
# total impulse remains zero
215+
bodies.append(
216+
Body(
217+
random.uniform(-0.5, 0.5),
218+
random.uniform(-0.5, 0.5),
219+
velocity_x,
220+
velocity_y,
221+
size=0.05,
222+
)
223+
)
224+
bodies.append(
225+
Body(
226+
random.uniform(-0.5, 0.5),
227+
random.uniform(-0.5, 0.5),
228+
-velocity_x,
229+
-velocity_y,
230+
size=0.05,
231+
)
232+
)
233+
body_system3 = BodySystem(bodies, 0.01, 0.01, 0.1)
234+
plot("Random system with many bodies", body_system3, -1.5, 1.5, -1.5, 1.5)

0 commit comments

Comments
 (0)