Skip to content

Commit 3332900

Browse files
algobytewisegithub-actionsdhruvmanilacclauss
authored
Add algorithm for N-body simulation - retry (TheAlgorithms#4298)
* add n_body_simulation.py * updating DIRECTORY.md * Rename other/n_body_simulation.py to physics/n_body_simulation.py * updating DIRECTORY.md * Update build.yml * refactor examples & add doctests * removed type-hints from self-parameter * Apply suggestions from code review * Update physics/n_body_simulation.py * Update physics/n_body_simulation.py * Update physics/n_body_simulation.py * Don't forget self * Fix velocity Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Co-authored-by: Dhruv Manilawala <[email protected]> Co-authored-by: Christian Clauss <[email protected]>
1 parent 3a50904 commit 3332900

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-0
lines changed

DIRECTORY.md

+3
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@
551551
* [Sdes](https://github.com/TheAlgorithms/Python/blob/master/other/sdes.py)
552552
* [Tower Of Hanoi](https://github.com/TheAlgorithms/Python/blob/master/other/tower_of_hanoi.py)
553553

554+
## Physics
555+
* [N Body Simulation](https://github.com/TheAlgorithms/Python/blob/master/physics/n_body_simulation.py)
556+
554557
## Project Euler
555558
* Problem 001
556559
* [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_001/sol1.py)

physics/n_body_simulation.py

+348
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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
20+
from matplotlib import pyplot as plt
21+
22+
23+
class Body:
24+
def __init__(
25+
self,
26+
position_x: float,
27+
position_y: float,
28+
velocity_x: float,
29+
velocity_y: float,
30+
mass: float = 1.0,
31+
size: float = 1.0,
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+
@property
47+
def position(self) -> tuple[float, float]:
48+
return self.position_x, self.position_y
49+
50+
@property
51+
def velocity(self) -> tuple[float, float]:
52+
return self.velocity_x, self.velocity_y
53+
54+
def update_velocity(
55+
self, force_x: float, force_y: float, delta_time: float
56+
) -> None:
57+
"""
58+
Euler algorithm for velocity
59+
60+
>>> body_1 = Body(0.,0.,0.,0.)
61+
>>> body_1.update_velocity(1.,0.,1.)
62+
>>> body_1.velocity
63+
(1.0, 0.0)
64+
65+
>>> body_1.update_velocity(1.,0.,1.)
66+
>>> body_1.velocity
67+
(2.0, 0.0)
68+
69+
>>> body_2 = Body(0.,0.,5.,0.)
70+
>>> body_2.update_velocity(0.,-10.,10.)
71+
>>> body_2.velocity
72+
(5.0, -100.0)
73+
74+
>>> body_2.update_velocity(0.,-10.,10.)
75+
>>> body_2.velocity
76+
(5.0, -200.0)
77+
"""
78+
self.velocity_x += force_x * delta_time
79+
self.velocity_y += force_y * delta_time
80+
81+
def update_position(self, delta_time: float) -> None:
82+
"""
83+
Euler algorithm for position
84+
85+
>>> body_1 = Body(0.,0.,1.,0.)
86+
>>> body_1.update_position(1.)
87+
>>> body_1.position
88+
(1.0, 0.0)
89+
90+
>>> body_1.update_position(1.)
91+
>>> body_1.position
92+
(2.0, 0.0)
93+
94+
>>> body_2 = Body(10.,10.,0.,-2.)
95+
>>> body_2.update_position(1.)
96+
>>> body_2.position
97+
(10.0, 8.0)
98+
99+
>>> body_2.update_position(1.)
100+
>>> body_2.position
101+
(10.0, 6.0)
102+
"""
103+
self.position_x += self.velocity_x * delta_time
104+
self.position_y += self.velocity_y * delta_time
105+
106+
107+
class BodySystem:
108+
"""
109+
This class is used to hold the bodies, the gravitation constant, the time
110+
factor and the softening factor. The time factor is used to control the speed
111+
of the simulation. The softening factor is used for softening, a numerical
112+
trick for N-body simulations to prevent numerical divergences when two bodies
113+
get too close to each other.
114+
"""
115+
116+
def __init__(
117+
self,
118+
bodies: list[Body],
119+
gravitation_constant: float = 1.0,
120+
time_factor: float = 1.0,
121+
softening_factor: float = 0.0,
122+
) -> None:
123+
self.bodies = bodies
124+
self.gravitation_constant = gravitation_constant
125+
self.time_factor = time_factor
126+
self.softening_factor = softening_factor
127+
128+
def __len__(self) -> int:
129+
return len(self.bodies)
130+
131+
def update_system(self, delta_time: float) -> None:
132+
"""
133+
For each body, loop through all other bodies to calculate the total
134+
force they exert on it. Use that force to update the body's velocity.
135+
136+
>>> body_system_1 = BodySystem([Body(0,0,0,0), Body(10,0,0,0)])
137+
>>> len(body_system_1)
138+
2
139+
>>> body_system_1.update_system(1)
140+
>>> body_system_1.bodies[0].position
141+
(0.01, 0.0)
142+
>>> body_system_1.bodies[0].velocity
143+
(0.01, 0.0)
144+
145+
>>> body_system_2 = BodySystem([Body(-10,0,0,0), Body(10,0,0,0, mass=4)], 1, 10)
146+
>>> body_system_2.update_system(1)
147+
>>> body_system_2.bodies[0].position
148+
(-9.0, 0.0)
149+
>>> body_system_2.bodies[0].velocity
150+
(0.1, 0.0)
151+
"""
152+
for body1 in self.bodies:
153+
force_x = 0.0
154+
force_y = 0.0
155+
for body2 in self.bodies:
156+
if body1 != body2:
157+
dif_x = body2.position_x - body1.position_x
158+
dif_y = body2.position_y - body1.position_y
159+
160+
# Calculation of the distance using Pythagoras's theorem
161+
# Extra factor due to the softening technique
162+
distance = (dif_x ** 2 + dif_y ** 2 + self.softening_factor) ** (
163+
1 / 2
164+
)
165+
166+
# Newton's law of universal gravitation.
167+
force_x += (
168+
self.gravitation_constant * body2.mass * dif_x / distance ** 3
169+
)
170+
force_y += (
171+
self.gravitation_constant * body2.mass * dif_y / distance ** 3
172+
)
173+
174+
# Update the body's velocity once all the force components have been added
175+
body1.update_velocity(force_x, force_y, delta_time * self.time_factor)
176+
177+
# Update the positions only after all the velocities have been updated
178+
for body in self.bodies:
179+
body.update_position(delta_time * self.time_factor)
180+
181+
182+
def update_step(
183+
body_system: BodySystem, delta_time: float, patches: list[plt.Circle]
184+
) -> None:
185+
"""
186+
Updates the body-system and applies the change to the patch-list used for plotting
187+
188+
>>> body_system_1 = BodySystem([Body(0,0,0,0), Body(10,0,0,0)])
189+
>>> patches_1 = [plt.Circle((body.position_x, body.position_y), body.size,
190+
... fc=body.color)for body in body_system_1.bodies] #doctest: +ELLIPSIS
191+
>>> update_step(body_system_1, 1, patches_1)
192+
>>> patches_1[0].center
193+
(0.01, 0.0)
194+
195+
>>> body_system_2 = BodySystem([Body(-10,0,0,0), Body(10,0,0,0, mass=4)], 1, 10)
196+
>>> patches_2 = [plt.Circle((body.position_x, body.position_y), body.size,
197+
... fc=body.color)for body in body_system_2.bodies] #doctest: +ELLIPSIS
198+
>>> update_step(body_system_2, 1, patches_2)
199+
>>> patches_2[0].center
200+
(-9.0, 0.0)
201+
"""
202+
# Update the positions of the bodies
203+
body_system.update_system(delta_time)
204+
205+
# Update the positions of the patches
206+
for patch, body in zip(patches, body_system.bodies):
207+
patch.center = (body.position_x, body.position_y)
208+
209+
210+
def plot(
211+
title: str,
212+
body_system: BodySystem,
213+
x_start: float = -1,
214+
x_end: float = 1,
215+
y_start: float = -1,
216+
y_end: float = 1,
217+
) -> None:
218+
"""
219+
Utility function to plot how the given body-system evolves over time.
220+
No doctest provided since this function does not have a return value.
221+
"""
222+
223+
INTERVAL = 20 # Frame rate of the animation
224+
DELTA_TIME = INTERVAL / 1000 # Time between time steps in seconds
225+
226+
fig = plt.figure()
227+
fig.canvas.set_window_title(title)
228+
ax = plt.axes(
229+
xlim=(x_start, x_end), ylim=(y_start, y_end)
230+
) # Set section to be plotted
231+
plt.gca().set_aspect("equal") # Fix aspect ratio
232+
233+
# Each body is drawn as a patch by the plt-function
234+
patches = [
235+
plt.Circle((body.position_x, body.position_y), body.size, fc=body.color)
236+
for body in body_system.bodies
237+
]
238+
239+
for patch in patches:
240+
ax.add_patch(patch)
241+
242+
# Function called at each step of the animation
243+
def update(frame: int) -> list[plt.Circle]:
244+
update_step(body_system, DELTA_TIME, patches)
245+
return patches
246+
247+
anim = animation.FuncAnimation( # noqa: F841
248+
fig, update, interval=INTERVAL, blit=True
249+
)
250+
251+
plt.show()
252+
253+
254+
def example_1() -> BodySystem:
255+
"""
256+
Example 1: figure-8 solution to the 3-body-problem
257+
This example can be seen as a test of the implementation: given the right
258+
initial conditions, the bodies should move in a figure-8.
259+
(initial conditions taken from http://www.artcompsci.org/vol_1/v1_web/node56.html)
260+
>>> body_system = example_1()
261+
>>> len(body_system)
262+
3
263+
"""
264+
265+
position_x = 0.9700436
266+
position_y = -0.24308753
267+
velocity_x = 0.466203685
268+
velocity_y = 0.43236573
269+
270+
bodies1 = [
271+
Body(position_x, position_y, velocity_x, velocity_y, size=0.2, color="red"),
272+
Body(-position_x, -position_y, velocity_x, velocity_y, size=0.2, color="green"),
273+
Body(0, 0, -2 * velocity_x, -2 * velocity_y, size=0.2, color="blue"),
274+
]
275+
return BodySystem(bodies1, time_factor=3)
276+
277+
278+
def example_2() -> BodySystem:
279+
"""
280+
Example 2: Moon's orbit around the earth
281+
This example can be seen as a test of the implementation: given the right
282+
initial conditions, the moon should orbit around the earth as it actually does.
283+
(mass, velocity and distance taken from https://en.wikipedia.org/wiki/Earth
284+
and https://en.wikipedia.org/wiki/Moon)
285+
No doctest provided since this function does not have a return value.
286+
"""
287+
288+
moon_mass = 7.3476e22
289+
earth_mass = 5.972e24
290+
velocity_dif = 1022
291+
earth_moon_distance = 384399000
292+
gravitation_constant = 6.674e-11
293+
294+
# Calculation of the respective velocities so that total impulse is zero,
295+
# i.e. the two bodies together don't move
296+
moon_velocity = earth_mass * velocity_dif / (earth_mass + moon_mass)
297+
earth_velocity = moon_velocity - velocity_dif
298+
299+
moon = Body(-earth_moon_distance, 0, 0, moon_velocity, moon_mass, 10000000, "grey")
300+
earth = Body(0, 0, 0, earth_velocity, earth_mass, 50000000, "blue")
301+
return BodySystem([earth, moon], gravitation_constant, time_factor=1000000)
302+
303+
304+
def example_3() -> BodySystem:
305+
"""
306+
Example 3: Random system with many bodies.
307+
No doctest provided since this function does not have a return value.
308+
"""
309+
310+
bodies = []
311+
for i in range(10):
312+
velocity_x = random.uniform(-0.5, 0.5)
313+
velocity_y = random.uniform(-0.5, 0.5)
314+
315+
# Bodies are created pairwise with opposite velocities so that the
316+
# total impulse remains zero
317+
bodies.append(
318+
Body(
319+
random.uniform(-0.5, 0.5),
320+
random.uniform(-0.5, 0.5),
321+
velocity_x,
322+
velocity_y,
323+
size=0.05,
324+
)
325+
)
326+
bodies.append(
327+
Body(
328+
random.uniform(-0.5, 0.5),
329+
random.uniform(-0.5, 0.5),
330+
-velocity_x,
331+
-velocity_y,
332+
size=0.05,
333+
)
334+
)
335+
return BodySystem(bodies, 0.01, 10, 0.1)
336+
337+
338+
if __name__ == "__main__":
339+
plot("Figure-8 solution to the 3-body-problem", example_1(), -2, 2, -2, 2)
340+
plot(
341+
"Moon's orbit around the earth",
342+
example_2(),
343+
-430000000,
344+
430000000,
345+
-430000000,
346+
430000000,
347+
)
348+
plot("Random system with many bodies", example_3(), -1.5, 1.5, -1.5, 1.5)

0 commit comments

Comments
 (0)