use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Instant;
use super::super::{
advecator::Advector,
integrator::{StepProducts, StepTimings, TimeIntegrator},
phasespace::PhaseSpaceRepr,
progress::{StepPhase, StepProgress},
solver::PoissonSolver,
};
use super::helpers;
use crate::CausticError;
#[derive(Clone, Debug, Default)]
pub struct StepRankDiagnostics {
pub pre_drift_ranks: Option<Vec<usize>>,
pub post_drift_ranks: Option<Vec<usize>>,
pub post_kick_ranks: Option<Vec<usize>>,
pub post_final_ranks: Option<Vec<usize>>,
pub poisson_rank_amplification: Option<f64>,
pub advection_rank_amplification: Option<f64>,
pub rank_growth_rate: Option<f64>,
pub singular_value_spectrum: Option<Vec<Vec<f64>>>,
pub rank_budget_fraction: f64,
}
fn extract_ranks(repr: &dyn PhaseSpaceRepr) -> Option<Vec<usize>> {
use super::super::algos::ht::HtTensor;
let any = repr.as_any();
any.downcast_ref::<HtTensor>()
.map(|ht| ht.nodes.iter().map(|n| n.rank()).collect())
}
fn singular_values(mat: &faer::Mat<f64>) -> Vec<f64> {
let m = mat.nrows();
let n = mat.ncols();
let k = m.min(n);
if k == 0 {
return vec![];
}
match mat.as_ref().thin_svd() {
Ok(svd) => {
let s_diag = svd.S().column_vector();
(0..k).map(|i| s_diag[i]).collect()
}
Err(_) => {
if n == 1 {
let norm: f64 = (0..m)
.map(|i| mat[(i, 0)] * mat[(i, 0)])
.sum::<f64>()
.sqrt();
if norm.is_finite() {
return vec![norm];
}
return vec![];
}
if m == 1 {
let norm: f64 = (0..n)
.map(|j| mat[(0, j)] * mat[(0, j)])
.sum::<f64>()
.sqrt();
if norm.is_finite() {
return vec![norm];
}
return vec![];
}
let mut fro2 = 0.0;
for i in 0..m {
for j in 0..n {
fro2 += mat[(i, j)] * mat[(i, j)];
}
}
let fro = fro2.sqrt();
if fro.is_finite() { vec![fro] } else { vec![] }
}
}
}
fn extract_singular_values(repr: &dyn PhaseSpaceRepr) -> Option<Vec<Vec<f64>>> {
use super::super::algos::ht::{HtNode, HtTensor};
let any = repr.as_any();
any.downcast_ref::<HtTensor>().map(|ht| {
ht.nodes
.iter()
.map(|node| match node {
HtNode::Leaf { frame, .. } => singular_values(frame),
HtNode::Interior {
transfer, ranks, ..
} => {
let [kt, kl, kr] = *ranks;
let cols = kl * kr;
let mat = faer::Mat::from_fn(kt, cols, |i, j| transfer[i * cols + j]);
singular_values(&mat)
}
})
.collect()
})
}
fn extract_max_rank_budget(repr: &dyn PhaseSpaceRepr) -> Option<usize> {
use super::super::algos::ht::HtTensor;
let any = repr.as_any();
any.downcast_ref::<HtTensor>().map(|ht| ht.max_rank)
}
fn max_rank(ranks: &[usize]) -> f64 {
ranks.iter().copied().max().unwrap_or(1).max(1) as f64
}
const RANK_HISTORY_LEN: usize = 10;
fn compute_growth_rate(history: &VecDeque<f64>) -> Option<f64> {
let n = history.len();
if n < 3 {
return None;
}
let log_ranks: Vec<f64> = history.iter().map(|&r| r.max(1.0).ln()).collect();
let mean_x = (n - 1) as f64 / 2.0;
let mean_y: f64 = log_ranks.iter().sum::<f64>() / n as f64;
let mut num = 0.0;
let mut den = 0.0;
for (i, &ly) in log_ranks.iter().enumerate() {
let xi = i as f64 - mean_x;
num += xi * (ly - mean_y);
den += xi * xi;
}
let growth_rate = if den > 1e-30 { num / den } else { 0.0 };
Some(growth_rate)
}
pub struct InstrumentedStrangSplitting {
pub inner: super::strang::StrangSplitting,
pub last_diagnostics: StepRankDiagnostics,
last_timings: StepTimings,
progress: Option<Arc<StepProgress>>,
rank_history: VecDeque<f64>,
}
impl InstrumentedStrangSplitting {
pub fn new(g: f64) -> Self {
Self {
inner: super::strang::StrangSplitting::new(g),
last_diagnostics: StepRankDiagnostics::default(),
last_timings: StepTimings::default(),
progress: None,
rank_history: VecDeque::with_capacity(RANK_HISTORY_LEN),
}
}
}
impl TimeIntegrator for InstrumentedStrangSplitting {
fn advance(
&mut self,
repr: &mut dyn PhaseSpaceRepr,
solver: &dyn PoissonSolver,
advector: &dyn Advector,
dt: f64,
) -> Result<StepProducts, CausticError> {
let _span = tracing::info_span!("instrumented_strang_advance").entered();
let mut timings = StepTimings::default();
if let Some(ref p) = self.progress {
p.start_step();
}
helpers::report_phase!(self.progress, StepPhase::DriftHalf1, 0, 5);
let mut diag = StepRankDiagnostics {
pre_drift_ranks: extract_ranks(&*repr),
..Default::default()
};
helpers::time_ms!(timings, drift_ms, advector.drift(repr, dt / 2.0));
diag.post_drift_ranks = extract_ranks(&*repr);
helpers::report_phase!(self.progress, StepPhase::PoissonSolve, 1, 5);
let (_density, _potential, accel) = helpers::time_ms!(
timings,
poisson_ms,
helpers::solve_poisson(repr, solver, self.inner.g)
);
helpers::report_phase!(self.progress, StepPhase::Kick, 2, 5);
helpers::time_ms!(timings, kick_ms, advector.kick(repr, &accel, dt));
diag.post_kick_ranks = extract_ranks(&*repr);
helpers::report_phase!(self.progress, StepPhase::DriftHalf2, 3, 5);
helpers::time_ms!(timings, drift_ms, advector.drift(repr, dt / 2.0));
diag.post_final_ranks = extract_ranks(&*repr);
if let (Some(pre), Some(post_drift)) = (&diag.pre_drift_ranks, &diag.post_drift_ranks) {
let r_pre = max_rank(pre);
let r_post = max_rank(post_drift);
diag.advection_rank_amplification = Some(r_post / r_pre);
}
if let (Some(post_drift), Some(post_kick)) = (&diag.post_drift_ranks, &diag.post_kick_ranks)
{
let r_drift = max_rank(post_drift);
let r_kick = max_rank(post_kick);
diag.poisson_rank_amplification = Some(r_kick / r_drift);
}
if let Some(ref final_ranks) = diag.post_final_ranks {
let current_max = max_rank(final_ranks);
if self.rank_history.len() >= RANK_HISTORY_LEN {
self.rank_history.pop_front();
}
self.rank_history.push_back(current_max);
let growth_rate = compute_growth_rate(&self.rank_history);
diag.rank_growth_rate = growth_rate;
if let Some(budget) = extract_max_rank_budget(&*repr) {
let budget_f = budget.max(1) as f64;
diag.rank_budget_fraction = current_max / budget_f;
}
diag.singular_value_spectrum = extract_singular_values(&*repr);
if let Some(gr) = growth_rate.filter(|&r| r > 0.5) {
tracing::warn!(
"Rank explosion warning: growth rate {:.2} (doubling every {:.1} steps)",
gr,
0.693 / gr.max(0.01)
);
}
if diag.rank_budget_fraction > 0.9 {
tracing::warn!(
"Rank budget nearly saturated: {:.0}% of max_rank used",
diag.rank_budget_fraction * 100.0
);
}
}
helpers::report_phase!(self.progress, StepPhase::StepComplete, 4, 5);
let (density, potential, acceleration) = helpers::time_ms!(
timings,
density_ms,
helpers::solve_poisson(repr, solver, self.inner.g)
);
self.last_diagnostics = diag;
self.last_timings = timings;
Ok(StepProducts {
density,
potential,
acceleration,
})
}
fn max_dt(&self, repr: &dyn PhaseSpaceRepr, cfl_factor: f64) -> f64 {
self.inner.max_dt(repr, cfl_factor)
}
fn last_step_timings(&self) -> Option<&StepTimings> {
Some(&self.last_timings)
}
fn set_progress(&mut self, progress: Arc<StepProgress>) {
self.progress = Some(progress);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instrumented_strang_on_uniform_grid() {
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::phasespace::PhaseSpaceRepr as _;
use crate::tooling::core::poisson::fft::FftPoisson;
let domain = Domain::builder()
.spatial_extent(1.0)
.velocity_extent(1.0)
.spatial_resolution(4)
.velocity_resolution(4)
.t_final(0.1)
.spatial_bc(SpatialBoundType::Periodic)
.velocity_bc(VelocityBoundType::Open)
.build()
.unwrap();
let mut grid = UniformGrid6D::new(domain.clone());
for v in grid.data.iter_mut() {
*v = 1.0;
}
let poisson = FftPoisson::new(&domain);
let advector = SemiLagrangian::new();
let mut integrator = InstrumentedStrangSplitting::new(1.0);
integrator
.advance(&mut grid, &poisson, &advector, 0.01)
.unwrap();
assert!(integrator.last_diagnostics.pre_drift_ranks.is_none());
assert!(integrator.last_diagnostics.post_drift_ranks.is_none());
assert!(integrator.last_diagnostics.post_kick_ranks.is_none());
assert!(
integrator
.last_diagnostics
.poisson_rank_amplification
.is_none()
);
assert!(
integrator
.last_diagnostics
.advection_rank_amplification
.is_none()
);
assert!(integrator.last_diagnostics.rank_growth_rate.is_none());
assert!(
integrator
.last_diagnostics
.singular_value_spectrum
.is_none()
);
assert_eq!(integrator.last_diagnostics.rank_budget_fraction, 0.0);
}
#[test]
fn test_rank_growth_rate_zero_for_equilibrium() {
use crate::tooling::core::algos::ht::HtTensor;
use crate::tooling::core::algos::lagrangian::SemiLagrangian;
use crate::tooling::core::init::domain::{Domain, SpatialBoundType, VelocityBoundType};
use crate::tooling::core::poisson::fft::FftPoisson;
let n = 4usize;
let domain = Domain::builder()
.spatial_extent(1.0)
.velocity_extent(1.0)
.spatial_resolution(n as i128)
.velocity_resolution(n as i128)
.t_final(1.0)
.spatial_bc(SpatialBoundType::Periodic)
.velocity_bc(VelocityBoundType::Open)
.build()
.unwrap();
let shape = [n; 6];
let total = n.pow(6);
let mut data = vec![0.0f64; total];
for i0 in 0..n {
for i1 in 0..n {
for i2 in 0..n {
for i3 in 0..n {
for i4 in 0..n {
for i5 in 0..n {
let idx = i0 * n.pow(5)
+ i1 * n.pow(4)
+ i2 * n.pow(3)
+ i3 * n.pow(2)
+ i4 * n
+ i5;
data[idx] = 1.0; }
}
}
}
}
}
let mut ht = HtTensor::from_full(&data, shape, &domain, 1e-10);
ht.max_rank = 16;
let poisson = FftPoisson::new(&domain);
let advector = SemiLagrangian::new();
let mut integrator = InstrumentedStrangSplitting::new(1.0);
let dt = 0.001;
for _ in 0..5 {
integrator
.advance(&mut ht, &poisson, &advector, dt)
.unwrap();
}
let diag = &integrator.last_diagnostics;
assert!(
diag.rank_growth_rate.is_some(),
"rank_growth_rate should be Some after 5 steps"
);
let gr = diag.rank_growth_rate.unwrap();
assert!(
gr.abs() < 0.5,
"rank growth rate should be near zero for equilibrium, got {gr:.4}"
);
}
#[test]
fn test_rank_budget_fraction_computed() {
use crate::tooling::core::algos::ht::HtTensor;
use crate::tooling::core::algos::lagrangian::SemiLagrangian;
use crate::tooling::core::init::domain::{Domain, SpatialBoundType, VelocityBoundType};
use crate::tooling::core::poisson::fft::FftPoisson;
let n = 4usize;
let domain = Domain::builder()
.spatial_extent(1.0)
.velocity_extent(1.0)
.spatial_resolution(n as i128)
.velocity_resolution(n as i128)
.t_final(1.0)
.spatial_bc(SpatialBoundType::Periodic)
.velocity_bc(VelocityBoundType::Open)
.build()
.unwrap();
let shape = [n; 6];
let total = n.pow(6);
let mut data = vec![0.0f64; total];
for i0 in 0..n {
for i1 in 0..n {
for i2 in 0..n {
for i3 in 0..n {
for i4 in 0..n {
for i5 in 0..n {
let idx = i0 * n.pow(5)
+ i1 * n.pow(4)
+ i2 * n.pow(3)
+ i3 * n.pow(2)
+ i4 * n
+ i5;
data[idx] = ((i0 + 1)
* (i1 + 1)
* (i2 + 1)
* (i3 + 1)
* (i4 + 1)
* (i5 + 1)) as f64;
}
}
}
}
}
}
let mut ht = HtTensor::from_full(&data, shape, &domain, 1e-10);
ht.max_rank = 16;
let poisson = FftPoisson::new(&domain);
let advector = SemiLagrangian::new();
let mut integrator = InstrumentedStrangSplitting::new(0.0);
integrator
.advance(&mut ht, &poisson, &advector, 0.001)
.unwrap();
let diag = &integrator.last_diagnostics;
assert!(
diag.rank_budget_fraction >= 0.0 && diag.rank_budget_fraction <= 1.0,
"rank_budget_fraction should be in [0, 1], got {}",
diag.rank_budget_fraction
);
assert!(
diag.singular_value_spectrum.is_some(),
"singular_value_spectrum should be Some for HtTensor"
);
let svs = diag.singular_value_spectrum.as_ref().unwrap();
assert_eq!(svs.len(), 11, "should have 11 nodes for 6D HT");
for (i, node_svs) in svs.iter().enumerate() {
for &sv in node_svs {
assert!(
sv >= 0.0 && sv.is_finite(),
"node {i}: singular values should be non-negative and finite, got {sv}"
);
}
}
}
}