caustic/tooling/core/init/
stability.rs1use rust_decimal::prelude::ToPrimitive;
5
6use super::super::types::PhaseSpaceSnapshot;
7use super::domain::Domain;
8use super::isolated::IsolatedEquilibrium;
9
10fn elliptic_k(m: f64) -> f64 {
14 if m < 0.0 {
15 return std::f64::consts::FRAC_PI_2;
16 }
17 if m >= 1.0 {
18 return f64::INFINITY;
19 }
20 let m1 = 1.0 - m;
21 let a0 = 1.386_294_361_12;
22 let a1 = 0.096_663_442_59;
23 let a2 = 0.035_900_923_83;
24 let a3 = 0.037_425_637_13;
25 let a4 = 0.014_511_962_12;
26 let b0 = 0.5;
27 let b1 = 0.124_985_935_97;
28 let b2 = 0.068_802_485_76;
29 let b3 = 0.033_283_553_46;
30 let b4 = 0.004_417_870_12;
31 let poly_a = a0 + m1 * (a1 + m1 * (a2 + m1 * (a3 + m1 * a4)));
32 let poly_b = b0 + m1 * (b1 + m1 * (b2 + m1 * (b3 + m1 * b4)));
33 poly_a - poly_b * m1.ln()
34}
35
36fn elliptic_e(m: f64) -> f64 {
40 if m <= 0.0 {
41 return std::f64::consts::FRAC_PI_2;
42 }
43 if m >= 1.0 {
44 return 1.0;
45 }
46 let m1 = 1.0 - m;
47 let a1 = 0.443_251_414_09;
48 let a2 = 0.062_606_012_20;
49 let a3 = 0.047_573_835_46;
50 let a4 = 0.017_365_064_51;
51 let b1 = 0.249_968_730_16;
52 let b2 = 0.092_001_800_37;
53 let b3 = 0.040_696_975_26;
54 let b4 = 0.005_264_496_39;
55 let poly_a = 1.0 + m1 * (a1 + m1 * (a2 + m1 * (a3 + m1 * a4)));
56 let poly_b = m1 * (b1 + m1 * (b2 + m1 * (b3 + m1 * b4)));
57 poly_a - poly_b * m1.ln()
58}
59
60pub struct DiskStabilityIC {
62 pub disk_surface_density: Box<dyn Fn(f64) -> f64 + Send + Sync>,
64 pub disk_velocity_dispersion: Box<dyn Fn(f64) -> f64 + Send + Sync>,
66 pub bulge: Option<Box<dyn IsolatedEquilibrium>>,
68 pub halo_potential: Option<Box<dyn Fn([f64; 3]) -> f64 + Send + Sync>>,
70 pub perturbation_mode_m: u32,
72 pub perturbation_pattern_speed: f64,
74 pub perturbation_amplitude: f64,
76}
77
78impl DiskStabilityIC {
79 pub fn new(
80 disk_surface_density: Box<dyn Fn(f64) -> f64 + Send + Sync>,
81 disk_velocity_dispersion: Box<dyn Fn(f64) -> f64 + Send + Sync>,
82 perturbation_mode_m: u32,
83 perturbation_pattern_speed: f64,
84 perturbation_amplitude: f64,
85 ) -> Self {
86 Self {
87 disk_surface_density,
88 disk_velocity_dispersion,
89 bulge: None,
90 halo_potential: None,
91 perturbation_mode_m,
92 perturbation_pattern_speed,
93 perturbation_amplitude,
94 }
95 }
96
97 fn vc_squared(&self, radius: f64) -> f64 {
103 let g = 1.0;
104 let eps = 1e-6 * radius.max(1e-10);
105
106 let n_quad = 200;
109 let dr = radius / n_quad as f64;
110 let mut m_enc = 0.0;
111 for i in 0..n_quad {
112 let r_mid = (i as f64 + 0.5) * dr;
113 m_enc += (self.disk_surface_density)(r_mid) * r_mid * dr;
114 }
115 m_enc *= 2.0 * std::f64::consts::PI;
116 let mut vc2 = if radius > 1e-30 {
117 g * m_enc / radius
118 } else {
119 0.0
120 };
121
122 if let Some(ref bulge) = self.bulge {
124 let phi_plus = bulge.potential(radius + eps);
125 let phi_minus = bulge.potential((radius - eps).max(1e-30));
126 let dphi_dr = (phi_plus - phi_minus) / (2.0 * eps);
127 vc2 += -radius * dphi_dr;
128 }
129
130 if let Some(ref halo_pot) = self.halo_potential {
132 let phi_plus = halo_pot([radius + eps, 0.0, 0.0]);
133 let phi_minus = halo_pot([(radius - eps).max(1e-30), 0.0, 0.0]);
134 let dphi_dr = (phi_plus - phi_minus) / (2.0 * eps);
135 vc2 += -radius * dphi_dr;
136 }
137
138 vc2.max(0.0)
139 }
140
141 fn total_potential(&self, r_cyl: f64, z: f64, x: [f64; 3]) -> f64 {
144 let g = 1.0;
145 let r_sph = (r_cyl * r_cyl + z * z).sqrt();
146
147 let n_quad = 200;
149 let r_int = r_sph.max(1e-30);
150 let dr = r_int / n_quad as f64;
151 let mut m_enc = 0.0;
152 for i in 0..n_quad {
153 let r_mid = (i as f64 + 0.5) * dr;
154 m_enc += (self.disk_surface_density)(r_mid) * r_mid * dr;
155 }
156 m_enc *= 2.0 * std::f64::consts::PI;
157 let mut phi = if r_sph > 1e-30 {
158 -g * m_enc / r_sph
159 } else {
160 0.0
161 };
162
163 if let Some(ref bulge) = self.bulge {
165 phi += bulge.potential(r_sph);
166 }
167
168 if let Some(ref halo_pot) = self.halo_potential {
170 phi += halo_pot(x);
171 }
172
173 phi
174 }
175
176 fn find_guiding_radius(&self, lz: f64, r_max: f64) -> f64 {
179 let mut lo = 1e-10_f64;
182 let mut hi = r_max;
183
184 for _ in 0..80 {
185 let mid = 0.5 * (lo + hi);
186 let vc2 = self.vc_squared(mid);
187 let lz_mid = mid * vc2.sqrt();
188 if lz_mid < lz {
189 lo = mid;
190 } else {
191 hi = mid;
192 }
193 }
194 0.5 * (lo + hi)
195 }
196
197 pub fn toomre_q(&self, radius: f64) -> f64 {
202 let g = 1.0;
203 let sigma_r = (self.disk_velocity_dispersion)(radius);
204 let sigma_surf = (self.disk_surface_density)(radius);
205
206 if sigma_surf.abs() < 1e-30 {
207 return f64::INFINITY; }
209
210 let vc2 = self.vc_squared(radius);
212 let omega2 = if radius > 1e-30 {
213 vc2 / (radius * radius)
214 } else {
215 return f64::INFINITY;
216 };
217
218 let eps = 1e-4 * radius.max(1e-8);
221 let r_plus = radius + eps;
222 let r_minus = (radius - eps).max(1e-30);
223 let vc2_plus = self.vc_squared(r_plus);
224 let vc2_minus = self.vc_squared(r_minus);
225 let omega2_plus = vc2_plus / (r_plus * r_plus);
226 let omega2_minus = vc2_minus / (r_minus * r_minus);
227 let domega2_dr = (omega2_plus - omega2_minus) / (r_plus - r_minus);
228
229 let kappa2 = radius * domega2_dr + 4.0 * omega2;
230 if kappa2 <= 0.0 {
231 return 0.0; }
233 let kappa = kappa2.sqrt();
234
235 sigma_r * kappa / (3.36 * g * sigma_surf)
237 }
238
239 pub fn sample_on_grid(&self, domain: &Domain) -> PhaseSpaceSnapshot {
250 let g = 1.0;
251
252 let nx1 = domain.spatial_res.x1 as usize;
253 let nx2 = domain.spatial_res.x2 as usize;
254 let nx3 = domain.spatial_res.x3 as usize;
255 let nv1 = domain.velocity_res.v1 as usize;
256 let nv2 = domain.velocity_res.v2 as usize;
257 let nv3 = domain.velocity_res.v3 as usize;
258
259 let dx = domain.dx();
260 let dv = domain.dv();
261 let lx = [
262 domain.spatial.x1.to_f64().unwrap(),
263 domain.spatial.x2.to_f64().unwrap(),
264 domain.spatial.x3.to_f64().unwrap(),
265 ];
266 let lv = [
267 domain.velocity.v1.to_f64().unwrap(),
268 domain.velocity.v2.to_f64().unwrap(),
269 domain.velocity.v3.to_f64().unwrap(),
270 ];
271
272 let r_max = (lx[0] * lx[0] + lx[1] * lx[1]).sqrt() * 1.5;
274
275 let s_v3 = 1usize;
277 let s_v2 = nv3;
278 let s_v1 = nv2 * nv3;
279 let s_x3 = nv1 * s_v1;
280 let s_x2 = nx3 * s_x3;
281 let s_x1 = nx2 * s_x2;
282
283 let total = nx1 * nx2 * nx3 * nv1 * nv2 * nv3;
284 let mut data = vec![0.0f64; total];
285
286 let perturbation_m = self.perturbation_mode_m;
287 let perturbation_amp = self.perturbation_amplitude;
288
289 for ix1 in 0..nx1 {
290 let x1 = -lx[0] + (ix1 as f64 + 0.5) * dx[0];
291 for ix2 in 0..nx2 {
292 let x2 = -lx[1] + (ix2 as f64 + 0.5) * dx[1];
293
294 let r_cyl = (x1 * x1 + x2 * x2).sqrt();
295 let phi_azimuthal = x2.atan2(x1);
296
297 let pert_factor =
299 1.0 + perturbation_amp * (perturbation_m as f64 * phi_azimuthal).cos();
300
301 for ix3 in 0..nx3 {
302 let x3 = -lx[2] + (ix3 as f64 + 0.5) * dx[2];
303 let pos = [x1, x2, x3];
304
305 let phi_total = self.total_potential(r_cyl, x3, pos);
306
307 let sigma_r_here = (self.disk_velocity_dispersion)(r_cyl);
310 let sigma_surf_here = (self.disk_surface_density)(r_cyl);
311 let sigma_z = sigma_r_here; let z_0 = if sigma_surf_here > 1e-30 {
313 sigma_z / (4.0 * std::f64::consts::PI * g * sigma_surf_here).sqrt()
314 } else {
315 1e10 };
317 let sech_arg = x3 / z_0;
318 let f_z = if sech_arg.abs() < 50.0 {
319 let cosh_val = sech_arg.cosh();
320 1.0 / (cosh_val * cosh_val * 2.0 * z_0)
321 } else {
322 0.0 };
324
325 if f_z < 1e-30 {
326 continue;
328 }
329
330 let base = ix1 * s_x1 + ix2 * s_x2 + ix3 * s_x3;
331
332 for iv1 in 0..nv1 {
333 let v1 = -lv[0] + (iv1 as f64 + 0.5) * dv[0];
334 for iv2 in 0..nv2 {
335 let v2 = -lv[1] + (iv2 as f64 + 0.5) * dv[1];
336 for iv3 in 0..nv3 {
337 let v3 = -lv[2] + (iv3 as f64 + 0.5) * dv[2];
338
339 let lz = if r_cyl > 1e-30 {
342 v2 * x1 - v1 * x2 } else {
344 0.0
345 };
346
347 if lz <= 0.0 {
349 continue;
350 }
351
352 let r_c = self.find_guiding_radius(lz, r_max);
354
355 let vc2_rc = self.vc_squared(r_c);
357 let vc_rc = vc2_rc.sqrt();
358 let omega_rc = if r_c > 1e-30 {
359 vc_rc / r_c
360 } else {
361 continue;
362 };
363
364 let sigma_r_rc = (self.disk_velocity_dispersion)(r_c);
365 let sigma_surf_rc = (self.disk_surface_density)(r_c);
366 let sigma_r2_rc = sigma_r_rc * sigma_r_rc;
367
368 if sigma_r2_rc < 1e-30 || sigma_surf_rc < 1e-30 {
369 continue;
370 }
371
372 let eps_fd = 1e-4 * r_c.max(1e-8);
374 let r_cp = r_c + eps_fd;
375 let r_cm = (r_c - eps_fd).max(1e-30);
376 let omega2_rc = omega_rc * omega_rc;
377 let omega2_plus = self.vc_squared(r_cp) / (r_cp * r_cp);
378 let omega2_minus = self.vc_squared(r_cm) / (r_cm * r_cm);
379 let domega2_dr = (omega2_plus - omega2_minus) / (r_cp - r_cm);
380 let kappa2_rc = r_c * domega2_dr + 4.0 * omega2_rc;
381 if kappa2_rc <= 0.0 {
382 continue;
383 }
384 let kappa_rc = kappa2_rc.sqrt();
385
386 let phi_rc = self.total_potential(r_c, 0.0, [r_c, 0.0, 0.0]);
389 let e_c = 0.5 * vc2_rc + phi_rc;
390
391 let v2sq = v1 * v1 + v2 * v2 + v3 * v3;
393 let energy = 0.5 * v2sq + phi_total;
394
395 let de = energy - e_c;
397 if de > 0.0 {
398 continue;
400 }
401
402 let arg = -de / sigma_r2_rc;
404 if arg > 500.0 {
405 continue; }
407
408 let f_shu = sigma_surf_rc * omega_rc
412 / (std::f64::consts::PI * kappa_rc * sigma_r2_rc)
413 * arg.exp();
414
415 let f_val = f_shu * f_z * pert_factor;
417
418 if f_val > 0.0 {
419 data[base + iv1 * s_v1 + iv2 * s_v2 + iv3 * s_v3] = f_val;
420 }
421 }
422 }
423 }
424 }
425 }
426 }
427
428 PhaseSpaceSnapshot {
429 data,
430 shape: [nx1, nx2, nx3, nv1, nv2, nv3],
431 time: 0.0,
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::tooling::core::init::domain::{Domain, SpatialBoundType, VelocityBoundType};
440
441 #[test]
442 fn toomre_q_exponential_disk() {
443 let rd = 1.0;
445 let sigma_0 = 0.5;
446 let sigma_d0 = 0.3;
447 let ic = DiskStabilityIC::new(
448 Box::new(move |r: f64| sigma_0 * (-r / rd).exp()),
449 Box::new(move |r: f64| sigma_d0 * (-r / (2.0 * rd)).exp()),
450 2,
451 0.0,
452 0.0,
453 );
454 let q = ic.toomre_q(2.0);
455 assert!(q.is_finite(), "Toomre Q should be finite, got {q}");
456 assert!(q > 0.0, "Toomre Q should be positive, got {q}");
457 println!("Toomre Q at R=2: {q:.4}");
458 }
459
460 #[test]
461 fn disk_stability_sample_on_grid() {
462 let rd = 3.0;
463 let sigma_0 = 0.5;
464 let sigma_d0 = 0.3;
465 let ic = DiskStabilityIC::new(
466 Box::new(move |r: f64| sigma_0 * (-r / rd).exp()),
467 Box::new(move |r: f64| sigma_d0 * (-r / (2.0 * rd)).exp()),
468 2,
469 0.1,
470 0.05,
471 );
472 let domain = Domain::builder()
473 .spatial_extent(8.0)
474 .velocity_extent(2.0)
475 .spatial_resolution(8)
476 .velocity_resolution(8)
477 .t_final(1.0)
478 .spatial_bc(SpatialBoundType::Periodic)
479 .velocity_bc(VelocityBoundType::Open)
480 .build()
481 .unwrap();
482 let snap = ic.sample_on_grid(&domain);
483 assert_eq!(snap.shape, [8, 8, 8, 8, 8, 8]);
484 assert!(snap.data.iter().all(|v| v.is_finite()));
485 assert!(
486 snap.data.iter().any(|&v| v > 0.0),
487 "should have some nonzero values"
488 );
489 }
490}