import numpy as np import logging logger = logging.getLogger(__name__) def n_body_forces(particles: np.ndarray, G: float, softening: float = 0): """ Computes the gravitational forces between a set of particles. Assumes that the particles array has the following columns: x, y, z, m. """ if particles.shape[1] != 4: raise ValueError("Particles array must have 4 columns: x, y, z, m") x_vec = particles[:, 0:3] masses = particles[:, 3] n = particles.shape[0] forces = np.zeros((n, 3)) logger.debug(f"Computing forces for {n} particles using n^2 algorithm (using {softening=:.2g})") for i in range(n): # the current particle is at x_current x_current = x_vec[i, :] m_current = masses[i] # first compute the displacement to all other particles (and its magnitude) r_vec = x_vec - x_current r = np.linalg.norm(r_vec, axis=1) # add softening to the denominator r_adjusted = r**2 + softening**2 # usually with a square root: r' = sqrt(r^2 + softening^2) and then cubed, but we combine that below # the numerator is tricky: # m is a list of scalars and r_vec is a list of vectors (2D array) # we only want row_wise multiplication num = G * (masses * r_vec.T).T # a zero value is expected where we have the same particle r_adjusted[i] = 1 num[i] = 0 f = - np.sum((num.T / r_adjusted**1.5).T, axis=0) * m_current forces[i] = f if i!= 0 and i % 5000 == 0: logger.debug(f"Particle {i} done") return forces def n_body_forces_basic(particles: np.ndarray, G: float, softening: float = 0): if particles.shape[1] != 4: raise ValueError("Particles array must have 4 columns: x, y, z, m") x_vec = particles[:, 0:3] masses = particles[:, 3] n = particles.shape[0] forces = np.zeros((n, 3)) for i in range(n): for j in range(n): if i == j: continue # keep the value at zero r_vec = x_vec[j] - x_vec[i] r = np.linalg.norm(r_vec) f = - G * masses[i] * masses[j] * r_vec / (r**3 + softening**3) forces[i] += f return forces def analytical_forces(particles: np.ndarray): """ Computes the interparticle forces without computing the n^2 interactions. This is done by using newton's second theorem for a spherical mass distribution. The force on a particle at radius r is simply the force exerted by a point mass with the enclosed mass. Assumes that the particles array has the following columns: x, y, z, m. """ n = particles.shape[0] forces = np.zeros((n, 3)) logger.debug(f"Computing forces for {n} particles using spherical approximation") r_particles = np.linalg.norm(particles[:, :3], axis=1) for i in range(n): r_current = np.linalg.norm(particles[i, 0:3]) m_current = particles[i, 3] m_enclosed = np.sum(particles[r_particles < r_current, 3]) # the force is the same as the force exerted by a point mass at the center f = - m_current * m_enclosed / r_current**2 forces[i] = f if i % 5000 == 0: logger.debug(f"Particle {i} done") return forces