use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KaczmarzVariant {
Classic,
Randomized,
}
#[derive(Debug, Clone)]
pub struct KaczmarzConfig {
pub max_iter: usize,
pub tol: f64,
pub variant: KaczmarzVariant,
pub seed: u64,
pub relaxation: f64,
pub track_residuals: bool,
}
impl Default for KaczmarzConfig {
fn default() -> Self {
Self {
max_iter: 1000,
tol: 1e-8,
variant: KaczmarzVariant::Classic,
seed: 42,
relaxation: 1.0,
track_residuals: false,
}
}
}
#[derive(Debug, Clone)]
pub struct KaczmarzResult {
pub x: Array1<f64>,
pub residual_norm: f64,
pub iterations: usize,
pub converged: bool,
pub residual_history: Vec<f64>,
}
pub struct KaczmarzSolver {
config: KaczmarzConfig,
}
impl KaczmarzSolver {
pub fn new(config: KaczmarzConfig) -> Self {
Self { config }
}
pub fn default_solver() -> Self {
Self::new(KaczmarzConfig::default())
}
pub fn solve(
&self,
a: &Array2<f64>,
b: &Array1<f64>,
x0: Option<&Array1<f64>>,
) -> OptimizeResult<KaczmarzResult> {
let m = a.nrows();
let n = a.ncols();
if b.len() != m {
return Err(OptimizeError::InvalidInput(format!(
"Dimension mismatch: A is {}x{} but b has length {}",
m,
n,
b.len()
)));
}
if let Some(x_init) = x0 {
if x_init.len() != n {
return Err(OptimizeError::InvalidInput(format!(
"Initial guess has length {} but A has {} columns",
x_init.len(),
n
)));
}
}
let mut x = match x0 {
Some(x_init) => x_init.clone(),
None => Array1::zeros(n),
};
let row_norms_sq: Vec<f64> = (0..m)
.map(|i| {
let row = a.row(i);
row.dot(&row)
})
.collect();
for (i, &norm_sq) in row_norms_sq.iter().enumerate() {
if norm_sq < 1e-30 && b[i].abs() > 1e-15 {
return Err(OptimizeError::InvalidInput(format!(
"Row {} of A is zero but b[{}] = {} is nonzero: inconsistent system",
i, i, b[i]
)));
}
}
let mut rng = StdRng::seed_from_u64(self.config.seed);
let mut residual_history = Vec::new();
let frobenius_sq: f64 = row_norms_sq.iter().sum();
let mut converged = false;
let mut iterations = 0;
for iter in 0..self.config.max_iter {
iterations = iter + 1;
for _step in 0..m {
let row_idx = match self.config.variant {
KaczmarzVariant::Classic => _step,
KaczmarzVariant::Randomized => {
if frobenius_sq < 1e-30 {
_step } else {
let threshold: f64 = rng.random::<f64>() * frobenius_sq;
let mut cumsum = 0.0;
let mut selected = m - 1;
for (i, &norm_sq) in row_norms_sq.iter().enumerate() {
cumsum += norm_sq;
if cumsum >= threshold {
selected = i;
break;
}
}
selected
}
}
};
let norm_sq = row_norms_sq[row_idx];
if norm_sq < 1e-30 {
continue; }
let row = a.row(row_idx);
let dot_ax = row.dot(&x);
let residual_i = b[row_idx] - dot_ax;
let scale = self.config.relaxation * residual_i / norm_sq;
for j in 0..n {
x[j] += scale * row[j];
}
}
let residual = a.dot(&x) - b;
let res_norm = residual.dot(&residual).sqrt();
if self.config.track_residuals {
residual_history.push(res_norm);
}
if res_norm < self.config.tol {
converged = true;
break;
}
}
let final_residual = a.dot(&x) - b;
let residual_norm = final_residual.dot(&final_residual).sqrt();
Ok(KaczmarzResult {
x,
residual_norm,
iterations,
converged,
residual_history,
})
}
}
pub struct BlockKaczmarz {
config: KaczmarzConfig,
block_size: usize,
}
impl BlockKaczmarz {
pub fn new(config: KaczmarzConfig, block_size: usize) -> Self {
Self { config, block_size }
}
pub fn solve(
&self,
a: &Array2<f64>,
b: &Array1<f64>,
x0: Option<&Array1<f64>>,
) -> OptimizeResult<KaczmarzResult> {
let m = a.nrows();
let n = a.ncols();
if b.len() != m {
return Err(OptimizeError::InvalidInput(format!(
"Dimension mismatch: A is {}x{} but b has length {}",
m,
n,
b.len()
)));
}
let block_size = self.block_size.min(m).max(1);
let mut x = match x0 {
Some(x_init) => {
if x_init.len() != n {
return Err(OptimizeError::InvalidInput(format!(
"Initial guess has length {} but A has {} columns",
x_init.len(),
n
)));
}
x_init.clone()
}
None => Array1::zeros(n),
};
let mut blocks: Vec<(usize, usize)> = Vec::new();
let mut start = 0;
while start < m {
let end = (start + block_size).min(m);
blocks.push((start, end));
start = end;
}
let mut residual_history = Vec::new();
let mut converged = false;
let mut iterations = 0;
for iter in 0..self.config.max_iter {
iterations = iter + 1;
for &(block_start, block_end) in &blocks {
let bk_size = block_end - block_start;
let a_block = a.slice(s![block_start..block_end, ..]);
let b_block = b.slice(s![block_start..block_end]);
let ax_block = block_mat_vec(&a_block, &x.view());
let mut r_block = Array1::zeros(bk_size);
for i in 0..bk_size {
r_block[i] = b_block[i] - ax_block[i];
}
let aat = block_aat(&a_block);
let z = match solve_small_system(&aat, &r_block) {
Some(z) => z,
None => continue, };
for j in 0..n {
let mut update = 0.0;
for i in 0..bk_size {
update += a_block[[i, j]] * z[i];
}
x[j] += self.config.relaxation * update;
}
}
let residual = a.dot(&x) - b;
let res_norm = residual.dot(&residual).sqrt();
if self.config.track_residuals {
residual_history.push(res_norm);
}
if res_norm < self.config.tol {
converged = true;
break;
}
}
let final_residual = a.dot(&x) - b;
let residual_norm = final_residual.dot(&final_residual).sqrt();
Ok(KaczmarzResult {
x,
residual_norm,
iterations,
converged,
residual_history,
})
}
}
pub struct ExtendedKaczmarz {
config: KaczmarzConfig,
}
impl ExtendedKaczmarz {
pub fn new(config: KaczmarzConfig) -> Self {
Self { config }
}
pub fn solve(
&self,
a: &Array2<f64>,
b: &Array1<f64>,
x0: Option<&Array1<f64>>,
) -> OptimizeResult<KaczmarzResult> {
let m = a.nrows();
let n = a.ncols();
if b.len() != m {
return Err(OptimizeError::InvalidInput(format!(
"Dimension mismatch: A is {}x{} but b has length {}",
m,
n,
b.len()
)));
}
let mut x = match x0 {
Some(x_init) => {
if x_init.len() != n {
return Err(OptimizeError::InvalidInput(format!(
"Initial guess has length {} but A has {} columns",
x_init.len(),
n
)));
}
x_init.clone()
}
None => Array1::zeros(n),
};
let mut z: Array1<f64> = b.clone();
let row_norms_sq: Vec<f64> = (0..m)
.map(|i| {
let row = a.row(i);
row.dot(&row)
})
.collect();
let col_norms_sq: Vec<f64> = (0..n)
.map(|j| {
let col = a.column(j);
col.dot(&col)
})
.collect();
let frobenius_sq: f64 = row_norms_sq.iter().sum();
let mut rng = StdRng::seed_from_u64(self.config.seed);
let mut residual_history = Vec::new();
let mut converged = false;
let mut iterations = 0;
for iter in 0..self.config.max_iter {
iterations = iter + 1;
for _step in 0..m {
let col_idx = match self.config.variant {
KaczmarzVariant::Randomized => {
let frobenius_sq_cols: f64 = col_norms_sq.iter().sum();
if frobenius_sq_cols < 1e-30 {
_step % n
} else {
let threshold = rng.random::<f64>() * frobenius_sq_cols;
let mut cumsum = 0.0;
let mut selected = n - 1;
for (j, &ns) in col_norms_sq.iter().enumerate() {
cumsum += ns;
if cumsum >= threshold {
selected = j;
break;
}
}
selected
}
}
KaczmarzVariant::Classic => _step % n,
};
let col_norm_sq = col_norms_sq[col_idx];
if col_norm_sq > 1e-30 {
let a_col = a.column(col_idx);
let dot_val: f64 = a_col.dot(&z);
let scale: f64 = dot_val / col_norm_sq;
for i in 0..m {
z[i] -= scale * a_col[i];
}
}
let row_idx = match self.config.variant {
KaczmarzVariant::Randomized => {
if frobenius_sq < 1e-30 {
_step
} else {
let threshold = rng.random::<f64>() * frobenius_sq;
let mut cumsum = 0.0;
let mut selected = m - 1;
for (i, &ns) in row_norms_sq.iter().enumerate() {
cumsum += ns;
if cumsum >= threshold {
selected = i;
break;
}
}
selected
}
}
KaczmarzVariant::Classic => _step,
};
let norm_sq = row_norms_sq[row_idx];
if norm_sq < 1e-30 {
continue;
}
let row = a.row(row_idx);
let dot_ax: f64 = row.dot(&x);
let target: f64 = b[row_idx] - z[row_idx];
let residual_i: f64 = target - dot_ax;
let scale: f64 = self.config.relaxation * residual_i / norm_sq;
for j in 0..n {
x[j] += scale * row[j];
}
}
let residual = a.dot(&x) - b;
let res_norm = residual.dot(&residual).sqrt();
if self.config.track_residuals {
residual_history.push(res_norm);
}
if res_norm < self.config.tol {
converged = true;
break;
}
}
let final_residual = a.dot(&x) - b;
let residual_norm = final_residual.dot(&final_residual).sqrt();
Ok(KaczmarzResult {
x,
residual_norm,
iterations,
converged,
residual_history,
})
}
}
fn block_mat_vec(a_block: &ArrayView2<f64>, x: &ArrayView1<f64>) -> Array1<f64> {
let m = a_block.nrows();
let n = a_block.ncols();
let mut result = Array1::zeros(m);
for i in 0..m {
let mut sum = 0.0;
for j in 0..n {
sum += a_block[[i, j]] * x[j];
}
result[i] = sum;
}
result
}
fn block_aat(a_block: &ArrayView2<f64>) -> Array2<f64> {
let m = a_block.nrows();
let n = a_block.ncols();
let mut result = Array2::zeros((m, m));
for i in 0..m {
for j in 0..=i {
let mut sum = 0.0;
for k in 0..n {
sum += a_block[[i, k]] * a_block[[j, k]];
}
result[[i, j]] = sum;
result[[j, i]] = sum;
}
}
result
}
fn solve_small_system(a: &Array2<f64>, b: &Array1<f64>) -> Option<Array1<f64>> {
let n = a.nrows();
if n == 0 || a.ncols() != n || b.len() != n {
return None;
}
let mut aug = Array2::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n]] = b[i];
}
for col in 0..n {
let mut max_val = aug[[col, col]].abs();
let mut max_row = col;
for row in (col + 1)..n {
let val = aug[[row, col]].abs();
if val > max_val {
max_val = val;
max_row = row;
}
}
if max_val < 1e-14 {
return None; }
if max_row != col {
for j in 0..=n {
let tmp = aug[[col, j]];
aug[[col, j]] = aug[[max_row, j]];
aug[[max_row, j]] = tmp;
}
}
let pivot = aug[[col, col]];
for row in (col + 1)..n {
let factor = aug[[row, col]] / pivot;
for j in col..=n {
let val = aug[[col, j]];
aug[[row, j]] -= factor * val;
}
}
}
let mut x = Array1::zeros(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];
}
if aug[[i, i]].abs() < 1e-14 {
return None;
}
x[i] = sum / aug[[i, i]];
}
Some(x)
}
pub fn kaczmarz_solve(
a: &Array2<f64>,
b: &Array1<f64>,
config: Option<KaczmarzConfig>,
) -> OptimizeResult<KaczmarzResult> {
let config = config.unwrap_or_default();
let solver = KaczmarzSolver::new(config);
solver.solve(a, b, None)
}
pub fn randomized_kaczmarz_solve(
a: &Array2<f64>,
b: &Array1<f64>,
seed: u64,
config: Option<KaczmarzConfig>,
) -> OptimizeResult<KaczmarzResult> {
let mut config = config.unwrap_or_default();
config.variant = KaczmarzVariant::Randomized;
config.seed = seed;
let solver = KaczmarzSolver::new(config);
solver.solve(a, b, None)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{array, Array1, Array2};
#[test]
fn test_classic_kaczmarz_exact_system() {
let a = Array2::eye(2);
let b = array![3.0, 4.0];
let config = KaczmarzConfig {
max_iter: 100,
tol: 1e-10,
variant: KaczmarzVariant::Classic,
..Default::default()
};
let result = kaczmarz_solve(&a, &b, Some(config));
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(result.converged);
assert!((result.x[0] - 3.0).abs() < 1e-8, "x[0]={}", result.x[0]);
assert!((result.x[1] - 4.0).abs() < 1e-8, "x[1]={}", result.x[1]);
}
#[test]
fn test_classic_kaczmarz_nontrivial() {
let a = array![[2.0, 1.0], [1.0, 3.0]];
let b = array![5.0, 10.0];
let config = KaczmarzConfig {
max_iter: 5000,
tol: 1e-8,
variant: KaczmarzVariant::Classic,
relaxation: 0.5, ..Default::default()
};
let result = kaczmarz_solve(&a, &b, Some(config));
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(
result.converged,
"Did not converge, residual={}",
result.residual_norm
);
assert!((result.x[0] - 1.0).abs() < 1e-4, "x[0]={}", result.x[0]);
assert!((result.x[1] - 3.0).abs() < 1e-4, "x[1]={}", result.x[1]);
}
#[test]
fn test_randomized_kaczmarz_overdetermined() {
let a = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let b = array![1.0, 2.0, 3.0];
let config = KaczmarzConfig {
max_iter: 5000,
tol: 1e-8,
variant: KaczmarzVariant::Randomized,
seed: 42,
relaxation: 0.8,
..Default::default()
};
let result = kaczmarz_solve(&a, &b, Some(config));
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(
result.converged,
"Did not converge, residual={}",
result.residual_norm
);
assert!((result.x[0] - 1.0).abs() < 1e-4, "x[0]={}", result.x[0]);
assert!((result.x[1] - 2.0).abs() < 1e-4, "x[1]={}", result.x[1]);
}
#[test]
fn test_block_kaczmarz() {
let a = array![
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 1.0, 1.0]
];
let b = array![1.0, 2.0, 3.0, 6.0];
let config = KaczmarzConfig {
max_iter: 5000,
tol: 1e-8,
relaxation: 0.5,
..Default::default()
};
let solver = BlockKaczmarz::new(config, 2);
let result = solver.solve(&a, &b, None);
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(
result.converged,
"Did not converge, residual={}",
result.residual_norm
);
assert!((result.x[0] - 1.0).abs() < 1e-4, "x[0]={}", result.x[0]);
assert!((result.x[1] - 2.0).abs() < 1e-4, "x[1]={}", result.x[1]);
assert!((result.x[2] - 3.0).abs() < 1e-4, "x[2]={}", result.x[2]);
}
#[test]
fn test_extended_kaczmarz_inconsistent() {
let a = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let b = array![1.0, 2.0, 4.0];
let config = KaczmarzConfig {
max_iter: 10000,
tol: 1e-6,
variant: KaczmarzVariant::Randomized,
seed: 42,
relaxation: 0.5,
..Default::default()
};
let solver = ExtendedKaczmarz::new(config);
let result = solver.solve(&a, &b, None);
assert!(result.is_ok());
let result = result.expect("should succeed");
let expected_x0 = 4.0 / 3.0;
let expected_x1 = 7.0 / 3.0;
assert!(
(result.x[0] - expected_x0).abs() < 0.5,
"x[0]={}, expected ~{}",
result.x[0],
expected_x0
);
assert!(
(result.x[1] - expected_x1).abs() < 0.5,
"x[1]={}, expected ~{}",
result.x[1],
expected_x1
);
}
#[test]
fn test_residual_tracking() {
let a = Array2::eye(3);
let b = array![1.0, 2.0, 3.0];
let config = KaczmarzConfig {
max_iter: 20,
tol: 1e-20, track_residuals: true,
..Default::default()
};
let result = kaczmarz_solve(&a, &b, Some(config));
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(!result.residual_history.is_empty());
assert!(
result
.residual_history
.last()
.copied()
.unwrap_or(f64::INFINITY)
< 1e-10
);
}
#[test]
fn test_dimension_mismatch() {
let a = Array2::eye(3);
let b = array![1.0, 2.0];
let result = kaczmarz_solve(&a, &b, None);
assert!(result.is_err());
}
#[test]
fn test_with_initial_guess() {
let a = Array2::eye(2);
let b = array![5.0, 7.0];
let x0 = array![4.9, 6.9];
let config = KaczmarzConfig {
max_iter: 10,
tol: 1e-10,
..Default::default()
};
let solver = KaczmarzSolver::new(config);
let result = solver.solve(&a, &b, Some(&x0));
assert!(result.is_ok());
let result = result.expect("should succeed");
assert!(result.converged);
assert!((result.x[0] - 5.0).abs() < 1e-8);
assert!((result.x[1] - 7.0).abs() < 1e-8);
}
}