90 lines
3.0 KiB
Python
90 lines
3.0 KiB
Python
import numpy as np
|
|
import scipy.integrate as spi
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def ode_setup(particles: np.ndarray, force_function: callable) -> tuple[np.ndarray, callable]:
|
|
"""
|
|
Linearizes the ODE system for the particles interacting gravitationally.
|
|
Returns:
|
|
- the Y0 array corresponding to the initial conditions (x0 and v0)
|
|
- the function that computes the right hand side of the ODE with function signature f(t, y)
|
|
Assumes that the particles array has the following columns: x, y, z, vx, vy, vz, m.
|
|
"""
|
|
if particles.shape[1] != 7:
|
|
raise ValueError("Particles array must have 7 columns: x, y, z, vx, vy, vz, m")
|
|
|
|
# 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')
|
|
logger.debug(f"Reshaped 7 columns into {particles.shape=}")
|
|
|
|
def f(y, t):
|
|
"""
|
|
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
|
|
|
|
forces = force_function(y[:, [0, 1, 2, -1]])
|
|
|
|
# compute the accelerations
|
|
masses = y[:, -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
|
|
# the masses remain unchanged
|
|
dydt[:, -1] = masses
|
|
|
|
# flatten the array again
|
|
# logger.debug(f"As particles: {y}")
|
|
dydt = dydt.reshape(-1, copy=False, order='A')
|
|
|
|
# logger.debug(f"As column: {y}")
|
|
return dydt
|
|
|
|
return particles, f
|
|
|
|
|
|
def to_particles(y: np.ndarray) -> np.ndarray:
|
|
"""
|
|
Converts the 1D array y into a 2D array
|
|
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')
|
|
# logger.debug(f"Unflattened array into {y.shape=}")
|
|
return y
|
|
|
|
|
|
def to_particles_3d(y: np.ndarray) -> np.ndarray:
|
|
"""
|
|
Converts the 2D sol array with one vector per timestep into a 3D array:
|
|
2d particles (nx7) x nsteps
|
|
"""
|
|
n_steps = y.shape[0]
|
|
n_particles = y.shape[1] // 7
|
|
y = y.reshape((n_steps, n_particles, 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
|