~finished task 1, on to integration
This commit is contained in:
		@@ -223,7 +223,6 @@
 | 
			
		||||
   "source": [
 | 
			
		||||
    "def acceleration(phi, grid, center, end):\n",
 | 
			
		||||
    "    # line between center and end\n",
 | 
			
		||||
    "    # todo center and end are wrong\n",
 | 
			
		||||
    "    direction = (end - center / np.linalg.norm(end - center)).astype(int)\n",
 | 
			
		||||
    "    print(direction)\n",
 | 
			
		||||
    "    points = [center + i * direction for i in range(1, phi.shape[0]//2)]\n",
 | 
			
		||||
@@ -253,7 +252,7 @@
 | 
			
		||||
   "name": "python",
 | 
			
		||||
   "nbconvert_exporter": "python",
 | 
			
		||||
   "pygments_lexer": "ipython3",
 | 
			
		||||
   "version": "3.12.7"
 | 
			
		||||
   "version": "3.13.1"
 | 
			
		||||
  }
 | 
			
		||||
 },
 | 
			
		||||
 "nbformat": 4,
 | 
			
		||||
 
 | 
			
		||||
@@ -35,3 +35,4 @@
 | 
			
		||||
    - plot total energy vs time
 | 
			
		||||
    - plot particle positions?
 | 
			
		||||
- What is the parameter a of the Hernquist model?
 | 
			
		||||
- How to handle zero values for the k vector in the mesh method?
 | 
			
		||||
							
								
								
									
										9914
									
								
								nbody/data/data1_noise.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9914
									
								
								nbody/data/data1_noise.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								nbody/reference/N_Body_project_2022_full.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								nbody/reference/N_Body_project_2022_full.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -60,11 +60,11 @@ def analytical_forces(particles: np.ndarray):
 | 
			
		||||
 | 
			
		||||
    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]
 | 
			
		||||
 | 
			
		||||
        r_particles = np.linalg.norm(particles[:, :3], axis=1)
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,10 @@ def mesh_poisson(mesh: np.ndarray, G: float) -> np.ndarray:
 | 
			
		||||
 | 
			
		||||
#### Version 2 - only storing the scalar potential
 | 
			
		||||
def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callable) -> np.ndarray:
 | 
			
		||||
    """
 | 
			
		||||
    Computes the gravitational force acting on a set of particles using a mesh-based approach.
 | 
			
		||||
    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")
 | 
			
		||||
 | 
			
		||||
@@ -62,9 +66,10 @@ def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callab
 | 
			
		||||
    if logger.level >= logging.DEBUG:
 | 
			
		||||
        show_mesh_information(mesh, "Density mesh")
 | 
			
		||||
 | 
			
		||||
    # compute the potential and its gradient
 | 
			
		||||
    spacing = axis[1] - axis[0]
 | 
			
		||||
    logger.debug(f"Using mesh spacing: {spacing}")
 | 
			
		||||
    phi = mesh_poisson_v2(mesh, G)
 | 
			
		||||
    phi = mesh_poisson_v2(mesh, G, spacing)
 | 
			
		||||
    logger.debug(f"Got phi with: {phi.shape}, {np.max(phi)}")
 | 
			
		||||
    phi_grad = np.stack(np.gradient(phi, spacing), axis=0)
 | 
			
		||||
    if logger.level >= logging.DEBUG:
 | 
			
		||||
@@ -72,6 +77,7 @@ def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callab
 | 
			
		||||
        show_mesh_information(phi_grad[0], "Potential gradient")
 | 
			
		||||
    logger.debug(f"Got phi_grad with: {phi_grad.shape}, {np.max(phi_grad)}")
 | 
			
		||||
    
 | 
			
		||||
    # 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
 | 
			
		||||
@@ -82,16 +88,23 @@ def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callab
 | 
			
		||||
    return forces
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mesh_poisson_v2(mesh: np.ndarray, G: float) -> np.ndarray:
 | 
			
		||||
def mesh_poisson_v2(mesh: np.ndarray, G: float, spacing: float) -> np.ndarray:
 | 
			
		||||
    """
 | 
			
		||||
    Solves the poisson equation for the mesh using the FFT.
 | 
			
		||||
    Returns the scalar potential.
 | 
			
		||||
    """
 | 
			
		||||
    rho_hat = fft.fftn(mesh)
 | 
			
		||||
    k = np.fft.fftfreq(mesh.shape[0])
 | 
			
		||||
    k = fft.fftfreq(mesh.shape[0], spacing)
 | 
			
		||||
    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
 | 
			
		||||
    # avoid division by zero
 | 
			
		||||
    # TODO: review this
 | 
			
		||||
    k_sr[k_sr == 0] = np.inf
 | 
			
		||||
    phi_hat = - 4 * np.pi * G * rho_hat / k_sr
 | 
			
		||||
    # - comes from i squared
 | 
			
		||||
    # TODO: 4pi stays since the backtransform removes the 1/2pi factor
 | 
			
		||||
    phi = np.real(fft.ifftn(phi_hat))
 | 
			
		||||
    return phi
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,3 +66,12 @@ def to_particles(y: np.ndarray) -> np.ndarray:
 | 
			
		||||
    y = y.reshape((n, 7), copy=False, order='F')
 | 
			
		||||
    logger.debug(f"Unflattened array into {y.shape=}")
 | 
			
		||||
    return y
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def runge_kutta_4(y0 : np.ndarray, t : float, f, dt : float):
 | 
			
		||||
    k1 = f(y0, t)
 | 
			
		||||
    k2 = f(y0 + k1/2 * dt, t + dt/2)
 | 
			
		||||
    k3 = f(y0 + k2/2 * dt, t + dt/2)
 | 
			
		||||
    k4 = f(y0 + k3 * dt, t + dt)
 | 
			
		||||
    return y0 + (k1 + 2*k2 + 2*k3 + k4)/6 * dt
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ def seed_scales(r_scale: u.Quantity, m_scale: u.Quantity):
 | 
			
		||||
    global M_SCALE, R_SCALE
 | 
			
		||||
    M_SCALE = m_scale
 | 
			
		||||
    R_SCALE = r_scale
 | 
			
		||||
    logger.info(f"Set scales: M_SCALE = {M_SCALE}, R_SCALE = {R_SCALE}")
 | 
			
		||||
    logger.info(f"Set scales: M_SCALE = {M_SCALE:.2g}, R_SCALE = {R_SCALE:.2g}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def apply_units(columns: np.array, quantity: str):
 | 
			
		||||
@@ -25,17 +25,20 @@ def apply_units(columns: np.array, quantity: str):
 | 
			
		||||
        return columns * M_SCALE
 | 
			
		||||
    elif quantity == "position":
 | 
			
		||||
        return columns * R_SCALE
 | 
			
		||||
    elif quantity == "velocity":
 | 
			
		||||
        return columns * R_SCALE / u.s
 | 
			
		||||
    elif quantity == "time":
 | 
			
		||||
        return columns * u.s
 | 
			
		||||
    elif quantity == "acceleration":
 | 
			
		||||
        return columns * R_SCALE / u.s**2
 | 
			
		||||
    elif quantity == "force":
 | 
			
		||||
        return columns * M_SCALE * R_SCALE / u.s**2
 | 
			
		||||
    elif quantity == "volume":
 | 
			
		||||
        return columns * R_SCALE**3
 | 
			
		||||
    elif quantity == "density":
 | 
			
		||||
        return columns * M_SCALE / R_SCALE**3
 | 
			
		||||
    
 | 
			
		||||
    ## Derived quantities
 | 
			
		||||
    elif quantity == "force":
 | 
			
		||||
        # using F = GMm/R^2 => F = M_SCALE**2 / R_SCALE**2 (G = 1)
 | 
			
		||||
        return columns * M_SCALE**2 / R_SCALE**2
 | 
			
		||||
    elif quantity == "velocity":
 | 
			
		||||
        # using the Virial theorem: v^2 = GM/R => v = sqrt(GM/R) => v = sqrt(M_SCALE / R_SCALE) (G = 1)
 | 
			
		||||
        return columns * np.sqrt(M_SCALE / R_SCALE)
 | 
			
		||||
    elif quantity == "time":
 | 
			
		||||
        # using the dynamical time: t_dyn = 1/sqrt(G*rho) => t_dyn = sqrt(4/3 * pi * R_SCALE**3 / M_SCALE) (G = 1)
 | 
			
		||||
        return columns * np.sqrt(4/3 * np.pi * R_SCALE**3 / M_SCALE)
 | 
			
		||||
    else:
 | 
			
		||||
        raise ValueError(f"Unknown quantity: {quantity}")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user