|
| 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.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 | + 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 | + >>> body = Body(0.,0.,0.,0.) |
| 53 | + >>> body.update_velocity(1.,0.,1.) |
| 54 | + >>> body.velocity_x |
| 55 | + 1.0 |
| 56 | + """ |
| 57 | + self.velocity_x += force_x * delta_time |
| 58 | + self.velocity_y += force_y * delta_time |
| 59 | + |
| 60 | + def update_position(self: Body, delta_time: float) -> None: |
| 61 | + """ |
| 62 | + Euler algorithm for position |
| 63 | +
|
| 64 | + >>> body = Body(0.,0.,1.,0.) |
| 65 | + >>> body.update_position(1.) |
| 66 | + >>> body.position_x |
| 67 | + 1.0 |
| 68 | + """ |
| 69 | + self.position_x += self.velocity_x * delta_time |
| 70 | + self.position_y += self.velocity_y * delta_time |
| 71 | + |
| 72 | + |
| 73 | +class BodySystem: |
| 74 | + """ |
| 75 | + This class is used to hold the bodies, the gravitation constant, the time |
| 76 | + factor and the softening factor. The time factor is used to control the speed |
| 77 | + of the simulation. The softening factor is used for softening, a numerical |
| 78 | + trick for N-body simulations to prevent numerical divergences when two bodies |
| 79 | + get too close to each other. |
| 80 | + """ |
| 81 | + |
| 82 | + def __init__( |
| 83 | + self: BodySystem, |
| 84 | + bodies: list[Body], |
| 85 | + gravitation_constant: float = 1.0, |
| 86 | + time_factor: float = 1.0, |
| 87 | + softening_factor: float = 0.0, |
| 88 | + ) -> None: |
| 89 | + self.bodies = bodies |
| 90 | + self.gravitation_constant = gravitation_constant |
| 91 | + self.time_factor = time_factor |
| 92 | + self.softening_factor = softening_factor |
| 93 | + |
| 94 | + def update_system(self: BodySystem, delta_time: float) -> None: |
| 95 | + """ |
| 96 | + For each body, loop through all other bodies to calculate the total |
| 97 | + force they exert on it. Use that force to update the body's velocity. |
| 98 | +
|
| 99 | + >>> body_system = BodySystem([Body(0,0,0,0), Body(10,0,0,0)]) |
| 100 | + >>> body_system.update_system(1) |
| 101 | + >>> body_system.bodies[0].position_x |
| 102 | + 0.01 |
| 103 | + """ |
| 104 | + for body1 in self.bodies: |
| 105 | + force_x = 0.0 |
| 106 | + force_y = 0.0 |
| 107 | + for body2 in self.bodies: |
| 108 | + if body1 != body2: |
| 109 | + dif_x = body2.position_x - body1.position_x |
| 110 | + dif_y = body2.position_y - body1.position_y |
| 111 | + |
| 112 | + # Calculation of the distance using Pythagoras's theorem |
| 113 | + # Extra factor due to the softening technique |
| 114 | + distance = (dif_x ** 2 + dif_y ** 2 + self.softening_factor) ** ( |
| 115 | + 1 / 2 |
| 116 | + ) |
| 117 | + |
| 118 | + # Newton's law of universal gravitation. |
| 119 | + force_x += ( |
| 120 | + self.gravitation_constant * body2.mass * dif_x / distance ** 3 |
| 121 | + ) |
| 122 | + force_y += ( |
| 123 | + self.gravitation_constant * body2.mass * dif_y / distance ** 3 |
| 124 | + ) |
| 125 | + |
| 126 | + # Update the body's velocity once all the force components have been added |
| 127 | + body1.update_velocity(force_x, force_y, delta_time * self.time_factor) |
| 128 | + |
| 129 | + # Update the positions only after all the velocities have been updated |
| 130 | + for body in self.bodies: |
| 131 | + body.update_position(delta_time * self.time_factor) |
| 132 | + |
| 133 | + |
| 134 | +def plot( |
| 135 | + title: str, |
| 136 | + body_system: BodySystem, |
| 137 | + x_start: float = -1, |
| 138 | + x_end: float = 1, |
| 139 | + y_start: float = -1, |
| 140 | + y_end: float = 1, |
| 141 | +) -> None: |
| 142 | + INTERVAL = 20 # Frame rate of the animation |
| 143 | + DELTA_TIME = INTERVAL / 1000 # Time between time steps in seconds |
| 144 | + |
| 145 | + fig = plt.figure() |
| 146 | + fig.canvas.set_window_title(title) |
| 147 | + |
| 148 | + # Set section to be plotted |
| 149 | + ax = plt.axes(xlim=(x_start, x_end), ylim=(y_start, y_end)) |
| 150 | + |
| 151 | + # Each body is drawn as a patch by the plt-function |
| 152 | + patches = [] |
| 153 | + for body in body_system.bodies: |
| 154 | + patches.append( |
| 155 | + plt.Circle((body.position_x, body.position_y), body.size, fc=body.color) |
| 156 | + ) |
| 157 | + |
| 158 | + # Function called once at the start of the animation |
| 159 | + def init() -> list[patches.Circle]: # type: ignore |
| 160 | + axes = plt.gca() |
| 161 | + axes.set_aspect("equal") |
| 162 | + |
| 163 | + for patch in patches: |
| 164 | + ax.add_patch(patch) |
| 165 | + return patches |
| 166 | + |
| 167 | + # Function called at each step of the animation |
| 168 | + def animate(i: int) -> list[patches.Circle]: # type: ignore |
| 169 | + # Update the positions of the bodies |
| 170 | + body_system.update_system(DELTA_TIME) |
| 171 | + |
| 172 | + # Update the positions of the patches |
| 173 | + for patch, body in zip(patches, body_system.bodies): |
| 174 | + patch.center = (body.position_x, body.position_y) |
| 175 | + return patches |
| 176 | + |
| 177 | + anim = animation.FuncAnimation( # noqa: F841 |
| 178 | + fig, animate, init_func=init, interval=INTERVAL, blit=True |
| 179 | + ) |
| 180 | + |
| 181 | + plt.show() |
| 182 | + |
| 183 | + |
| 184 | +if __name__ == "__main__": |
| 185 | + # Example 1: figure-8 solution to the 3-body-problem |
| 186 | + position_x = 0.9700436 |
| 187 | + position_y = -0.24308753 |
| 188 | + velocity_x = 0.466203685 |
| 189 | + velocity_y = 0.43236573 |
| 190 | + |
| 191 | + bodies1 = [ |
| 192 | + Body(position_x, position_y, velocity_x, velocity_y, size=0.2, color="red"), |
| 193 | + Body(-position_x, -position_y, velocity_x, velocity_y, size=0.2, color="green"), |
| 194 | + Body(0, 0, -2 * velocity_x, -2 * velocity_y, size=0.2, color="blue"), |
| 195 | + ] |
| 196 | + body_system1 = BodySystem(bodies1, time_factor=3) |
| 197 | + plot("Figure-8 solution to the 3-body-problem", body_system1, -2, 2, -2, 2) |
| 198 | + |
| 199 | + # Example 2: Moon's orbit around the earth |
| 200 | + moon_mass = 7.3476e22 |
| 201 | + earth_mass = 5.972e24 |
| 202 | + velocity_dif = 1022 |
| 203 | + earth_moon_distance = 384399000 |
| 204 | + gravitation_constant = 6.674e-11 |
| 205 | + |
| 206 | + # Calculation of the respective velocities so that total impulse is zero, |
| 207 | + # i.e. the two bodies together don't move |
| 208 | + moon_velocity = earth_mass * velocity_dif / (earth_mass + moon_mass) |
| 209 | + earth_velocity = moon_velocity - velocity_dif |
| 210 | + |
| 211 | + moon = Body(-earth_moon_distance, 0, 0, moon_velocity, moon_mass, 10000000, "grey") |
| 212 | + earth = Body(0, 0, 0, earth_velocity, earth_mass, 50000000, "blue") |
| 213 | + body_system2 = BodySystem([earth, moon], gravitation_constant, time_factor=1000000) |
| 214 | + plot( |
| 215 | + "Moon's orbit around the earth", |
| 216 | + body_system2, |
| 217 | + -430000000, |
| 218 | + 430000000, |
| 219 | + -430000000, |
| 220 | + 430000000, |
| 221 | + ) |
| 222 | + |
| 223 | + # Example 3: Random system with many bodies |
| 224 | + bodies = [] |
| 225 | + for i in range(10): |
| 226 | + velocity_x = random.uniform(-0.5, 0.5) |
| 227 | + velocity_y = random.uniform(-0.5, 0.5) |
| 228 | + |
| 229 | + # Bodies are created pairwise with opposite velocities so that the |
| 230 | + # total impulse remains zero |
| 231 | + bodies.append( |
| 232 | + Body( |
| 233 | + random.uniform(-0.5, 0.5), |
| 234 | + random.uniform(-0.5, 0.5), |
| 235 | + velocity_x, |
| 236 | + velocity_y, |
| 237 | + size=0.05, |
| 238 | + ) |
| 239 | + ) |
| 240 | + bodies.append( |
| 241 | + Body( |
| 242 | + random.uniform(-0.5, 0.5), |
| 243 | + random.uniform(-0.5, 0.5), |
| 244 | + -velocity_x, |
| 245 | + -velocity_y, |
| 246 | + size=0.05, |
| 247 | + ) |
| 248 | + ) |
| 249 | + body_system3 = BodySystem(bodies, 0.01, 10, 0.1) |
| 250 | + plot("Random system with many bodies", body_system3, -1.5, 1.5, -1.5, 1.5) |
0 commit comments