better structure. most of task 1 done
This commit is contained in:
parent
65c21c6033
commit
77a4959fe2
@ -4,8 +4,16 @@
|
||||
- [ ] Compute characteristic quantities/scales
|
||||
- [x] Compare analytical model and particle density distribution
|
||||
- [ ] Compute forces through nbody simulation
|
||||
- [ ] vary softening length and compare results
|
||||
- [ ] compare with the analytical expectation from Newtons 2nd law
|
||||
- [x] vary softening length and compare results
|
||||
- [x] compare with the analytical expectation from Newtons 2nd law
|
||||
- [ ] compute the relaxation time
|
||||
|
||||
### Task 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Questions
|
||||
- Procedure for each time step of a mesh simulation? Potential on mesh -> forces on particles -> update particle positions -> new mesh potential? or skip the creation of particles in each time step?
|
File diff suppressed because one or more lines are too long
366
nbody/task1.ipynb
Normal file
366
nbody/task1.ipynb
Normal file
File diff suppressed because one or more lines are too long
443
nbody/task2-particle-mesh.ipynb
Normal file
443
nbody/task2-particle-mesh.ipynb
Normal file
File diff suppressed because one or more lines are too long
@ -1,7 +1,14 @@
|
||||
# Import all functions in all the files in the current directory
|
||||
## Import all functions in all the files in the current directory
|
||||
# Basic helpers for interacting with the data
|
||||
from .load import *
|
||||
from .mesh import *
|
||||
from .model import *
|
||||
from .particles import *
|
||||
from .forces import *
|
||||
from .integrate import *
|
||||
|
||||
# Helpers for computing the forces
|
||||
from .forces_basic import *
|
||||
from .forces_tree import *
|
||||
from .forces_mesh import *
|
||||
|
||||
# Helpers for solving the IVP and having time evolution
|
||||
from .integrate import *
|
||||
|
@ -16,7 +16,7 @@ def n_body_forces(particles: np.ndarray, G: float, softening: float = 0):
|
||||
|
||||
n = particles.shape[0]
|
||||
forces = np.zeros((n, 3))
|
||||
logger.debug(f"Computing forces for {n} particles using n^2 algorithm")
|
||||
logger.debug(f"Computing forces for {n} particles using n^2 algorithm (using {softening=})")
|
||||
|
||||
for i in range(n):
|
||||
# the current particle is at x_current
|
||||
@ -41,7 +41,7 @@ def n_body_forces(particles: np.ndarray, G: float, softening: float = 0):
|
||||
f = np.sum((num.T / r_adjusted**1.5).T, axis=0) * m_current
|
||||
forces[i] = -f
|
||||
|
||||
if i % 1000 == 0:
|
||||
if i % 5000 == 0:
|
||||
logger.debug(f"Particle {i} done")
|
||||
|
||||
return forces
|
||||
@ -71,7 +71,7 @@ def analytical_forces(particles: np.ndarray):
|
||||
f = - m_current * m_enclosed / r_current**2
|
||||
forces[i] = f
|
||||
|
||||
if i % 1000 == 0:
|
||||
if i % 5000 == 0:
|
||||
logger.debug(f"Particle {i} done")
|
||||
|
||||
return forces
|
185
nbody/utils/forces_mesh.py
Normal file
185
nbody/utils/forces_mesh.py
Normal file
@ -0,0 +1,185 @@
|
||||
## Implementation of a mesh based force solver
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from scipy import fft
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#### Version 1 - keeping the derivative of phi
|
||||
|
||||
def mesh_forces(particles: np.ndarray, G: float, n_grid: int, mapping: callable) -> np.ndarray:
|
||||
"""
|
||||
Computes the gravitational forces between a set of particles using a mesh.
|
||||
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")
|
||||
|
||||
mesh, axis = to_mesh(particles, n_grid, mapping)
|
||||
# show_mesh_information(mesh, "Initial mesh")
|
||||
|
||||
grad_phi = mesh_poisson(mesh, G)
|
||||
# show_mesh_information(mesh, "Mesh potential")
|
||||
|
||||
# compute the particle forces from the mesh potential
|
||||
forces = np.zeros_like(particles[:, :3])
|
||||
for i, p in enumerate(particles):
|
||||
ijk = np.digitize(p, axis) - 1
|
||||
forces[i] = -grad_phi[ijk[0], ijk[1], ijk[2]] * p[3]
|
||||
|
||||
return forces
|
||||
|
||||
|
||||
def mesh_poisson(mesh: np.ndarray, G: float) -> np.ndarray:
|
||||
"""
|
||||
'Solves' the poisson equation for the mesh using the FFT.
|
||||
Returns the gradient of the potential since this is required for the force computation.
|
||||
"""
|
||||
rho_hat = fft.fftn(mesh)
|
||||
# the laplacian in fourier space takes the form of a multiplication
|
||||
k = np.fft.fftfreq(mesh.shape[0])
|
||||
# TODO: probably need to take the actual mesh bounds into account
|
||||
kx, ky, kz = np.meshgrid(k, k, k)
|
||||
k_vec = np.array([kx, ky, kz])
|
||||
logger.debug(f"Got k_square with: {k_vec.shape}, {np.max(k_vec)} {np.min(k_vec)}")
|
||||
|
||||
grad_phi_hat = 4 * np.pi * G * rho_hat / (1j * k_vec * 2 * np.pi)
|
||||
|
||||
# the inverse fourier transform gives the potential (or its gradient)
|
||||
grad_phi = np.real(fft.ifftn(grad_phi_hat))
|
||||
return grad_phi
|
||||
|
||||
|
||||
#### Version 2 - only storing the scalar potential
|
||||
def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callable) -> np.ndarray:
|
||||
if particles.shape[1] != 4:
|
||||
raise ValueError("Particles array must have 4 columns: x, y, z, m")
|
||||
|
||||
logger.debug(f"Computing forces for {particles.shape[0]} particles using mesh (using mapping={mapping.__name__})")
|
||||
|
||||
mesh, axis = to_mesh(particles, n_grid, mapping)
|
||||
show_mesh_information(mesh, "Mesh")
|
||||
|
||||
spacing = axis[1] - axis[0]
|
||||
logger.debug(f"Using mesh spacing: {spacing}")
|
||||
phi = mesh_poisson_v2(mesh, G)
|
||||
logger.debug(f"Got phi with: {phi.shape}, {np.max(phi)}")
|
||||
phi_grad = np.stack(np.gradient(phi, spacing), axis=0)
|
||||
show_mesh_information(phi, "Mesh potential")
|
||||
show_mesh_information(phi_grad[0], "Mesh potential grad x")
|
||||
logger.debug(f"Got phi_grad with: {phi_grad.shape}, {np.max(phi_grad)}")
|
||||
forces = np.zeros_like(particles[:, :3])
|
||||
for i, p in enumerate(particles):
|
||||
ijk = np.digitize(p, axis) - 1
|
||||
# this gives 4 entries since p[3] the mass is digitized as well -> this is meaningless and we discard it
|
||||
# logger.debug(f"Particle {p} maps to cell {ijk}")
|
||||
forces[i] = - p[3] * phi_grad[..., ijk[0], ijk[1], ijk[2]]
|
||||
|
||||
return forces
|
||||
|
||||
|
||||
def mesh_poisson_v2(mesh: np.ndarray, G: float) -> np.ndarray:
|
||||
rho_hat = fft.fftn(mesh)
|
||||
k = np.fft.fftfreq(mesh.shape[0])
|
||||
kx, ky, kz = np.meshgrid(k, k, k)
|
||||
k_sr = kx**2 + ky**2 + kz**2
|
||||
logger.debug(f"Got k_square with: {k_sr.shape}, {np.max(k_sr)} {np.min(k_sr)}")
|
||||
logger.debug(f"Count of zeros: {np.sum(k_sr == 0)}")
|
||||
k_sr[k_sr == 0] = 1e-10 # Add a small epsilon to avoid division by zero
|
||||
phi_hat = - G * rho_hat / k_sr
|
||||
# 4pi cancels, - comes from i squared
|
||||
phi = np.real(fft.ifftn(phi_hat))
|
||||
return phi
|
||||
|
||||
|
||||
## Helper functions for star mapping
|
||||
def to_mesh(particles: np.ndarray, n_grid: int, mapping: callable) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Maps a list of particles to a of mesh of size n_grid x n_grid x n_grid.
|
||||
Assumes that the particles array has the following columns: x, y, z, ..., m.
|
||||
Uses the mass of the particles and a smoothing function to detemine the contribution to each cell.
|
||||
"""
|
||||
# axis provide an easy way to map the particles to the mesh
|
||||
max_pos = np.max(particles[:, :3])
|
||||
axis = np.linspace(-max_pos, max_pos, n_grid)
|
||||
mesh_grid = np.meshgrid(axis, axis, axis)
|
||||
mesh = np.zeros_like(mesh_grid[0])
|
||||
|
||||
for p in particles:
|
||||
m = p[-1]
|
||||
if logger.level <= logging.DEBUG and m <= 0:
|
||||
logger.warning(f"Particle with negative mass: {p}")
|
||||
# spread the star onto cells through the shape function, taking into account the mass
|
||||
ijks, weights = mapping(p, axis)
|
||||
for ijk, weight in zip(ijks, weights):
|
||||
mesh[ijk[0], ijk[1], ijk[2]] += weight * m
|
||||
|
||||
return mesh, axis
|
||||
|
||||
|
||||
def particle_to_cells_nn(particle, axis):
|
||||
# find the single cell that contains the particle
|
||||
ijk = np.digitize(particle, axis) - 1
|
||||
# the weight is obviously 1
|
||||
return [ijk], [1]
|
||||
|
||||
|
||||
def particle_to_cells_cic(particle, axis, width):
|
||||
# create a virtual cell around the particle
|
||||
cell_bounds = [
|
||||
particle + np.array([1, 0, 0]) * width,
|
||||
particle + np.array([-1, 0, 0]) * width,
|
||||
particle + np.array([1, 1, 0]) * width,
|
||||
particle + np.array([-1, -1, 0]) * width,
|
||||
particle + np.array([1, 1, 1]) * width,
|
||||
particle + np.array([-1, -1, 1]) * width,
|
||||
particle + np.array([1, 1, -1]) * width,
|
||||
particle + np.array([-1, -1, -1]) * width,
|
||||
]
|
||||
# find all the cells that intersect with the virtual cell
|
||||
ijks = []
|
||||
weights = []
|
||||
for b in cell_bounds:
|
||||
w = np.linalg.norm(particle - b)
|
||||
ijk = np.digitize(b, axis) - 1
|
||||
# print(f"b: {b}, ijk: {ijk}")
|
||||
ijks.append(ijk)
|
||||
weights.append(w)
|
||||
# ensure that the weights sum to 1
|
||||
weights = np.array(weights)
|
||||
weights /= np.sum(weights)
|
||||
return ijks, weights
|
||||
|
||||
|
||||
|
||||
## Helper functions for mesh plotting
|
||||
def show_mesh_information(mesh: np.ndarray, name: str):
|
||||
print(f"Mesh information for {name}")
|
||||
print(f"Total mapped mass: {np.sum(mesh):.0f}")
|
||||
print(f"Max cell value: {np.max(mesh)}")
|
||||
print(f"Min cell value: {np.min(mesh)}")
|
||||
print(f"Mean cell value: {np.mean(mesh)}")
|
||||
plot_3d(mesh, name)
|
||||
plot_2d(mesh, name)
|
||||
|
||||
|
||||
def plot_3d(mesh: np.ndarray, name: str):
|
||||
fig = plt.figure()
|
||||
fig.suptitle(name)
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
ax.scatter(*np.where(mesh), c=mesh[np.where(mesh)], cmap='viridis')
|
||||
plt.show()
|
||||
|
||||
|
||||
def plot_2d(mesh: np.ndarray, name: str):
|
||||
fig = plt.figure()
|
||||
fig.suptitle(name)
|
||||
axs = fig.subplots(1, 3)
|
||||
axs[0].imshow(np.sum(mesh, axis=0))
|
||||
axs[0].set_title("Flattened in x")
|
||||
axs[1].imshow(np.sum(mesh, axis=1))
|
||||
axs[1].set_title("Flattened in y")
|
||||
axs[2].imshow(np.sum(mesh, axis=2))
|
||||
axs[2].set_title("Flattened in z")
|
||||
plt.show()
|
0
nbody/utils/forces_tree.py
Normal file
0
nbody/utils/forces_tree.py
Normal file
15
nbody/utils/logging_config.py
Normal file
15
nbody/utils/logging_config.py
Normal file
@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
## set logging level
|
||||
# level=logging.INFO,
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
# silence some debug messages
|
||||
logging.getLogger('matplotlib.font_manager').setLevel(logging.WARNING)
|
||||
logging.getLogger('matplotlib.ticker').setLevel(logging.WARNING)
|
||||
logging.getLogger('matplotlib.pyplot').setLevel(logging.WARNING)
|
Loading…
x
Reference in New Issue
Block a user