cleanup for the presentation
This commit is contained in:
parent
7db6480e51
commit
37a2687ffe
82
nbody/presentation/helpers.typ
Normal file
82
nbody/presentation/helpers.typ
Normal file
@ -0,0 +1,82 @@
|
||||
// Helpers for code block displaying
|
||||
|
||||
#import "@preview/based:0.2.0": base64
|
||||
|
||||
#let code_font_scale = 0.6em
|
||||
|
||||
#let cell_matcher(cell, cell_tag) = {
|
||||
// Matching function to check if a cell has a specific tag
|
||||
if cell.cell_type != "code" {
|
||||
return false
|
||||
}
|
||||
let metadata = cell.metadata
|
||||
if metadata.keys().contains("tags") == false {
|
||||
return false
|
||||
}
|
||||
return cell.metadata.tags.contains(cell_tag)
|
||||
}
|
||||
|
||||
|
||||
#let code_cell(fcontent, cell_tag) = {
|
||||
// Extract the content of a cell and display it as a code block
|
||||
let cells = fcontent.cells
|
||||
let matching_cell = cells.find(x => cell_matcher(x, cell_tag))
|
||||
|
||||
let cell_content = matching_cell.source
|
||||
let single_line = cell_content.fold("", (acc, x) => acc + x)
|
||||
|
||||
text(
|
||||
raw(
|
||||
single_line,
|
||||
lang: "python",
|
||||
block: true
|
||||
),
|
||||
size: code_font_scale
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#let image_cell(fcontent, cell_tag) = {
|
||||
// Extract the output (image) of a cell and display it as an image
|
||||
let cells = fcontent.cells
|
||||
let matching_cell = cells.find(x => cell_matcher(x, cell_tag))
|
||||
|
||||
let outputs = matching_cell.outputs
|
||||
for output in outputs {
|
||||
let image_data = output.at("data", default: (:)).at("image/png", default: none)
|
||||
if image_data != none {
|
||||
align(
|
||||
center,
|
||||
image.decode(
|
||||
base64.decode(image_data),
|
||||
// height: 70% // the height should be set by the caller. This gives the flexibility to adjust the height of the image
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#let code_reference_cell(fcontent, cell_tag) = {
|
||||
// Extract the output (text) of a cell and display it as a code block
|
||||
// This is useful for showing the code of imported functions
|
||||
let cells = fcontent.cells
|
||||
let matching_cell = cells.find(x => cell_matcher(x, cell_tag))
|
||||
|
||||
let outputs = matching_cell.outputs
|
||||
for output in outputs {
|
||||
let cell_output = output.at("text", default: (:))
|
||||
if cell_output != none {
|
||||
let single_line = cell_output.join("")
|
||||
text(
|
||||
raw(
|
||||
single_line,
|
||||
lang: "python",
|
||||
block: true
|
||||
),
|
||||
size: code_font_scale
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -1,7 +1,6 @@
|
||||
#import "@preview/diatypst:0.2.0": *
|
||||
#import "@preview/based:0.2.0": base64
|
||||
|
||||
#set text(font: "Cantarell")
|
||||
// #set text(font: "Cantarell")
|
||||
// #set heading(numbering: (..nums)=>"")
|
||||
|
||||
#show: slides.with(
|
||||
@ -11,74 +10,25 @@
|
||||
authors: ("Rémy Moll"),
|
||||
toc: false,
|
||||
// layout: "large",
|
||||
// ratio: 16/9,
|
||||
)
|
||||
|
||||
|
||||
#import "helpers.typ"
|
||||
|
||||
// KINDA COOL:
|
||||
// _diatypst_ defines some default styling for elements, e.g Terms created with ```typc / Term: Definition``` will look like this
|
||||
|
||||
// / *Term*: Definition
|
||||
|
||||
|
||||
|
||||
// Helpers for code block displaying
|
||||
|
||||
#let cell_matcher(cell, cell_tag) = {
|
||||
if cell.cell_type != "code" {
|
||||
return false
|
||||
}
|
||||
let metadata = cell.metadata
|
||||
if metadata.keys().contains("tags") == false {
|
||||
return false
|
||||
}
|
||||
|
||||
return cell.metadata.tags.contains(cell_tag)
|
||||
}
|
||||
|
||||
#let code_cell(fcontent, cell_tag) = {
|
||||
let cells = fcontent.cells
|
||||
let matching_cell = cells.find(x => cell_matcher(x, cell_tag))
|
||||
|
||||
let cell_content = matching_cell.source
|
||||
// format the cell content
|
||||
let single_line = cell_content.fold("", (acc, x) => acc + x)
|
||||
|
||||
text(
|
||||
raw(
|
||||
single_line,
|
||||
lang: "python",
|
||||
block: true
|
||||
),
|
||||
size: 0.8em
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
#let image_cell(fcontent, cell_tag) = {
|
||||
let cells = fcontent.cells
|
||||
let matching_cell = cells.find(x => cell_matcher(x, cell_tag))
|
||||
|
||||
let outputs = matching_cell.outputs
|
||||
for output in outputs {
|
||||
let image_data = output.at("data", default: (:)).at("image/png", default: none)
|
||||
if image_data != none {
|
||||
align(
|
||||
center,
|
||||
image.decode(
|
||||
base64.decode(image_data),
|
||||
// format: "png",
|
||||
height: 70%
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Where is the code?
|
||||
// Setup of code location
|
||||
#let t1 = json("../task1.ipynb")
|
||||
#let t2 = json("../task2-particle-mesh.ipynb")
|
||||
|
||||
|
||||
|
||||
// Content
|
||||
// Finally - The real content
|
||||
= N-body forces and analytical solutions
|
||||
|
||||
== Objective
|
||||
@ -87,62 +37,188 @@ Implement naive N-body force computation and get an intuition of the challenges:
|
||||
- computation time
|
||||
- stability
|
||||
|
||||
$=>$ still useful to compute basic quantities of the system, but too limited for large systems or the dynamical evolution of the system
|
||||
$==>$ still useful to compute basic quantities of the system, but too limited for large systems or the dynamical evolution of the system
|
||||
|
||||
|
||||
== Overview - the system
|
||||
Get a feel for the particles and their distribution. [Code at @task1:plot_particle_distribution[]]
|
||||
#image_cell(t1, "plot_particle_distribution")
|
||||
Get a feel for the particles and their distribution. [#link(<task1:plot_particle_distribution>)[code]]
|
||||
|
||||
#code_cell(t1, "plotting")
|
||||
|
||||
== Inspecting the data
|
||||
#code_cell(t2, "plotting")
|
||||
|
||||
#image_cell(t2, "plotting")
|
||||
#columns(2)[
|
||||
#helpers.image_cell(t1, "plot_particle_distribution")
|
||||
Note: for visibility the outer particles are not shown.
|
||||
#colbreak()
|
||||
The system at hand is characterized by:
|
||||
- $N ~ 10^4$ stars
|
||||
- a _spherical_ distribution
|
||||
|
||||
$==>$ treat the system as a *globular cluster*
|
||||
|
||||
]
|
||||
|
||||
|
||||
== Density
|
||||
Some images about the density
|
||||
We compare the computed density with the analytical model provided by the _Hernquist_ model:
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 2fr),
|
||||
inset: 0.5em,
|
||||
block[
|
||||
$
|
||||
rho(r) = M/(2 pi) a / (r dot (r + a)^3)
|
||||
$
|
||||
where we infer $a$ from the half-mass radius:
|
||||
$
|
||||
r_"hm" = (1 + sqrt(2)) dot a
|
||||
$
|
||||
|
||||
#text(size: 0.6em)[
|
||||
Density sampling [#link(<task1:function_density_distribution>)[code]];
|
||||
]
|
||||
],
|
||||
block[
|
||||
#helpers.image_cell(t1, "plot_density_distribution")
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
#block(
|
||||
height: 1fr,
|
||||
)
|
||||
|
||||
|
||||
== Force computation
|
||||
// N Body and variations
|
||||
#grid(
|
||||
columns: (2fr, 1fr),
|
||||
inset: 0.5em,
|
||||
block[
|
||||
#helpers.image_cell(t1, "plot_force_radial")
|
||||
// The radial force is computed as the sum of the forces of all particles in the system.
|
||||
#text(size: 0.6em)[
|
||||
Analytical force [#link(<task1:function_analytical_forces>)[code]];
|
||||
$N^2$ force [#link(<task1:function_n2_forces>)[code]];
|
||||
$epsilon$ computation [#link(<task1:function_interparticle_distance>)[code]];
|
||||
]
|
||||
],
|
||||
block[
|
||||
Discussion:
|
||||
- the analytical method replicates the behavior accurately
|
||||
- at small softenings the $N^2$ method has noisy artifacts
|
||||
- a $1 dot epsilon$ softening is a good compromise between accuracy and stability
|
||||
]
|
||||
)
|
||||
|
||||
== N Body and variations
|
||||
sdsd
|
||||
|
||||
== Relaxation
|
||||
sd
|
||||
Relaxation [#link(<task1:compute_relaxation_time>)[code]]:
|
||||
// #helpers.code_cell(t1, "compute_relaxation_time")
|
||||
|
||||
|
||||
= Default Styling in diatypst
|
||||
Discussion!
|
||||
|
||||
== Terms, Code, Lists
|
||||
|
||||
_diatypst_ defines some default styling for elements, e.g Terms created with ```typc / Term: Definition``` will look like this
|
||||
|
||||
/ *Term*: Definition
|
||||
|
||||
A code block like this
|
||||
= Particle Mesh
|
||||
|
||||
```python
|
||||
// Example Code
|
||||
print("Hello World!")
|
||||
```
|
||||
|
||||
Lists have their marker respect the `title-color`
|
||||
|
||||
#columns(2)[
|
||||
- A
|
||||
- AAA
|
||||
- B
|
||||
#colbreak()
|
||||
1. AAA
|
||||
2. BBB
|
||||
3. CCC
|
||||
== Overview - the system
|
||||
#page(
|
||||
columns: 2
|
||||
)[
|
||||
#helpers.image_cell(t2, "plot_particle_distribution")
|
||||
]
|
||||
|
||||
|
||||
|
||||
== Force computation
|
||||
#helpers.code_reference_cell(t2, "function_mesh_force")
|
||||
|
||||
#helpers.image_cell(t2, "plot_force_radial")
|
||||
|
||||
#grid(
|
||||
columns: (2fr, 1fr),
|
||||
inset: 0.5em,
|
||||
block[
|
||||
#helpers.image_cell(t2, "plot_force_radial_single")
|
||||
// The radial force is computed as the sum of the forces of all particles in the system.
|
||||
#text(size: 0.6em)[
|
||||
$N^2$ force [#link(<task1:function_n2_forces>)[code]];
|
||||
$epsilon$ computation [#link(<task1:function_interparticle_distance>)[code]];
|
||||
Mesh force [#link(<task2:function_mesh_force>)[code]];
|
||||
]
|
||||
],
|
||||
block[
|
||||
Discussion:
|
||||
- using the (established) baseline of $N^2$ with $1 dot epsilon$ softening
|
||||
- small grids are stable but inaccurate at the center
|
||||
- very large grids have issues with overdiscretization
|
||||
|
||||
$==> 75 times 75 times 75$ as a good compromise
|
||||
]
|
||||
)
|
||||
|
||||
== Time integration
|
||||
=== Runge-Kutta
|
||||
#helpers.code_reference_cell(t2, "function_runge_kutta")
|
||||
|
||||
|
||||
#pagebreak()
|
||||
=== Results
|
||||
#align(center, block(
|
||||
height: 1fr,
|
||||
)[
|
||||
#helpers.image_cell(t2, "plot_system_evolution")
|
||||
])
|
||||
|
||||
|
||||
== Particle mesh solver
|
||||
sdlsd
|
||||
|
||||
|
||||
|
||||
|
||||
= Appendix - Code <appendix>
|
||||
|
||||
= Appendix - Code
|
||||
== Code
|
||||
#helpers.code_cell(t1, "plot_particle_distribution")
|
||||
<task1:plot_particle_distribution>
|
||||
#code_cell(t1, "plot_particle_distribution")
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_reference_cell(t1, "function_density_distribution")
|
||||
<task1:function_density_distribution>
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_reference_cell(t1, "function_analytical_forces")
|
||||
<task1:function_analytical_forces>
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_reference_cell(t1, "function_n2_forces")
|
||||
<task1:function_n2_forces>
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_reference_cell(t1, "function_interparticle_distance")
|
||||
<task1:function_interparticle_distance>
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_cell(t1, "compute_relaxation_time")
|
||||
<task1:compute_relaxation_time>
|
||||
|
||||
#pagebreak(weak: true)
|
||||
|
||||
#helpers.code_reference_cell(t2, "function_mesh_force")
|
||||
<task2:function_mesh_force>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#context {
|
||||
counter(page).update(locate(<appendix>).page())
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -29,7 +29,8 @@ def n_body_forces(particles: np.ndarray, G: float, softening: float = 0):
|
||||
|
||||
# add softening to the denominator
|
||||
r_adjusted = r**2 + softening**2
|
||||
# usually with a square root: r' = sqrt(r^2 + softening^2) and then cubed, but we combine that below
|
||||
# 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)
|
||||
|
@ -81,9 +81,12 @@ def to_particles_3d(y: np.ndarray) -> np.ndarray:
|
||||
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
|
||||
def runge_kutta_4(y: np.ndarray, t: float, f: callable, dt: float):
|
||||
"""
|
||||
Runge-Kutta 4th order integrator.
|
||||
"""
|
||||
k1 = f(y, t)
|
||||
k2 = f(y + k1/2 * dt, t + dt/2)
|
||||
k3 = f(y + k2/2 * dt, t + dt/2)
|
||||
k4 = f(y + k3 * dt, t + dt)
|
||||
return y + (k1 + 2*k2 + 2*k3 + k4)/6 * dt
|
||||
|
@ -1,7 +1,10 @@
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from astropy import units as u
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def density_distribution(r_bins: np.ndarray, particles: np.ndarray, ret_error: bool = False):
|
||||
"""
|
||||
@ -146,7 +149,8 @@ def total_energy(particles: np.ndarray):
|
||||
return ke + pe
|
||||
|
||||
|
||||
def particles_plot_3d(particles: np.ndarray, title: str = "Particle distribution (3D)"):
|
||||
|
||||
def particles_plot_3d(positions: np.ndarray, masses: np.ndarray, title: str = "Particle distribution (3D)"):
|
||||
"""
|
||||
Plots a 3D scatter plot of a set of particles.
|
||||
Assumes that the particles array has the shape:
|
||||
@ -154,48 +158,54 @@ def particles_plot_3d(particles: np.ndarray, title: str = "Particle distribution
|
||||
- or 7 columns: x, y, z, vx, vy, vz, m
|
||||
Colormap is the mass of the particles.
|
||||
"""
|
||||
if particles.shape[1] == 4:
|
||||
x, y, z, m = particles[:, 0], particles[:, 1], particles[:, 2], particles[:, 3]
|
||||
c = m
|
||||
elif particles.shape[1] == 7:
|
||||
x, y, z, m = particles[:, 0], particles[:, 1], particles[:, 2], particles[:, 6]
|
||||
c = m
|
||||
else:
|
||||
raise ValueError("Particles array must have 4 or 7 columns")
|
||||
|
||||
x, y, z = positions[:, 0], positions[:, 1], positions[:, 2]
|
||||
|
||||
fig = plt.figure()
|
||||
plt.title(title)
|
||||
fig.suptitle(title)
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
ax.scatter(particles[:,0], particles[:,1], particles[:,2], cmap='viridis', c=particles[:,3])
|
||||
sc = ax.scatter(x, y, z, cmap='coolwarm', c=masses)
|
||||
cbar = plt.colorbar(sc, ax=ax, pad=0.1)
|
||||
|
||||
try:
|
||||
cbar.set_label(f'Mass [{masses.unit:latex}]')
|
||||
ax.set_xlabel(f'x [{x.unit:latex}]')
|
||||
ax.set_ylabel(f'y [{x.unit:latex}]')
|
||||
ax.set_zlabel(f'z [{x.unit:latex}]')
|
||||
except AttributeError:
|
||||
cbar.set_label('Mass')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.set_zlabel('z')
|
||||
|
||||
plt.show()
|
||||
logger.debug("3D scatter plot with mass colormap")
|
||||
|
||||
|
||||
def particles_plot_2d(particles: np.ndarray, title: str = "Flattened distribution (along z)"):
|
||||
|
||||
def particles_plot_2d(particles: np.ndarray, title: str = "Flattened distribution (along z)", ax = None):
|
||||
"""
|
||||
Plots a 2 colormap of a set of particles, flattened in the z direction.
|
||||
Assumes that the particles array has the shape:
|
||||
- either 4 columns: x, y, z, m
|
||||
- either 3 columns: x, y, z
|
||||
- or 4 columns: x, y, z, m
|
||||
- or 7 columns: x, y, z, vx, vy, vz, m
|
||||
"""
|
||||
if particles.shape[1] == 4:
|
||||
if particles.shape[1] == 3:
|
||||
x, y, z = particles[:, 0], particles[:, 1], particles[:, 2]
|
||||
elif particles.shape[1] == 4:
|
||||
x, y, z, m = particles[:, 0], particles[:, 1], particles[:, 2], particles[:, 3]
|
||||
c = m
|
||||
elif particles.shape[1] == 7:
|
||||
x, y, z, m = particles[:, 0], particles[:, 1], particles[:, 2], particles[:, 6]
|
||||
c = m
|
||||
else:
|
||||
raise ValueError("Particles array must have 4 or 7 columns")
|
||||
|
||||
# plt.figure()
|
||||
# plt.title(title)
|
||||
# plt.scatter(x, y, c=range(particles.shape[0]))
|
||||
# plt.colorbar()
|
||||
# plt.show()
|
||||
raise ValueError("Particles array must have 3, 4 or 7 columns")
|
||||
|
||||
# or as a discrete heatmap
|
||||
plt.figure()
|
||||
plt.title(title)
|
||||
plt.hist2d(x, y, bins=100, cmap='viridis')
|
||||
plt.colorbar()
|
||||
plt.show()
|
||||
if ax is None:
|
||||
plt.figure()
|
||||
plt.title(title)
|
||||
plt.hist2d(x, y, bins=100, cmap='coolwarm')
|
||||
cbar = plt.colorbar()
|
||||
cbar.set_label(f'Particle count')
|
||||
|
||||
plt.show()
|
||||
else:
|
||||
ax.hist2d(x, y, bins=100, cmap='coolwarm')
|
||||
|
Loading…
x
Reference in New Issue
Block a user