caustic 0.0.12

A General-Purpose 6D Collisionless Gravitational Dynamics Solver
Documentation
//! Jeans instability with isolated boundary conditions — key deliverable.
//! Uses FftIsolated Poisson solver. Compares measured growth rate to analytic
//! dispersion relation γ = √(4πGρ₀ − k²σ²).

#[test]
#[allow(deprecated)]
fn jeans_instability_isolated() {
    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::FftIsolated;
    use crate::tooling::core::time::strang::StrangSplitting;

    // Jeans instability with isolated (vacuum) boundary conditions.
    // f(x,v) = C * exp(-v²/2σ²) * (1 + ε cos(k x₁))
    // With G=1, σ=1, ρ₀≈1: k_J = √(4πGρ₀)/σ ≈ 3.54
    // Use lx = 2π → k_fund = π/lx ≈ 0.5 < k_J → unstable
    // Analytic growth rate: γ = √(4πGρ₀ − k²σ²)
    let lx = 2.0 * std::f64::consts::PI;
    let lv = 4.0f64;
    let sigma = 1.0f64;
    let g = 1.0f64;
    let epsilon = 0.05f64; // small perturbation for linear regime
    let k = std::f64::consts::PI / lx; // fundamental mode

    let domain = Domain::builder()
        .spatial_extent(lx / 2.0) // domain [-lx/2, lx/2]
        .velocity_extent(lv)
        .spatial_resolution(16)
        .velocity_resolution(8)
        .t_final(2.0)
        .spatial_bc(SpatialBoundType::Isolated)
        .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();

    // Compute velocity normalization
    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];
                let v2sq = v1 * v1 + v2 * v2 + v3 * v3;
                s_norm += (-v2sq / (2.0 * sigma * sigma)).exp() * dv[0] * dv[1] * dv[2];
            }
        }
    }
    let c = 1.0 / s_norm;

    for ix1 in 0..nx1 {
        let x1 = -(lx / 2.0) + (ix1 as f64 + 0.5) * dx[0];
        let perturb = 1.0 + epsilon * (k * x1).cos();
        for ix2 in 0..nx2 {
            for ix3 in 0..nx3 {
                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 = c * (-v2sq / (2.0 * sigma * sigma)).exp() * perturb;
                            let idx = grid.index([ix1, ix2, ix3], [iv1, iv2, iv3]);
                            grid.data[idx] = f.max(0.0);
                        }
                    }
                }
            }
        }
    }

    // Record initial amplitude
    let rho_init = grid.compute_density();
    let rho_max_init = rho_init.data.iter().cloned().fold(0.0f64, f64::max);
    let rho_min_init = rho_init.data.iter().cloned().fold(f64::MAX, f64::min);
    let amp_init = (rho_max_init - rho_min_init).max(1e-30);

    // Analytic growth rate
    let rho0 = rho_init.data.iter().sum::<f64>() / rho_init.data.len() as f64;
    let omega_j_sq = 4.0 * std::f64::consts::PI * g * rho0;
    let gamma_sq = omega_j_sq - k * k * sigma * sigma;
    let gamma_analytic = if gamma_sq > 0.0 { gamma_sq.sqrt() } else { 0.0 };

    let poisson = FftIsolated::new(&domain);
    let advector = SemiLagrangian::new();
    let mut integrator = StrangSplitting::new(g);
    let dt = 0.05f64;
    let n_steps = 40; // t = 2.0

    // Track amplitude growth for rate measurement
    let mut amplitudes = vec![(0.0f64, amp_init)];

    for step in 0..n_steps {
        integrator
            .advance(&mut grid, &poisson, &advector, dt)
            .unwrap();

        if (step + 1) % 5 == 0 {
            let rho = grid.compute_density();
            let rho_max = rho.data.iter().cloned().fold(0.0f64, f64::max);
            let rho_min = rho.data.iter().cloned().fold(f64::MAX, f64::min);
            let amp = rho_max - rho_min;
            let t = (step + 1) as f64 * dt;
            amplitudes.push((t, amp));
        }
    }

    let rho_final = grid.compute_density();
    assert!(
        !rho_final.data.iter().any(|x| x.is_nan()),
        "Density contains NaN after Jeans-isolated evolution"
    );

    let rho_max_final = rho_final.data.iter().cloned().fold(0.0f64, f64::max);
    let rho_min_final = rho_final.data.iter().cloned().fold(f64::MAX, f64::min);
    let amp_final = rho_max_final - rho_min_final;

    // With isolated BC and k < k_J: amplitude should grow
    assert!(
        amp_final > amp_init,
        "Jeans-isolated: amplitude should grow. init={:.4e}, final={:.4e}",
        amp_init,
        amp_final
    );

    // Measure growth rate from amplitude history via linear fit on log(amp)
    // Only use points where amplitude is growing (linear regime)
    let gamma_measured = if amplitudes.len() >= 2 {
        let (t0, a0) = amplitudes[0];
        let (t_last, a_last) = amplitudes.last().unwrap();
        if *a_last > a0 && *t_last > t0 {
            (a_last / a0).ln() / (t_last - t0)
        } else {
            0.0
        }
    } else {
        0.0
    };

    println!(
        "Jeans-isolated: γ_analytic={:.3}, γ_measured={:.3}, amp_init={:.4e}, amp_final={:.4e}",
        gamma_analytic, gamma_measured, amp_init, amp_final
    );
    println!("  Amplitude history: {:?}", amplitudes);

    // Growth rate should be in the right ballpark (within factor of 3, given coarse grid)
    if gamma_analytic > 0.0 && gamma_measured > 0.0 {
        let ratio = gamma_measured / gamma_analytic;
        println!("  γ_measured/γ_analytic = {:.2}", ratio);
        // Coarse grid + isolated BC + only 16 points: don't expect perfect match
        // but growth rate should at least be positive and order-of-magnitude correct
        assert!(
            ratio > 0.01,
            "Measured growth rate is too small relative to analytic"
        );
    }
}