cleanup for the presentation
This commit is contained in:
		
							
								
								
									
										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') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user