#![allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
use std::sync::atomic::{AtomicUsize, Ordering};
use semiflow::{
general_operator::GeneralOperator,
generator_action::{CsrGenerator, DivFormGenerator, GeneratorAction, NegLaplacianGenerator},
phi_action::{phi_action, phi_action_batched, phi_combination, phi_cost_probe, PHI_MAX},
phi_sweep_probe,
scratch::ScratchPool,
Diffusion4thChernoff, Grid1D, SymmetricOperator,
};
mod phi_dense;
use phi_dense::{dense_phi_k, lcg_vec, sup, sup_diff};
struct Counted<G> {
inner: G,
calls: AtomicUsize,
}
impl<G: GeneratorAction<f64>> GeneratorAction<f64> for Counted<G> {
fn dim(&self) -> usize {
self.inner.dim()
}
fn apply_generator(&self, src: &[f64], dst: &mut [f64]) {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.apply_generator(src, dst);
}
fn norm_bound(&self) -> f64 {
self.inner.norm_bound()
}
fn diagonal_shift(&self) -> Option<(f64, f64)> {
self.inner.diagonal_shift()
}
}
fn build_dense(n: usize) -> (Vec<f64>, Vec<f64>) {
let t: Vec<f64> = (0..n - 1)
.map(|i| 0.8 + 0.4 * (i as f64 * 0.9).sin())
.collect();
let mut a = vec![0.0_f64; n * n];
for i in 0..n {
let left = if i > 0 { t[i - 1] } else { 0.0 };
let right = if i + 1 < n { t[i] } else { 0.0 };
a[i * n + i] = left + right + 0.2 * (1.0 + i as f64 / 3.0);
if i > 0 {
a[i * n + i - 1] = -left;
}
if i + 1 < n {
a[i * n + i + 1] = -right;
}
}
let mass = (0..n)
.map(|i| 1.0 + 49.0 * (i as f64 / (n - 1) as f64).powi(2))
.collect();
(a, mass)
}
fn csr(a: &[f64], n: usize) -> (Vec<usize>, Vec<u32>, Vec<f64>) {
let (mut rp, mut ci, mut va) = (vec![0_usize], Vec::new(), Vec::new());
for i in 0..n {
for j in 0..n {
if a[i * n + j] != 0.0 {
ci.push(u32::try_from(j).unwrap());
va.push(a[i * n + j]);
}
}
rp.push(va.len());
}
(rp, ci, va)
}
fn symmetric(a: &[f64], n: usize) -> SymmetricOperator<f64> {
let (rp, ci, va) = csr(a, n);
SymmetricOperator::from_csr(n, &rp, &ci, &va, 1e-14).unwrap()
}
fn cost<G: GeneratorAction<f64>>(name: &str, gen: G, z: f64, expected: (u32, u32)) {
let tau = z / gen.norm_bound();
let n = gen.dim();
let w: Vec<Vec<f64>> = (0..=PHI_MAX).map(|k| lcg_vec(7 + k as u64, n)).collect();
let refs: Vec<&[f64]> = w.iter().map(Vec::as_slice).collect();
let counted = Counted {
inner: gen,
calls: AtomicUsize::new(0),
};
let mut out = vec![0.0; n];
phi_combination(&counted, tau, &refs, &mut out, &mut ScratchPool::new()).unwrap();
let calls = counted.calls.load(Ordering::Relaxed) as u64;
let (s, m) = phi_sweep_probe(&counted, tau);
let (s0, m0) = phi_cost_probe(counted.norm_bound(), tau);
let unshifted = u64::from(s0) * u64::from(m0);
eprintln!("G_PHI_SHIFT_COST {name} z={z}: calls={calls} (s,m)=({s},{m}) unshifted={unshifted}");
assert_eq!(
calls,
u64::from(s) * u64::from(m),
"{name} z={z}: counter vs probe"
);
assert_eq!((s, m), expected, "{name} z={z}: schedule");
assert!(
10 * calls <= 6 * unshifted,
"{name} z={z}: {calls} > 0.6 x {unshifted}"
);
}
fn a_var(x: f64) -> f64 {
0.5 + 0.2 * x.sin()
}
fn a_var_p(x: f64) -> f64 {
0.2 * x.cos()
}
fn a_var_pp(x: f64) -> f64 {
-0.2 * x.sin()
}
#[test]
fn g_phi_shift_cost() {
let n = 64;
let (a, mass) = build_dense(n);
let op = symmetric(&a, n);
let grid = Grid1D::new(-3.0, 3.0, n).unwrap();
for (z, sym, div) in [(40.0, (13, 30), (13, 30)), (400.0, (117, 30), (115, 30))] {
let neg = NegLaplacianGenerator::new(op.clone());
bound_matches_dense("neg_laplacian", &neg);
cost("neg_laplacian", neg, z, sym);
let plain = CsrGenerator::from_symmetric(&op, None).unwrap();
bound_matches_dense("csr", &plain);
cost("csr", plain, z, sym);
let massed = CsrGenerator::from_symmetric(&op, Some(&mass)).unwrap();
bound_matches_dense("csr+mass", &massed);
cost("csr+mass", massed, z, sym);
let d4 = Diffusion4thChernoff::new(a_var, a_var_p, a_var_pp, 0.7, grid);
let df = DivFormGenerator::new(d4);
bound_matches_dense("div_form", &df);
cost("div_form", df, z, div);
}
for bad in [f64::NAN, -1.0, f64::INFINITY] {
let gen = BadShift(bad);
let tau = 400.0 / gen.norm_bound();
assert_eq!(
phi_sweep_probe(&gen, tau),
phi_cost_probe(gen.norm_bound(), tau),
"norm {bad}: must fall back to the unshifted schedule"
);
}
}
fn dense_of<G: GeneratorAction<f64>>(gen: &G) -> Vec<f64> {
let n = gen.dim();
let mut g = vec![0.0; n * n];
let (mut e, mut col) = (vec![0.0; n], vec![0.0; n]);
for j in 0..n {
e[j] = 1.0;
gen.apply_generator(&e, &mut col);
for i in 0..n {
g[i * n + j] = col[i];
}
e[j] = 0.0;
}
g
}
fn bound_matches_dense<G: GeneratorAction<f64>>(name: &str, gen: &G) {
let n = gen.dim();
let g = dense_of(gen);
let row = |i: usize| {
let r: f64 = (0..n).filter(|&j| j != i).map(|j| g[i * n + j].abs()).sum();
(g[i * n + i], r)
};
let hi = (0..n)
.map(|i| row(i).0 + row(i).1)
.fold(f64::NEG_INFINITY, f64::max);
let lo = (0..n)
.map(|i| row(i).0 - row(i).1)
.fold(f64::INFINITY, f64::min);
let mid = 0.5 * hi + 0.5 * lo;
let norm = (0..n)
.map(|i| (row(i).0 - mid).abs() + row(i).1)
.fold(0.0, f64::max);
let (gbar, ns) = gen
.diagonal_shift()
.expect("built-in generators report a shift");
let scale = hi.abs().max(lo.abs());
eprintln!("G_PHI_SHIFT_COST {name}: gbar {gbar:.6e} vs {mid:.6e}, norm {ns:.6e} vs {norm:.6e}");
assert!(
(gbar - mid).abs() <= 1e-12 * scale,
"{name}: centre {gbar} vs {mid}"
);
assert!(
(ns - norm).abs() <= 1e-12 * scale,
"{name}: norm {ns} vs {norm}"
);
}
struct BadShift(f64);
impl GeneratorAction<f64> for BadShift {
fn dim(&self) -> usize {
4
}
fn apply_generator(&self, src: &[f64], dst: &mut [f64]) {
for i in 0..4 {
dst[i] = -2.0 * src[i];
}
}
fn norm_bound(&self) -> f64 {
2.0
}
fn diagonal_shift(&self) -> Option<(f64, f64)> {
Some((-0.1, self.0))
}
}
fn dense_g(a: &[f64], mass: &[f64], n: usize) -> Vec<f64> {
(0..n * n).map(|ij| -a[ij] / mass[ij / n]).collect()
}
fn dense_check<G: GeneratorAction<f64>>(name: &str, gen: &G, g: &[f64], z: f64) {
let n = gen.dim();
let tau = z / gen.norm_bound();
let shifted = phi_sweep_probe(gen, tau) != phi_cost_probe(gen.norm_bound(), tau);
assert!(shifted, "{name} z={z}: shift not taken");
let mut sc = ScratchPool::new();
let v = lcg_vec(11, n);
for k in 0..=PHI_MAX {
let mut got = vec![0.0; n];
phi_action(gen, k, tau, &v, &mut got, &mut sc).unwrap();
let want = dense_phi_k(g, n, tau, k, &v);
let err = sup_diff(&got, &want) / sup(&want).max(1.0);
eprintln!("G_PHI_SHIFT_DENSE {name} z={z} phi_{k}: {err:.3e}");
assert!(err <= 1e-12, "{name} z={z} phi_{k}: {err:e}");
}
let w: Vec<Vec<f64>> = (0..=PHI_MAX).map(|k| lcg_vec(20 + k as u64, n)).collect();
let refs: Vec<&[f64]> = w.iter().map(Vec::as_slice).collect();
let mut got = vec![0.0; n];
phi_combination(gen, tau, &refs, &mut got, &mut sc).unwrap();
let mut want = dense_phi_k(g, n, tau, 0, &w[0]);
for (k, wk) in w.iter().enumerate().skip(1) {
let term = dense_phi_k(g, n, tau, k, wk);
let tk = tau.powi(i32::try_from(k).unwrap());
for (o, t) in want.iter_mut().zip(&term) {
*o += tk * t;
}
}
let err = sup_diff(&got, &want) / sup(&want).max(1.0);
eprintln!("G_PHI_SHIFT_DENSE {name} z={z} combination: {err:.3e}");
assert!(err <= 1e-12, "{name} z={z} combination: {err:e}");
}
#[test]
fn g_phi_shift_dense() {
let n = 10;
let (a, mass) = build_dense(n);
let op = symmetric(&a, n);
let ones = vec![1.0; n];
let (rp, ci, va) = csr(&a, n);
let general = GeneralOperator::from_csr(n, &rp, &ci, &va).unwrap();
for z in [2.0, 20.0, 200.0] {
dense_check(
"csr+mass",
&CsrGenerator::from_symmetric(&op, Some(&mass)).unwrap(),
&dense_g(&a, &mass, n),
z,
);
dense_check(
"csr",
&CsrGenerator::from_symmetric(&op, None).unwrap(),
&dense_g(&a, &ones, n),
z,
);
dense_check(
"general+mass",
&CsrGenerator::from_general(&general, Some(&mass)).unwrap(),
&dense_g(&a, &mass, n),
z,
);
dense_check(
"neg_laplacian",
&NegLaplacianGenerator::new(op.clone()),
&dense_g(&a, &ones, n),
z,
);
none_is_neg_laplacian(&op, z);
}
}
fn none_is_neg_laplacian(op: &SymmetricOperator<f64>, z: f64) {
let (csr, neg) = (
CsrGenerator::from_symmetric(op, None).unwrap(),
NegLaplacianGenerator::new(op.clone()),
);
let tau = z / csr.norm_bound();
let (s0, m0) = phi_cost_probe(csr.norm_bound(), tau);
let shifted = phi_sweep_probe(&neg, tau);
assert!(shifted.0 * shifted.1 < s0 * m0, "z={z}: shift not taken");
let n = op.n();
let v = lcg_vec(23, n);
let mut sc = ScratchPool::new();
let (mut a, mut b) = (vec![0.0; (PHI_MAX + 1) * n], vec![0.0; (PHI_MAX + 1) * n]);
phi_action_batched(&csr, PHI_MAX, tau, &v, &mut a, &mut sc).unwrap();
phi_action_batched(&neg, PHI_MAX, tau, &v, &mut b, &mut sc).unwrap();
let same = a.iter().zip(&b).all(|(x, y)| x.to_bits() == y.to_bits());
assert!(
same,
"z={z}: CsrGenerator vs NegLaplacianGenerator bits differ"
);
}
#[test]
fn g_phi_shift_homogeneity() {
let n = 10;
let (a, mass) = build_dense(n);
let op = symmetric(&a, n);
let gen = CsrGenerator::from_symmetric(&op, Some(&mass)).unwrap();
let tau = 20.0 / gen.norm_bound();
assert_ne!(
phi_sweep_probe(&gen, tau),
phi_cost_probe(gen.norm_bound(), tau),
"shift not taken"
);
let mut sc = ScratchPool::new();
let c = 2.0_f64.powi(40);
let v = lcg_vec(5, n);
let vc: Vec<f64> = v.iter().map(|x| x * c).collect();
for k in 0..=PHI_MAX {
let (mut y, mut yc) = (vec![0.0; n], vec![0.0; n]);
phi_action(&gen, k, tau, &v, &mut y, &mut sc).unwrap();
phi_action(&gen, k, tau, &vc, &mut yc, &mut sc).unwrap();
for (a, b) in y.iter().zip(&yc) {
assert_eq!(
(a * c).to_bits(),
b.to_bits(),
"phi_{k}: not bitwise homogeneous"
);
}
}
let w: Vec<Vec<f64>> = (0..=PHI_MAX).map(|k| lcg_vec(30 + k as u64, n)).collect();
let wc: Vec<Vec<f64>> = w
.iter()
.map(|x| x.iter().map(|y| y * c).collect())
.collect();
let (r, rc): (Vec<&[f64]>, Vec<&[f64]>) = (
w.iter().map(Vec::as_slice).collect(),
wc.iter().map(Vec::as_slice).collect(),
);
let (mut y, mut yc) = (vec![0.0; n], vec![0.0; n]);
phi_combination(&gen, tau, &r, &mut y, &mut sc).unwrap();
phi_combination(&gen, tau, &rc, &mut yc, &mut sc).unwrap();
for (a, b) in y.iter().zip(&yc) {
assert_eq!(
(a * c).to_bits(),
b.to_bits(),
"combination: not bitwise homogeneous"
);
}
}