use crate::models::CsrMatrix;
use crate::{GraphError, Result};
#[derive(Debug, Clone)]
pub struct SpmvConfig {
pub parallel: bool,
pub alpha: f64,
}
impl Default for SpmvConfig {
fn default() -> Self {
Self {
parallel: false,
alpha: 1.0,
}
}
}
impl SpmvConfig {
pub fn new() -> Self {
Self::default()
}
pub fn parallel(mut self) -> Self {
self.parallel = true;
self
}
pub fn with_alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
}
pub fn spmv(matrix: &CsrMatrix, x: &[f64]) -> Result<Vec<f64>> {
spmv_with_config(matrix, x, &SpmvConfig::default())
}
pub fn spmv_with_config(matrix: &CsrMatrix, x: &[f64], config: &SpmvConfig) -> Result<Vec<f64>> {
if x.len() != matrix.num_cols {
return Err(GraphError::DimensionMismatch {
expected: matrix.num_cols,
actual: x.len(),
});
}
if config.parallel {
spmv_parallel_impl(matrix, x, config.alpha)
} else {
spmv_sequential_impl(matrix, x, config.alpha)
}
}
fn spmv_sequential_impl(matrix: &CsrMatrix, x: &[f64], alpha: f64) -> Result<Vec<f64>> {
let mut y = vec![0.0; matrix.num_rows];
for (row, y_row) in y.iter_mut().enumerate() {
let start = matrix.row_ptr[row] as usize;
let end = matrix.row_ptr[row + 1] as usize;
let mut sum = 0.0;
for i in start..end {
let col = matrix.col_idx[i] as usize;
let val = matrix.values.as_ref().map(|v| v[i]).unwrap_or(1.0);
sum += val * x[col];
}
*y_row = alpha * sum;
}
Ok(y)
}
pub fn spmv_parallel(matrix: &CsrMatrix, x: &[f64]) -> Result<Vec<f64>> {
spmv_with_config(matrix, x, &SpmvConfig::default().parallel())
}
fn spmv_parallel_impl(matrix: &CsrMatrix, x: &[f64], alpha: f64) -> Result<Vec<f64>> {
let y: Vec<f64> = (0..matrix.num_rows)
.map(|row| {
let start = matrix.row_ptr[row] as usize;
let end = matrix.row_ptr[row + 1] as usize;
let mut sum = 0.0;
for i in start..end {
let col = matrix.col_idx[i] as usize;
let val = matrix.values.as_ref().map(|v| v[i]).unwrap_or(1.0);
sum += val * x[col];
}
alpha * sum
})
.collect();
Ok(y)
}
pub fn spmv_axpby(
matrix: &CsrMatrix,
x: &[f64],
y: &mut [f64],
alpha: f64,
beta: f64,
) -> Result<()> {
if x.len() != matrix.num_cols {
return Err(GraphError::DimensionMismatch {
expected: matrix.num_cols,
actual: x.len(),
});
}
if y.len() != matrix.num_rows {
return Err(GraphError::DimensionMismatch {
expected: matrix.num_rows,
actual: y.len(),
});
}
for (row, y_row) in y.iter_mut().enumerate() {
let start = matrix.row_ptr[row] as usize;
let end = matrix.row_ptr[row + 1] as usize;
let mut sum = 0.0;
for i in start..end {
let col = matrix.col_idx[i] as usize;
let val = matrix.values.as_ref().map(|v| v[i]).unwrap_or(1.0);
sum += val * x[col];
}
*y_row = alpha * sum + beta * *y_row;
}
Ok(())
}
pub fn dot(x: &[f64], y: &[f64]) -> f64 {
x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum()
}
pub fn norm2(x: &[f64]) -> f64 {
dot(x, x).sqrt()
}
pub fn normalize(x: &mut [f64]) {
let n = norm2(x);
if n > 1e-10 {
for xi in x.iter_mut() {
*xi /= n;
}
}
}
pub fn power_iteration(
matrix: &CsrMatrix,
max_iterations: usize,
tolerance: f64,
) -> Result<(Vec<f64>, f64)> {
if matrix.num_rows == 0 {
return Err(GraphError::EmptyGraph);
}
let n = matrix.num_rows;
let mut x = vec![1.0 / (n as f64).sqrt(); n];
let mut eigenvalue = 0.0;
for _ in 0..max_iterations {
let y = spmv(matrix, &x)?;
let new_eigenvalue = dot(&x, &y);
let norm = norm2(&y);
if norm < 1e-10 {
break;
}
x = y.into_iter().map(|yi| yi / norm).collect();
if (new_eigenvalue - eigenvalue).abs() < tolerance {
return Ok((x, new_eigenvalue));
}
eigenvalue = new_eigenvalue;
}
Ok((x, eigenvalue))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spmv_identity() {
let edges = [(0, 0, 1.0), (1, 1, 1.0), (2, 2, 1.0)];
let matrix = CsrMatrix::from_weighted_edges(3, &edges);
let x = vec![1.0, 2.0, 3.0];
let y = spmv(&matrix, &x).unwrap();
assert!((y[0] - 1.0).abs() < 1e-10);
assert!((y[1] - 2.0).abs() < 1e-10);
assert!((y[2] - 3.0).abs() < 1e-10);
}
#[test]
fn test_spmv_unweighted() {
let edges = [(0, 1), (0, 2), (1, 2)];
let matrix = CsrMatrix::from_edges(3, &edges);
let x = vec![1.0, 1.0, 1.0];
let y = spmv(&matrix, &x).unwrap();
assert!((y[0] - 2.0).abs() < 1e-10);
assert!((y[1] - 1.0).abs() < 1e-10);
assert!((y[2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_spmv_weighted() {
let edges = [(0, 1, 2.0), (0, 2, 3.0), (1, 2, 4.0)];
let matrix = CsrMatrix::from_weighted_edges(3, &edges);
let x = vec![1.0, 1.0, 1.0];
let y = spmv(&matrix, &x).unwrap();
assert!((y[0] - 5.0).abs() < 1e-10);
assert!((y[1] - 4.0).abs() < 1e-10);
assert!((y[2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_spmv_alpha() {
let edges = [(0, 1, 1.0)];
let matrix = CsrMatrix::from_weighted_edges(2, &edges);
let x = vec![1.0, 2.0];
let config = SpmvConfig::new().with_alpha(0.5);
let y = spmv_with_config(&matrix, &x, &config).unwrap();
assert!((y[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_spmv_dimension_mismatch() {
let matrix = CsrMatrix::from_edges(3, &[(0, 1)]);
let x = vec![1.0, 2.0];
let result = spmv(&matrix, &x);
assert!(matches!(result, Err(GraphError::DimensionMismatch { .. })));
}
#[test]
fn test_spmv_parallel_same_as_sequential() {
let edges = [(0, 1, 1.0), (0, 2, 2.0), (1, 2, 3.0), (2, 0, 4.0)];
let matrix = CsrMatrix::from_weighted_edges(3, &edges);
let x = vec![1.0, 2.0, 3.0];
let seq = spmv(&matrix, &x).unwrap();
let par = spmv_parallel(&matrix, &x).unwrap();
for i in 0..3 {
assert!((seq[i] - par[i]).abs() < 1e-10);
}
}
#[test]
fn test_spmv_axpby() {
let edges = [(0, 1, 1.0)];
let matrix = CsrMatrix::from_weighted_edges(2, &edges);
let x = vec![1.0, 2.0];
let mut y = vec![1.0, 1.0];
spmv_axpby(&matrix, &x, &mut y, 2.0, 3.0).unwrap();
assert!((y[0] - 7.0).abs() < 1e-10);
assert!((y[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_dot_product() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![4.0, 5.0, 6.0];
assert!((dot(&x, &y) - 32.0).abs() < 1e-10);
}
#[test]
fn test_norm2() {
let x = vec![3.0, 4.0];
assert!((norm2(&x) - 5.0).abs() < 1e-10);
}
#[test]
fn test_normalize() {
let mut x = vec![3.0, 4.0];
normalize(&mut x);
assert!((norm2(&x) - 1.0).abs() < 1e-10);
assert!((x[0] - 0.6).abs() < 1e-10);
assert!((x[1] - 0.8).abs() < 1e-10);
}
#[test]
fn test_power_iteration() {
let mut builder = crate::models::CsrMatrixBuilder::new(2);
builder.add_weighted_edge(0, 0, 2.0);
builder.add_weighted_edge(0, 1, 1.0);
builder.add_weighted_edge(1, 0, 1.0);
builder.add_weighted_edge(1, 1, 2.0);
let matrix = builder.build();
let (eigenvector, eigenvalue) = power_iteration(&matrix, 100, 1e-6).unwrap();
assert!((eigenvalue - 3.0).abs() < 0.01);
let expected = 1.0 / 2.0_f64.sqrt();
assert!((eigenvector[0].abs() - expected).abs() < 0.01);
assert!((eigenvector[1].abs() - expected).abs() < 0.01);
}
}