cleanup for the presentation

This commit is contained in:
Remy Moll 2025-02-01 18:33:07 +01:00
parent 7db6480e51
commit 37a2687ffe
8 changed files with 904 additions and 638 deletions

View 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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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')