#[test]
fn strong_landau_damping() {
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::solver::PoissonSolver as _;
use crate::tooling::core::time::strang::StrangSplitting;
let epsilon = 0.5_f64; let k = 0.5_f64; let lx = std::f64::consts::PI / k; let lv = 6.0_f64;
let g = 1.0_f64;
let t_final = 10.0_f64;
let n_spatial = 16_usize;
let n_velocity = 16_usize;
let domain = Domain::builder()
.spatial_extent(lx)
.velocity_extent(lv)
.spatial_resolution(n_spatial as i128)
.velocity_resolution(n_velocity as i128)
.t_final(t_final)
.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 v_norm = 0.0_f64;
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;
v_norm += (-v2sq / 2.0).exp() * dv[0] * dv[1] * dv[2];
}
}
}
let c = 1.0 / v_norm;
for ix1 in 0..nx1 {
let x1 = -lx + (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).exp() * perturb;
let idx = grid.index([ix1, ix2, ix3], [iv1, iv2, iv3]);
grid.data[idx] = f.max(0.0);
}
}
}
}
}
}
let poisson = FftPoisson::new(&domain);
let advector = SemiLagrangian::new();
let mut integrator = StrangSplitting::new(g);
let dt = 0.1_f64;
let n_steps = (t_final / dt) as usize;
let compute_field_energy = |grid: &UniformGrid6D, poisson: &FftPoisson, dx: &[f64; 3]| {
let rho = grid.compute_density();
let potential = poisson.solve(&rho, g);
let accel = poisson.compute_acceleration(&potential);
let dx3 = dx[0] * dx[1] * dx[2];
let n_sp = accel.gx.len();
let mut e_field = 0.0_f64;
for i in 0..n_sp {
let gx = accel.gx[i];
let gy = accel.gy[i];
let gz = accel.gz[i];
e_field += gx * gx + gy * gy + gz * gz;
}
0.5 * e_field * dx3
};
let e_field_init = compute_field_energy(&grid, &poisson, &dx);
let mut field_energies = vec![e_field_init];
let rho_init = grid.compute_density();
let pot_init = poisson.solve(&rho_init, g);
let dx3 = dx[0] * dx[1] * dx[2];
let w_init: f64 = rho_init
.data
.iter()
.zip(pot_init.data.iter())
.map(|(&rho, &phi)| 0.5 * rho * phi)
.sum::<f64>()
* dx3;
let t_init = grid.total_kinetic_energy().unwrap();
let e_total_init = t_init + w_init;
for step in 0..n_steps {
integrator
.advance(&mut grid, &poisson, &advector, dt)
.unwrap();
if (step + 1) % 5 == 0 {
let e_f = compute_field_energy(&grid, &poisson, &dx);
field_energies.push(e_f);
}
}
let rho_final = grid.compute_density();
assert!(
!rho_final.data.iter().any(|x| x.is_nan()),
"Final density contains NaN"
);
let pot_final = poisson.solve(&rho_final, g);
let w_final: f64 = rho_final
.data
.iter()
.zip(pot_final.data.iter())
.map(|(&rho, &phi)| 0.5 * rho * phi)
.sum::<f64>()
* dx3;
let t_final_ke = grid.total_kinetic_energy().unwrap();
let e_total_final = t_final_ke + w_final;
let energy_drift = if e_total_init.abs() > 1e-30 {
(e_total_final - e_total_init).abs() / e_total_init.abs()
} else {
0.0
};
assert!(
energy_drift < 1.0,
"Strong Landau: energy drift {:.2e} exceeds 100% threshold",
energy_drift
);
let n_early = (field_energies.len()).min(5);
if n_early >= 2 {
let early_min = field_energies[1..n_early]
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
if early_min >= field_energies[0] {
println!(
"Strong Landau: WARNING initial decay not observed at this resolution. \
E_field(0)={:.4e}, early_min={:.4e}",
field_energies[0], early_min
);
}
}
let mut saw_decrease = false;
let mut saw_increase_after_decrease = false;
for w in field_energies.windows(2) {
if w[1] < w[0] {
saw_decrease = true;
}
if saw_decrease && w[1] > w[0] {
saw_increase_after_decrease = true;
break;
}
}
if !saw_increase_after_decrease {
println!(
"Strong Landau: WARNING recurrence not observed at this resolution. Energies: {:?}",
&field_energies[..field_energies.len().min(10)]
);
}
println!(
"Strong Landau: energy_drift={:.2e}, E_field(0)={:.4e}, E_field(end)={:.4e}, n_samples={}",
energy_drift,
field_energies[0],
field_energies.last().unwrap(),
field_energies.len()
);
}