n square solver orking

This commit is contained in:
Remy Moll 2025-01-22 17:21:22 +01:00
parent 8737441fbd
commit 65b893b41a
6 changed files with 529 additions and 100 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,23 +23,24 @@ def n_body_forces(particles: np.ndarray, G: float, softening: float = 0):
x_current = x_vec[i, :]
m_current = masses[i]
# first compute the displacement to all other particles
displacements = x_vec - x_current
# and its magnitude
r = np.linalg.norm(displacements, axis=1)
# 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
# the numerator is tricky:
# m is a list of scalars and displacements is a list of vectors (2D array)
# we only want row_wise multiplication
num = G * (masses * displacements.T).T
# 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
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")
@ -66,6 +67,7 @@ def n_body_forces_basic(particles: np.ndarray, G: float, softening: float = 0):
return forces
def analytical_forces(particles: np.ndarray):
"""
Computes the interparticle forces without computing the n^2 interactions.

View File

@ -90,7 +90,7 @@ def mesh_forces_v2(particles: np.ndarray, G: float, n_grid: int, mapping: callab
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]] / 10
forces[i] = - p[3] * phi_grad[..., ijk[0], ijk[1], ijk[2]]
# TODO remove factor of 10
# TODO could also index phi_grad the other way around?
@ -141,8 +141,6 @@ def to_mesh(particles: np.ndarray, n_grid: int, mapping: callable) -> tuple[np.n
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):
@ -157,28 +155,31 @@ def particle_to_cells_nn(particle, axis):
# the weight is obviously 1
return [ijk], [1]
bbox = np.array([
[1, 0, 0],
[-1, 0, 0],
[1, 1, 0],
[-1, -1, 0],
[1, 1, 1],
[-1, -1, 1],
[1, 1, -1],
[-1, -1, -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,
]
# create a virtual cell around the particle and check the intersections
bounding_cell = particle + width * bbox
# find all the cells that intersect with the virtual cell
ijks = []
weights = []
for b in cell_bounds:
for b in bounding_cell:
# TODO: this is not the correct weight
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)

View File

@ -18,7 +18,7 @@ def ode_setup(particles: np.ndarray, force_function: callable) -> tuple[np.ndarr
# for the integrators we need to flatten array which contains 7 columns for now
# we don't really care how we reshape as long as we unflatten consistently
particles = particles.reshape(-1, copy=False, order='A')
particles = particles.flatten()
logger.debug(f"Reshaped 7 columns into {particles.shape=}")
def f(y, t):
@ -26,45 +26,45 @@ def ode_setup(particles: np.ndarray, force_function: callable) -> tuple[np.ndarr
Computes the right hand side of the ODE system.
The ODE system is linearized around the current positions and velocities.
"""
y = to_particles(y)
# now y has shape (n, 7), with columns x, y, z, vx, vy, vz, m
p = to_particles(y)
# this is explicitly a copy, which has shape (n, 7)
# columns x, y, z, vx, vy, vz, m
# (need to keep y intact since integrators make multiple function calls)
forces = force_function(y[:, [0, 1, 2, -1]])
forces = force_function(p[:, [0, 1, 2, -1]])
# compute the accelerations
masses = y[:, -1]
masses = p[:, -1]
a = forces / masses[:, None]
# the [:, None] is to force broadcasting in order to divide each row of forces by the corresponding mass
dydt = np.zeros_like(y)
# the position columns become the velocities
# the velocity columns become the accelerations
dydt[:, 0:3] = y[:, 3:6]
dydt[:, 3:6] = a
p[:, 0:3] = p[:, 3:6]
p[:, 3:6] = a
# the masses remain unchanged
dydt[:, -1] = masses
# p[:, -1] = p[:, -1]
# flatten the array again
# logger.debug(f"As particles: {y}")
dydt = dydt.reshape(-1, copy=False, order='A')
p = p.reshape(-1, copy=False)
# logger.debug(f"As column: {y}")
return dydt
return p
return particles, f
def to_particles(y: np.ndarray) -> np.ndarray:
"""
Converts the 1D array y into a 2D array
Converts the 1D array y into a 2D array, by creating a copy
The new shape is (n, 7) where n is the number of particles.
The columns are x, y, z, vx, vy, vz, m
"""
if y.size % 7 != 0:
raise ValueError("The array y should be inflatable to 7 columns")
n = y.size // 7
y = y.reshape((n, 7), copy=False, order='F')
y = y.reshape((-1, 7), copy=True)
# logger.debug(f"Unflattened array into {y.shape=}")
return y
@ -76,7 +76,7 @@ def to_particles_3d(y: np.ndarray) -> np.ndarray:
"""
n_steps = y.shape[0]
n_particles = y.shape[1] // 7
y = y.reshape((n_steps, n_particles, 7), copy=False, order='F')
y = y.reshape((n_steps, n_particles, 7))
# logger.debug(f"Unflattened array into {y.shape=}")
return y

View File

@ -27,6 +27,7 @@ def density_distribution(r_bins: np.ndarray, particles: np.ndarray, ret_error: b
if ret_error:
count = np.count_nonzero(mask)
if count > 0:
# the absolute error is the square root of the number of particles
error_relative[i] = 1 / np.sqrt(count)
else:
error_relative[i] = 0