#[test]
fn spherical_collapse() {
use crate::tooling::core::algos::lagrangian::SemiLagrangian;
use crate::tooling::core::algos::uniform::UniformGrid6D;
use crate::tooling::core::init::domain::{Domain, SpatialBoundType, VelocityBoundType};
use crate::tooling::core::integrator::TimeIntegrator as _;
use crate::tooling::core::phasespace::PhaseSpaceRepr as _;
use crate::tooling::core::poisson::fft::FftPoisson;
use crate::tooling::core::time::strang::StrangSplitting;
let lx = 2.0f64; let lv = 1.0f64; let sigma_v = 0.3f64; let g = 1.0f64;
let rho0 = 1.0f64;
let r_sphere = 1.5f64;
let domain = Domain::builder()
.spatial_extent(lx)
.velocity_extent(lv)
.spatial_resolution(8)
.velocity_resolution(4)
.t_final(1.0)
.spatial_bc(SpatialBoundType::Periodic)
.velocity_bc(VelocityBoundType::Open)
.build()
.unwrap();
let mut grid = UniformGrid6D::new(domain.clone());
let dx = domain.dx();
let dv = domain.dv();
let [nx1, nx2, nx3, nv1, nv2, nv3] = grid.sizes();
let mut s_norm = 0.0f64;
for iv1 in 0..nv1 {
let v1 = -lv + (iv1 as f64 + 0.5) * dv[0];
for iv2 in 0..nv2 {
let v2 = -lv + (iv2 as f64 + 0.5) * dv[1];
for iv3 in 0..nv3 {
let v3 = -lv + (iv3 as f64 + 0.5) * dv[2];
s_norm += (-(v1 * v1 + v2 * v2 + v3 * v3) / (2.0 * sigma_v * sigma_v)).exp()
* dv[0]
* dv[1]
* dv[2];
}
}
}
for ix1 in 0..nx1 {
let x1 = -lx + (ix1 as f64 + 0.5) * dx[0];
for ix2 in 0..nx2 {
let x2 = -lx + (ix2 as f64 + 0.5) * dx[1];
for ix3 in 0..nx3 {
let x3 = -lx + (ix3 as f64 + 0.5) * dx[2];
let r = (x1 * x1 + x2 * x2 + x3 * x3).sqrt();
let density = if r <= r_sphere { rho0 } else { 0.0 };
if density == 0.0 {
continue;
}
for iv1 in 0..nv1 {
let v1 = -lv + (iv1 as f64 + 0.5) * dv[0];
for iv2 in 0..nv2 {
let v2 = -lv + (iv2 as f64 + 0.5) * dv[1];
for iv3 in 0..nv3 {
let v3 = -lv + (iv3 as f64 + 0.5) * dv[2];
let v2sq = v1 * v1 + v2 * v2 + v3 * v3;
let f = density / s_norm * (-v2sq / (2.0 * sigma_v * sigma_v)).exp();
let idx = grid.index([ix1, ix2, ix3], [iv1, iv2, iv3]);
grid.data[idx] = f;
}
}
}
}
}
}
let rho_init = grid.compute_density();
let rho_max_init = rho_init.data.iter().cloned().fold(0.0f64, f64::max);
assert!(rho_max_init > 0.0, "Initial density must be positive");
let poisson = FftPoisson::new(&domain);
let advector = SemiLagrangian::new();
let mut integrator = StrangSplitting::new(g);
let dt = 0.1f64;
let t_col = std::f64::consts::PI / (2.0 * (g * rho0 / 6.0).sqrt());
let n_steps = ((0.4 * t_col) / dt).ceil() as usize;
for _ in 0..n_steps {
integrator
.advance(&mut grid, &poisson, &advector, dt)
.unwrap();
}
let rho_final = grid.compute_density();
assert!(
!rho_final.data.iter().any(|x| x.is_nan()),
"Density contains NaN"
);
let rho_max_final = rho_final.data.iter().cloned().fold(0.0f64, f64::max);
assert!(rho_max_final > 0.0, "Density must remain positive");
println!(
"Spherical collapse (smoke test): t_col_analytic={:.3}, ran {} steps, \
rho_max_init={:.4}, rho_max_final={:.4}",
t_col, n_steps, rho_max_init, rho_max_final
);
}