use super::super::{phasespace::PhaseSpaceRepr, poisson::utils::fft_3d_c2c_inplace, types::*};
use rayon::prelude::*;
use rustfft::num_complex::Complex;
pub struct PhaseSpaceDiagnostics {
pub stream_count: StreamCountField,
pub local_entropy: Vec<f64>,
pub caustic_cells: Vec<[usize; 3]>,
}
impl PhaseSpaceDiagnostics {
pub fn compute(repr: &dyn PhaseSpaceRepr) -> Self {
let stream_count = repr.stream_count();
let [nx1, nx2, nx3] = stream_count.shape;
let mut caustic_cells = Vec::new();
for ix1 in 0..nx1 {
for ix2 in 0..nx2 {
for ix3 in 0..nx3 {
let idx = ix1 * nx2 * nx3 + ix2 * nx3 + ix3;
if stream_count.data[idx] > 1 {
caustic_cells.push([ix1, ix2, ix3]);
}
}
}
}
Self {
stream_count,
local_entropy: Vec::new(), caustic_cells,
}
}
pub fn velocity_distribution_at(repr: &dyn PhaseSpaceRepr, x: [f64; 3]) -> Vec<f64> {
repr.velocity_distribution(&x)
}
pub fn power_spectrum(density: &DensityField) -> Vec<(f64, f64)> {
let [nx, ny, nz] = density.shape;
let mut data: Vec<Complex<f64>> =
density.data.iter().map(|&r| Complex::new(r, 0.0)).collect();
fft_3d_c2c_inplace(&mut data, density.shape);
let k_max = ((nx * nx + ny * ny + nz * nz) as f64).sqrt().ceil() as usize + 1;
let mut power_sum = vec![0.0; k_max];
let mut count = vec![0u64; k_max];
for ix in 0..nx {
let kx = if ix < nx / 2 {
ix as i64
} else {
ix as i64 - nx as i64
};
for iy in 0..ny {
let ky = if iy < ny / 2 {
iy as i64
} else {
iy as i64 - ny as i64
};
for iz in 0..nz {
let kz = if iz < nz / 2 {
iz as i64
} else {
iz as i64 - nz as i64
};
let k_mag = ((kx * kx + ky * ky + kz * kz) as f64).sqrt();
let bin = k_mag.round() as usize;
if bin < k_max {
let c = data[ix * ny * nz + iy * nz + iz];
power_sum[bin] += c.norm_sqr();
count[bin] += 1;
}
}
}
}
let mut result = Vec::new();
for bin in 1..k_max {
if count[bin] > 0 {
result.push((bin as f64, power_sum[bin] / count[bin] as f64));
}
}
result
}
pub fn growth_rates(density_history: &[DensityField], dt: f64) -> Vec<(f64, f64)> {
if density_history.is_empty() {
return Vec::new();
}
let spectra: Vec<Vec<(f64, f64)>> = density_history
.par_iter()
.map(Self::power_spectrum)
.collect();
if spectra[0].is_empty() {
return Vec::new();
}
let mut result = Vec::new();
for (i, &(k, _)) in spectra[0].iter().enumerate() {
let mut sum_t = 0.0;
let mut sum_ln = 0.0;
let mut sum_t2 = 0.0;
let mut sum_tln = 0.0;
let mut n = 0.0;
for (step, spectrum) in spectra.iter().enumerate() {
if i < spectrum.len() {
let amp = spectrum[i].1.sqrt();
if amp > 0.0 {
let t = step as f64 * dt;
let ln_amp = amp.ln();
sum_t += t;
sum_ln += ln_amp;
sum_t2 += t * t;
sum_tln += t * ln_amp;
n += 1.0;
}
}
}
if n > 1.0 {
let denom = n * sum_t2 - sum_t * sum_t;
if denom.abs() > 1e-30 {
let gamma = (n * sum_tln - sum_t * sum_ln) / denom;
result.push((k, gamma));
}
}
}
result
}
}
pub fn field_energy_spectrum(potential: &PotentialField, dx: [f64; 3]) -> Vec<(f64, f64)> {
let [nx, ny, nz] = potential.shape;
let lx = nx as f64 * dx[0];
let ly = ny as f64 * dx[1];
let lz = nz as f64 * dx[2];
let mut data: Vec<Complex<f64>> = potential
.data
.iter()
.map(|&r| Complex::new(r, 0.0))
.collect();
fft_3d_c2c_inplace(&mut data, potential.shape);
let k_max = ((nx * nx + ny * ny + nz * nz) as f64).sqrt().ceil() as usize + 1;
let mut energy_sum = vec![0.0; k_max];
let mut count = vec![0u64; k_max];
use std::f64::consts::PI;
for ix in 0..nx {
let kx_idx = if ix < nx / 2 {
ix as i64
} else {
ix as i64 - nx as i64
};
let kx = 2.0 * PI * kx_idx as f64 / lx;
for iy in 0..ny {
let ky_idx = if iy < ny / 2 {
iy as i64
} else {
iy as i64 - ny as i64
};
let ky = 2.0 * PI * ky_idx as f64 / ly;
for iz in 0..nz {
let kz_idx = if iz < nz / 2 {
iz as i64
} else {
iz as i64 - nz as i64
};
let kz = 2.0 * PI * kz_idx as f64 / lz;
let k2 = kx * kx + ky * ky + kz * kz;
let k_mag_int = ((kx_idx * kx_idx + ky_idx * ky_idx + kz_idx * kz_idx) as f64)
.sqrt()
.round() as usize;
if k_mag_int > 0 && k_mag_int < k_max {
let c = data[ix * ny * nz + iy * nz + iz];
energy_sum[k_mag_int] += k2 * c.norm_sqr();
count[k_mag_int] += 1;
}
}
}
}
let mut result = Vec::new();
for bin in 1..k_max {
if count[bin] > 0 {
result.push((bin as f64, energy_sum[bin] / count[bin] as f64));
}
}
result
}
pub fn potential_power_spectrum(potential: &PotentialField, dx: [f64; 3]) -> Vec<(f64, f64)> {
let [nx, ny, nz] = potential.shape;
let n = nx * ny * nz;
if n == 0 {
return vec![];
}
let mut buffer: Vec<Complex<f64>> = potential
.data
.iter()
.map(|&v| Complex::new(v, 0.0))
.collect();
fft_3d_c2c_inplace(&mut buffer, potential.shape);
let dk = [
2.0 * std::f64::consts::PI / (nx as f64 * dx[0]),
2.0 * std::f64::consts::PI / (ny as f64 * dx[1]),
2.0 * std::f64::consts::PI / (nz as f64 * dx[2]),
];
let k_nyquist = [
(nx / 2) as f64 * dk[0],
(ny / 2) as f64 * dk[1],
(nz / 2) as f64 * dk[2],
];
let k_max = (k_nyquist[0].powi(2) + k_nyquist[1].powi(2) + k_nyquist[2].powi(2)).sqrt();
let dk_bin = dk.iter().copied().fold(f64::INFINITY, f64::min);
let n_bins = ((k_max / dk_bin) as usize + 1).max(1);
let mut power_bins = vec![0.0f64; n_bins];
let mut count_bins = vec![0usize; n_bins];
let norm = 1.0 / (n as f64 * n as f64);
for ikx in 0..nx {
let kx = if ikx <= nx / 2 {
ikx as f64
} else {
ikx as f64 - nx as f64
} * dk[0];
for iky in 0..ny {
let ky = if iky <= ny / 2 {
iky as f64
} else {
iky as f64 - ny as f64
} * dk[1];
for ikz in 0..nz {
let kz = if ikz <= nz / 2 {
ikz as f64
} else {
ikz as f64 - nz as f64
} * dk[2];
let k_mag = (kx * kx + ky * ky + kz * kz).sqrt();
if k_mag < 1e-14 {
continue;
}
let c = buffer[ikx * ny * nz + iky * nz + ikz];
let power = (c.re * c.re + c.im * c.im) * norm;
let bin = (k_mag / dk_bin).round() as usize;
if bin < n_bins {
power_bins[bin] += power;
count_bins[bin] += 1;
}
}
}
}
power_bins
.iter()
.zip(count_bins.iter())
.enumerate()
.filter(|&(_, (_, &c))| c > 0)
.map(|(i, (&p, &c))| {
let k = (i as f64 + 0.5) * dk_bin;
(k, p / c as f64)
})
.collect()
}
pub fn poisson_residual_l2(
density: &DensityField,
potential: &PotentialField,
g: f64,
dx: [f64; 3],
) -> f64 {
let [nx, ny, nz] = density.shape;
let four_pi_g = 4.0 * std::f64::consts::PI * g;
let inv_dx2 = [
1.0 / (dx[0] * dx[0]),
1.0 / (dx[1] * dx[1]),
1.0 / (dx[2] * dx[2]),
];
let mut sum_sq = 0.0;
let mut count = 0usize;
for ix in 1..nx.saturating_sub(1) {
for iy in 1..ny.saturating_sub(1) {
for iz in 1..nz.saturating_sub(1) {
let idx = ix * ny * nz + iy * nz + iz;
let phi = potential.data[idx];
let lap_x = (potential.data[(ix + 1) * ny * nz + iy * nz + iz]
+ potential.data[(ix - 1) * ny * nz + iy * nz + iz]
- 2.0 * phi)
* inv_dx2[0];
let lap_y = (potential.data[ix * ny * nz + (iy + 1) * nz + iz]
+ potential.data[ix * ny * nz + (iy - 1) * nz + iz]
- 2.0 * phi)
* inv_dx2[1];
let lap_z = (potential.data[ix * ny * nz + iy * nz + (iz + 1)]
+ potential.data[ix * ny * nz + iy * nz + (iz - 1)]
- 2.0 * phi)
* inv_dx2[2];
let laplacian = lap_x + lap_y + lap_z;
let rhs = four_pi_g * density.data[idx];
let residual = laplacian - rhs;
sum_sq += residual * residual;
count += 1;
}
}
}
if count > 0 {
(sum_sq / count as f64).sqrt()
} else {
0.0
}
}