Spaces:
Build error
Build error
File size: 7,587 Bytes
6b6806c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
from typing import Any
import numpy as np
import astropy.constants as c
import time
# Adapted from Python for Astronomers: An Introduction to Scientific Computing
# by Imad Pasha & Christopher Agostino
# https://prappleizer.github.io/Tutorials/RK4/RK4_Tutorial.html
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
# http://creativecommons.org/licenses/by-nc-sa/4.0/
class Body:
def __init__(self, mass, x_vec, v_vec, name=None, has_units=True):
"""
spawn instance of the Body class, which is used in Simulations.
:param: mass | mass of particle. if has_units=True, an Astropy Quantity, otherwise a float
:param: x_vec | a vector len(3) containing the x, y, z initial positions of the body.
the array can be unitless if has_units=False, or be of the form np.array([0,0,0])*u.km
:param: v_vec | vector len(3) containing the v_x, v_y, v_z initial velocities of the body.
:param: name | string containing a name, used for plotting later
:param: has_units | defines how the code treats the problem, as unit-ed, or unitless.
"""
self.name = name
self.has_units = has_units
if self.has_units:
self.mass = mass.cgs
self.x_vec = x_vec.cgs.value
self.v_vec = v_vec.cgs.value
else:
self.mass = mass
self.x_vec = x_vec
self.v_vec = v_vec
def return_vec(self):
"""
Concatenates the x and v vector into 1 vector 'y' used in RK formalism.
"""
return np.concatenate((self.x_vec, self.v_vec))
def return_mass(self):
"""
handler to strip the mass units if present (after converting to cgs) or return float
"""
if self.has_units:
return self.mass.cgs.value
else:
return self.mass
def return_name(self):
return self.name
class Simulation:
def __init__(self, bodies, has_units=True):
"""
Initializes instance of Simulation object.
-------------------------------------------
Params:
bodies (list): a list of Body() objects
has_units (bool): set whether bodies entered have units or not.
"""
self.has_units = has_units
self.bodies = bodies
self.N_bodies = len(self.bodies)
self.nDim = 6.0
self.quant_vec = np.concatenate(np.array([i.return_vec() for i in self.bodies]))
self.mass_vec = np.array([i.return_mass() for i in self.bodies])
self.name_vec = [i.return_name() for i in self.bodies]
def set_diff_eq(self, calc_diff_eqs, **kwargs):
"""
Method which assigns an external solver function as the diff-eq solver for RK4.
For N-body or gravitational setups, this is the function which calculates accelerations.
---------------------------------
Params:
calc_diff_eqs: A function which returns a [y] vector for RK4
**kwargs: Any additional inputs/hyperparameters the external function requires
"""
self.diff_eq_kwargs = kwargs
self.calc_diff_eqs = calc_diff_eqs
def rk4(self, t, dt):
"""
RK4 integrator. Calculates the K values and returns a new y vector
--------------------------------
Params:
t: a time. Only used if the diff eq depends on time (gravity doesn't).
dt: timestep. Non adaptive in this case
"""
k1 = dt * self.calc_diff_eqs(
t, self.quant_vec, self.mass_vec, **self.diff_eq_kwargs
)
k2 = dt * self.calc_diff_eqs(
t + 0.5 * dt,
self.quant_vec + 0.5 * k1,
self.mass_vec,
**self.diff_eq_kwargs,
)
k3 = dt * self.calc_diff_eqs(
t + 0.5 * dt,
self.quant_vec + 0.5 * k2,
self.mass_vec,
**self.diff_eq_kwargs,
)
k4 = dt * self.calc_diff_eqs(
t + dt, self.quant_vec + k2, self.mass_vec, **self.diff_eq_kwargs
)
y_new = self.quant_vec + ((k1 + 2 * k2 + 2 * k3 + k4) / 6.0)
return y_new
def run(self, T, dt, t0=0, progress=None):
"""
Method which runs the simulation on a given set of bodies.
---------------------
Params:
T: total time (in simulation units) to run the simulation. Can have units or not, just set has_units appropriately.
dt: timestep (in simulation units) to advance the simulation. Same as above
t0 (optional): set a non-zero start time to the simulation.
progress (optional): A shiny.ui.Progress object which will be used to send progress updates.
Returns:
None, but leaves an attribute history accessed via
'simulation.history' which contains all y vectors for the simulation.
These are of shape (Nstep,Nbodies * 6), so the x and y positions of particle 1 are
simulation.history[:,0], simulation.history[:,1], while the same for particle 2 are
simulation.history[:,6], simulation.history[:,7]. Velocities are also extractable.
"""
if not hasattr(self, "calc_diff_eqs"):
raise AttributeError("You must set a diff eq solver first.")
if self.has_units:
try:
_ = t0.unit
except:
t0 = (t0 * T.unit).cgs.value
T = T.cgs.value
dt = dt.cgs.value
self.history: Any = [self.quant_vec]
clock_time = t0
nsteps = int((T - t0) / dt)
start_time = time.time()
for step in range(nsteps):
if progress is not None and step % 5 == 0:
progress.set(
step,
message=f"Integrating step = {step} / {nsteps}",
detail=f"Elapsed time = {round(clock_time/1e6, 1)}",
)
y_new = self.rk4(0, dt)
self.history.append(y_new)
self.quant_vec = y_new
clock_time += dt
runtime = time.time() - start_time
self.history = np.array(self.history)
def nbody_solve(t, y, masses):
N_bodies = int(len(y) / 6)
solved_vector = np.zeros(y.size)
for i in range(N_bodies):
ioffset = i * 6
for j in range(N_bodies):
joffset = j * 6
solved_vector[ioffset] = y[ioffset + 3]
solved_vector[ioffset + 1] = y[ioffset + 4]
solved_vector[ioffset + 2] = y[ioffset + 5]
if i != j:
dx = y[ioffset] - y[joffset]
dy = y[ioffset + 1] - y[joffset + 1]
dz = y[ioffset + 2] - y[joffset + 2]
r = (dx**2 + dy**2 + dz**2) ** 0.5
ax = (-c.G.cgs * masses[j] / r**3) * dx
ay = (-c.G.cgs * masses[j] / r**3) * dy
az = (-c.G.cgs * masses[j] / r**3) * dz
ax = ax.value
ay = ay.value
az = az.value
solved_vector[ioffset + 3] += ax
solved_vector[ioffset + 4] += ay
solved_vector[ioffset + 5] += az
return solved_vector
def spherical_to_cartesian(
theta: float, phi: float, rho: float
) -> tuple[float, float, float]:
x = rho * sind(phi) * cosd(theta)
y = rho * sind(phi) * sind(theta)
z = rho * cosd(phi)
return (x, y, z)
def cosd(x):
return np.cos(x / 180 * np.pi)
def sind(x):
return np.sin(x / 180 * np.pi)
|