Serial Nbody Simulations#

[1]:
import os
from os import path
from time import time

import numpy as np

from astropy import units as u
from astropy import constants as const
[2]:
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as patches
plt.rcParams['figure.figsize'] = [6, 6]
plt.rcParams['image.origin'] = 'lower'
plt.rcParams['font.size'] = 12
[3]:
import peytonites
from peytonites import (
    Distribution, SimState,
    kpc_to_cm, cm_to_kpc,
    lyr_to_cm, cm_to_lyr,
    au_to_cm, cm_to_au
)

Inital Conditions#

Load Inital Condition#

[4]:
sim_init_cond = SimState.read('data/solar_system_10000step_init.dat')

Traditional Simulation#

[5]:
def traditional_simulation(sim_init_cond, out_dir, verbose=True):

    G = sim_init_cond.G # cm^3 / (g s^2)
    dt = sim_init_cond.dt
    nsteps = sim_init_cond.nsteps
    out_interval = sim_init_cond.out_interval
    soft = sim_init_cond.soft

    init_dist = sim_init_cond.distribution

    number_particles = init_dist.N

    x_arr = init_dist.x.copy()
    y_arr = init_dist.y.copy()
    z_arr = init_dist.z.copy()

    vx_arr = init_dist.vx.copy()
    vy_arr = init_dist.vy.copy()
    vz_arr = init_dist.vz.copy()

    mass_arr = init_dist.m.copy()
    #----------------------------------------
    for step in range(nsteps):
        ax_arr = np.zeros_like(x_arr)
        ay_arr = np.zeros_like(x_arr)
        az_arr = np.zeros_like(x_arr)

        for i in range(number_particles):
            ax, ay, az = 0., 0., 0.
            xi, yi, zi = x_arr[i], y_arr[i], z_arr[i]

            for j in range(number_particles):
                if i == j:
                    continue

                dx = xi - x_arr[j]
                dy = yi - y_arr[j]
                dz = zi - z_arr[j]

                r_ij_squared = dx**2 + dy**2 + dz**2 + soft**2
                r_ij = np.sqrt(r_ij_squared)
                r_ij_cubed = r_ij_squared * r_ij

                mj = mass_arr[j]
                a = - (G * mj) / r_ij_cubed

                ax += a * dx
                ay += a * dy
                az += a * dz

            ax_arr[i] = ax
            ay_arr[i] = ay
            az_arr[i] = az


        for i in range(number_particles):

            vx_arr[i] += ax_arr[i] * dt
            vy_arr[i] += ay_arr[i] * dt
            vz_arr[i] += az_arr[i] * dt


            x_arr[i] += vx_arr[i] * dt
            y_arr[i] += vy_arr[i] * dt
            z_arr[i] += vz_arr[i] * dt
    #----------------------------------------

        if step % out_interval == 0:
            if verbose:
                print(step)

            step_params = sim_init_cond.copy()
            if step > 0:
                step_dist = Distribution.from_arrays(
                    x_arr, y_arr, z_arr,
                    vx_arr, vy_arr, vz_arr,
                    mass_arr, name=init_dist.name)

                step_params = sim_init_cond.copy()
                step_params.distribution = step_dist

            step_filename = 'step_{:08d}.dat'.format(step)
            step_path = path.join(out_dir, step_filename)

            if not os.path.isdir(out_dir):
                os.makedirs(out_dir)

            step_params.write(step_path)

    if verbose:
        print(step)
    return

Define Output Dir#

Make sure to have a simout in the name for git ignore

[6]:
out_dir = './solar_system_simout'
[7]:
tstart = time()
traditional_simulation(sim_init_cond, out_dir, verbose=False)
tend = time()

traditional_run_time = (tend-tstart)*u.s
print('done', 'traditional_run_time:', traditional_run_time)
done traditional_run_time: 2.7658839225769043 s

Vector Simulation#

Running this will replace/overwrite the last outputs

Using vectorized operations in this implementation significantly reduces computation time compared to the double for-loop, but at the cost of massively increased memory usage due to storing large intermediate arrays for pairwise distances and accelerations.

[8]:
def vector_simulation(sim_init_cond, out_dir, verbose=True):

    G = sim_init_cond.G # cm^3 / (g s^2)
    dt = sim_init_cond.dt
    nsteps = sim_init_cond.nsteps
    out_interval = sim_init_cond.out_interval
    soft = sim_init_cond.soft

    init_dist = sim_init_cond.distribution

    number_particles = init_dist.N

    x_arr = init_dist.x.copy()
    y_arr = init_dist.y.copy()
    z_arr = init_dist.z.copy()

    vx_arr = init_dist.vx.copy()
    vy_arr = init_dist.vy.copy()
    vz_arr = init_dist.vz.copy()

    mass_arr = init_dist.m.copy()
    #----------------------------------------
    for step in range(nsteps):
        dx = x_arr[:, np.newaxis] - x_arr
        dy = y_arr[:, np.newaxis] - y_arr
        dz = z_arr[:, np.newaxis] - z_arr

        # Avoid division by zero by adding softening length
        r_squared = dx**2 + dy**2 + dz**2 + soft**2
        np.fill_diagonal(r_squared, 1)  # Avoid self-interaction

        r = np.sqrt(r_squared)
        r_cubed = r_squared * r

        # Compute accelerations
        a = -G * mass_arr / r_cubed
        np.fill_diagonal(a, 0)
        ax_arr = np.sum(a * dx, axis=1)
        ay_arr = np.sum(a * dy, axis=1)
        az_arr = np.sum(a * dz, axis=1)


        # Update velocities and positions
        vx_arr += ax_arr * dt
        vy_arr += ay_arr * dt
        vz_arr += az_arr * dt

        x_arr += vx_arr * dt
        y_arr += vy_arr * dt
        z_arr += vz_arr * dt
    #----------------------------------------

        if step % out_interval == 0:
            if verbose:
                print(step)

            step_params = sim_init_cond.copy()
            if step > 0:
                step_dist = Distribution.from_arrays(
                    x_arr, y_arr, z_arr,
                    vx_arr, vy_arr, vz_arr,
                    mass_arr, name=init_dist.name)

                step_params = sim_init_cond.copy()
                step_params.distribution = step_dist

            step_filename = 'step_{:08d}.dat'.format(step)
            step_path = path.join(out_dir, step_filename)

            if not os.path.isdir(out_dir):
                os.makedirs(out_dir)

            step_params.write(step_path)

    #----------------------------------------
    if verbose:
        print(step)
    return
[9]:
tstart = time()
vector_simulation(sim_init_cond, out_dir, verbose=False)
tend = time()

vector_run_time = (tend-tstart)*u.s
print('done', 'vector_run_time', vector_run_time)
done vector_run_time 0.6875908374786377 s

Visualization#

By “Hand”#

[10]:
file_path = path.join(out_dir, 'step_00001500.dat')

# Load simulation state
step_state = SimState.read(file_path)

# Get particle distribution
dist = step_state.distribution

# Plot x and y in cm
plt.scatter(dist.x, dist.y)
plt.title('Quick Plot')
plt.show()

# Plot x and y in AU
# (you can also use astropy units)
plt.scatter(cm_to_au(dist.x), cm_to_au(dist.y), c='k')

# Make the sun a star
plt.scatter(cm_to_au(dist.x[0]), cm_to_au(dist.y[0]),
            marker='*', c='orange', s=100, label='sun')

plt.xlim(-38, 38)
plt.ylim(-38, 38)
plt.title('Solar System in AU')
plt.legend()

plt.show()
_images/serial_nbody_16_0.png
_images/serial_nbody_16_1.png

2D GIF#

This gif will be saved in the out_dir. Its kind of slow but does the job!

[11]:
peytonites.simulation_to_gif_2d(
    out_dir,
    gif_filename='solar_sys_2d.gif',
    extent=38*u.AU,
    unit='AU'
)
_images/serial_nbody_18_0.png

im1

3D GIF#

This gif will be saved in the out_dir. Its kind of slow but does the job!

[12]:
peytonites.simulation_to_gif_3d(out_dir, gif_filename='solar_sys_3d.gif', extent=38*u.AU, unit='AU')
_images/serial_nbody_21_0.png

im2