use scirs2_core::ndarray::{Array1, Array2};
use crate::error::{GraphError, Result};
use crate::signal_processing::gsp::GraphFourierTransform;
#[derive(Debug, Clone)]
pub struct GraphSampling {
pub bandwidth: usize,
}
impl GraphSampling {
pub fn new(bandwidth: usize) -> Self {
Self { bandwidth }
}
pub fn greedy_sampling_set(&self, gft: &GraphFourierTransform) -> Result<Vec<usize>> {
let n = gft.num_nodes();
let k = self.bandwidth;
if k == 0 {
return Ok(Vec::new());
}
if k > n {
return Err(GraphError::InvalidParameter {
param: "bandwidth".into(),
value: k.to_string(),
expected: format!("<= n ({})", n),
context: "GraphSampling".into(),
});
}
let u_k = gft.eigenvectors.slice(scirs2_core::ndarray::s![.., ..k]).to_owned();
let mut selected: Vec<usize> = Vec::with_capacity(k);
let mut remaining: Vec<usize> = (0..n).collect();
for _ in 0..k {
let best = Self::pick_best_node(&u_k, &selected, &remaining, n, k)?;
selected.push(best);
remaining.retain(|&x| x != best);
}
Ok(selected)
}
fn pick_best_node(
u_k: &Array2<f64>,
selected: &[usize],
remaining: &[usize],
_n: usize,
k: usize,
) -> Result<usize> {
let mut best_score = -1.0_f64;
let mut best_node = remaining[0];
for &r in remaining {
let row = u_k.row(r);
let score = if selected.is_empty() {
row.iter().map(|&x| x * x).sum::<f64>()
} else {
let s = selected.len();
let mut sub = Array2::<f64>::zeros((s, k));
for (new_r, &old_r) in selected.iter().enumerate() {
for c in 0..k {
sub[[new_r, c]] = u_k[[old_r, c]];
}
}
let row_vec: Vec<f64> = row.to_vec();
let residual = gram_schmidt_residual(&row_vec, &sub);
residual.iter().map(|&x| x * x).sum::<f64>()
};
if score > best_score {
best_score = score;
best_node = r;
}
}
Ok(best_node)
}
pub fn is_valid_sampling_set(
&self,
gft: &GraphFourierTransform,
set: &[usize],
tol: f64,
) -> Result<bool> {
let k = self.bandwidth;
let n = gft.num_nodes();
if set.len() < k {
return Ok(false);
}
for &s in set {
if s >= n {
return Err(GraphError::InvalidParameter {
param: "set node index".into(),
value: s.to_string(),
expected: format!("< {n}"),
context: "is_valid_sampling_set".into(),
});
}
}
let m = set.len();
let mut r_mat = Array2::<f64>::zeros((m, k));
for (new_r, &old_r) in set.iter().enumerate() {
for c in 0..k {
r_mat[[new_r, c]] = gft.eigenvectors[[old_r, c]];
}
}
let min_sv = min_singular_value(&r_mat);
Ok(min_sv > tol)
}
}
fn gram_schmidt_residual(v: &[f64], basis: &Array2<f64>) -> Vec<f64> {
let k = v.len();
let m = basis.nrows();
let mut res = v.to_vec();
for i in 0..m {
let row = basis.row(i);
let dot: f64 = res.iter().zip(row.iter()).map(|(&a, &b)| a * b).sum();
let norm_sq: f64 = row.iter().map(|&x| x * x).sum();
if norm_sq > 1e-14 {
for (r, &b) in res.iter_mut().zip(row.iter()) {
*r -= (dot / norm_sq) * b;
}
}
}
let _ = k; res
}
fn min_singular_value(a: &Array2<f64>) -> f64 {
let m = a.nrows();
let k = a.ncols();
if m < k {
return 0.0;
}
let mut ata = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
let mut acc = 0.0_f64;
for r in 0..m {
acc += a[[r, i]] * a[[r, j]];
}
ata[[i, j]] = acc;
}
}
let frob: f64 = ata.iter().map(|&x| x * x).sum::<f64>().sqrt();
if frob < 1e-14 {
return 0.0;
}
let mut v: Vec<f64> = (0..k).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
for _ in 0..30 {
let w = solve_linear(&ata, &v);
let norm: f64 = w.iter().map(|&x| x * x).sum::<f64>().sqrt();
if norm < 1e-14 {
return 0.0;
}
v = w.iter().map(|&x| x / norm).collect();
}
let ata_v = matvec(&ata, &v);
let num: f64 = v.iter().zip(ata_v.iter()).map(|(&a, &b)| a * b).sum();
let den: f64 = v.iter().map(|&x| x * x).sum();
if den < 1e-14 {
return 0.0;
}
(num / den).max(0.0).sqrt()
}
fn matvec(a: &Array2<f64>, v: &[f64]) -> Vec<f64> {
let n = a.nrows();
let k = a.ncols();
let mut out = vec![0.0_f64; n];
for i in 0..n {
for j in 0..k {
out[i] += a[[i, j]] * v[j];
}
}
out
}
fn solve_linear(a: &Array2<f64>, b: &[f64]) -> Vec<f64> {
let n = a.nrows();
let mut aug: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row: Vec<f64> = (0..n).map(|j| a[[i, j]]).collect();
row.push(b[i]);
row
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
aug.swap(col, max_row);
let pivot = aug[col][col];
if pivot.abs() < 1e-14 {
return vec![0.0; n];
}
for row in (col + 1)..n {
let factor = aug[row][col] / pivot;
for k in col..=n {
let val = aug[col][k];
aug[row][k] -= factor * val;
}
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut sum = aug[i][n];
for j in (i + 1)..n {
sum -= aug[i][j] * x[j];
}
x[i] = sum / aug[i][i];
}
x
}
#[derive(Debug, Clone)]
pub struct BandlimitedReconstruction {
pub bandwidth: usize,
}
impl BandlimitedReconstruction {
pub fn new(bandwidth: usize) -> Self {
Self { bandwidth }
}
pub fn reconstruct(
&self,
gft: &GraphFourierTransform,
sampling_set: &[usize],
samples: &Array1<f64>,
) -> Result<Array1<f64>> {
let n = gft.num_nodes();
let k = self.bandwidth;
let s = sampling_set.len();
if samples.len() != s {
return Err(GraphError::InvalidParameter {
param: "samples.len()".into(),
value: samples.len().to_string(),
expected: format!("{s} (= |sampling_set|)"),
context: "BandlimitedReconstruction".into(),
});
}
if s < k {
return Err(GraphError::InvalidParameter {
param: "sampling_set.len()".into(),
value: s.to_string(),
expected: format!(">= bandwidth ({})", k),
context: "BandlimitedReconstruction".into(),
});
}
let mut u_s = Array2::<f64>::zeros((s, k));
for (new_r, &old_r) in sampling_set.iter().enumerate() {
if old_r >= n {
return Err(GraphError::InvalidParameter {
param: "sampling_set node".into(),
value: old_r.to_string(),
expected: format!("< {n}"),
context: "BandlimitedReconstruction".into(),
});
}
for c in 0..k {
u_s[[new_r, c]] = gft.eigenvectors[[old_r, c]];
}
}
let mut gram = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
let mut acc = 0.0_f64;
for r in 0..s {
acc += u_s[[r, i]] * u_s[[r, j]];
}
gram[[i, j]] = acc;
}
}
let rhs: Vec<f64> = (0..k)
.map(|c| {
(0..s)
.map(|r| u_s[[r, c]] * samples[r])
.sum::<f64>()
})
.collect();
let alpha = solve_linear(&gram, &rhs);
let mut x = Array1::<f64>::zeros(n);
for i in 0..n {
let mut acc = 0.0_f64;
for c in 0..k {
acc += gft.eigenvectors[[i, c]] * alpha[c];
}
x[i] = acc;
}
Ok(x)
}
}
#[derive(Debug, Clone)]
pub struct GershgorinBound {
pub lambda_max_upper: f64,
pub lambda_min_lower: f64,
pub radii: Vec<f64>,
pub centres: Vec<f64>,
}
impl GershgorinBound {
pub fn from_adjacency(adj: &Array2<f64>) -> Result<Self> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
let mut centres = vec![0.0_f64; n];
let mut radii = vec![0.0_f64; n];
for i in 0..n {
let deg = adj.row(i).iter().map(|&x| x.abs()).sum::<f64>();
centres[i] = deg; radii[i] = deg; }
let lambda_max_upper = centres
.iter()
.zip(radii.iter())
.map(|(&c, &r)| c + r)
.fold(0.0_f64, f64::max);
Ok(Self {
lambda_max_upper,
lambda_min_lower: 0.0,
radii,
centres,
})
}
pub fn signal_bandwidth(
gft: &GraphFourierTransform,
signal: &Array1<f64>,
threshold: f64,
) -> Result<usize> {
let x_hat = gft.transform(signal)?;
let total_energy: f64 = x_hat.iter().map(|&c| c * c).sum();
if total_energy < 1e-14 {
return Ok(0);
}
let mut cumulative = 0.0_f64;
for (k, &c) in x_hat.iter().enumerate() {
cumulative += c * c;
if cumulative / total_energy >= threshold {
return Ok(k + 1);
}
}
Ok(x_hat.len())
}
}
#[derive(Debug, Clone)]
pub struct GraphUncertaintyPrinciple {
pub spatial_distances_sq: Array1<f64>,
pub ref_frequency: f64,
}
impl GraphUncertaintyPrinciple {
pub fn new(adj: &Array2<f64>, v0: usize, ref_frequency: f64) -> Result<Self> {
let n = adj.nrows();
if v0 >= n {
return Err(GraphError::InvalidParameter {
param: "v0".into(),
value: v0.to_string(),
expected: format!("< {n}"),
context: "GraphUncertaintyPrinciple".into(),
});
}
let mut dist = vec![f64::INFINITY; n];
dist[v0] = 0.0;
let mut queue = std::collections::VecDeque::new();
queue.push_back(v0);
while let Some(u) = queue.pop_front() {
for v in 0..n {
if adj[[u, v]] != 0.0 && dist[v].is_infinite() {
dist[v] = dist[u] + 1.0;
queue.push_back(v);
}
}
}
let spatial_distances_sq = Array1::from_iter(dist.iter().map(|&d| d * d));
Ok(Self { spatial_distances_sq, ref_frequency })
}
pub fn spatial_spread(&self, signal: &Array1<f64>) -> Result<f64> {
let n = signal.len();
if n != self.spatial_distances_sq.len() {
return Err(GraphError::InvalidParameter {
param: "signal.len()".into(),
value: n.to_string(),
expected: self.spatial_distances_sq.len().to_string(),
context: "spatial_spread".into(),
});
}
let norm_sq: f64 = signal.iter().map(|&x| x * x).sum();
if norm_sq < 1e-14 {
return Ok(0.0);
}
let spread_sq: f64 = signal
.iter()
.zip(self.spatial_distances_sq.iter())
.map(|(&x, &d2)| d2 * x * x)
.sum::<f64>()
/ norm_sq;
Ok(spread_sq.sqrt())
}
pub fn spectral_spread(&self, gft: &GraphFourierTransform, signal: &Array1<f64>) -> Result<f64> {
let x_hat = gft.transform(signal)?;
let norm_sq: f64 = x_hat.iter().map(|&c| c * c).sum();
if norm_sq < 1e-14 {
return Ok(0.0);
}
let spread_sq: f64 = x_hat
.iter()
.zip(gft.eigenvalues.iter())
.map(|(&c, &lam)| {
let d = lam - self.ref_frequency;
d * d * c * c
})
.sum::<f64>()
/ norm_sq;
Ok(spread_sq.sqrt())
}
pub fn uncertainty(
&self,
gft: &GraphFourierTransform,
signal: &Array1<f64>,
) -> Result<(f64, f64, f64)> {
let dv = self.spatial_spread(signal)?;
let ds = self.spectral_spread(gft, signal)?;
Ok((dv, ds, dv * ds))
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
fn path_adj(n: usize) -> Array2<f64> {
let mut adj = Array2::<f64>::zeros((n, n));
for i in 0..(n - 1) {
adj[[i, i + 1]] = 1.0;
adj[[i + 1, i]] = 1.0;
}
adj
}
#[test]
fn test_greedy_sampling_set_size() {
let adj = path_adj(8);
let gft = GraphFourierTransform::from_adjacency(&adj).unwrap();
let sampler = GraphSampling::new(3);
let set = sampler.greedy_sampling_set(&gft).unwrap();
assert_eq!(set.len(), 3);
for &s in &set {
assert!(s < 8);
}
let mut uniq = set.clone();
uniq.sort();
uniq.dedup();
assert_eq!(uniq.len(), set.len());
}
#[test]
fn test_bandlimited_reconstruction() {
let n = 6;
let adj = path_adj(n);
let gft = GraphFourierTransform::from_adjacency(&adj).unwrap();
let k = 2;
let mut x_hat = Array1::<f64>::zeros(n);
x_hat[0] = 2.0;
x_hat[1] = 1.0;
let original = gft.inverse(&x_hat).unwrap();
let set: Vec<usize> = (0..n).collect();
let samples = Array1::from_iter(set.iter().map(|&i| original[i]));
let rec = BandlimitedReconstruction::new(k)
.reconstruct(&gft, &set, &samples)
.unwrap();
for (a, b) in original.iter().zip(rec.iter()) {
assert!((a - b).abs() < 1e-8, "Reconstruction mismatch: {a} vs {b}");
}
}
#[test]
fn test_gershgorin_bounds() {
let adj = path_adj(5);
let bounds = GershgorinBound::from_adjacency(&adj).unwrap();
assert!(bounds.lambda_min_lower == 0.0);
assert!(bounds.lambda_max_upper > 0.0);
assert!(bounds.lambda_max_upper <= 4.0 + 1e-9);
}
#[test]
fn test_signal_bandwidth() {
let adj = path_adj(8);
let gft = GraphFourierTransform::from_adjacency(&adj).unwrap();
let dc = Array1::from_vec(vec![1.0; 8]);
let bw = GershgorinBound::signal_bandwidth(&gft, &dc, 0.99).unwrap();
assert!(bw <= 2, "DC signal should have bandwidth 1 (got {bw})");
}
#[test]
fn test_uncertainty_principle() {
let adj = path_adj(7);
let gft = GraphFourierTransform::from_adjacency(&adj).unwrap();
let up = GraphUncertaintyPrinciple::new(&adj, 3, 0.0).unwrap();
let mut local = Array1::<f64>::zeros(7);
local[3] = 1.0;
let (dv, ds, prod) = up.uncertainty(&gft, &local).unwrap();
assert!(dv >= 0.0);
assert!(ds >= 0.0);
assert!(prod >= 0.0);
assert!(dv < 1.0, "Vertex-localised signal should have small dv: {dv}");
}
}