pub mod classical;
pub mod sketched_solver;
pub mod sparse_sketch;
pub mod types;
pub use classical::kaczmarz_solve as kaczmarz_solve_ext;
pub use sketched_solver::{iterative_sketching, sketch_and_solve};
pub use sparse_sketch::{apply_sketch, apply_sketch_to_vec, build_sketch};
pub use types::{
KaczmarzConfigExt, KaczmarzVariantExt, ProjectionResult, SketchConfig, SketchTypeExt,
};
use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::ndarray::{s, Array1, Array2};
use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum KaczmarzVariant {
Randomized,
Block,
GreedyMaxResidual,
Cyclic,
}
impl Default for KaczmarzVariant {
fn default() -> Self {
KaczmarzVariant::Randomized
}
}
#[derive(Clone, Debug)]
pub struct KaczmarzConfig {
pub variant: KaczmarzVariant,
pub max_iter: usize,
pub tol: f64,
pub block_size: usize,
pub seed: u64,
pub relaxation: f64,
}
impl Default for KaczmarzConfig {
fn default() -> Self {
Self {
variant: KaczmarzVariant::Randomized,
max_iter: 10_000,
tol: 1e-6,
block_size: 32,
seed: 42,
relaxation: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct KaczmarzResult {
pub x: Vec<f64>,
pub residual_norm: f64,
pub n_iter: usize,
pub converged: bool,
}
fn row_dot(a: &Array2<f64>, row: usize, x: &[f64]) -> f64 {
let r = a.row(row);
r.iter().zip(x.iter()).map(|(ai, xi)| ai * xi).sum()
}
fn row_norm_sq(a: &Array2<f64>, row: usize) -> f64 {
let r = a.row(row);
r.iter().map(|ai| ai * ai).sum()
}
fn sample_row(probs: &[f64], rng: &mut StdRng) -> usize {
let u: f64 = rng.random::<f64>();
let mut cumsum = 0.0;
for (i, &p) in probs.iter().enumerate() {
cumsum += p;
if u <= cumsum {
return i;
}
}
probs.len() - 1
}
fn kaczmarz_update(a: &Array2<f64>, b: &[f64], x: &mut Vec<f64>, row: usize, omega: f64) {
let row_ns = row_norm_sq(a, row);
if row_ns < f64::EPSILON {
return;
}
let residual = b[row] - row_dot(a, row, x);
let step = omega * residual / row_ns;
let ai = a.row(row);
for (xi, aij) in x.iter_mut().zip(ai.iter()) {
*xi += step * aij;
}
}
fn block_kaczmarz_update(
a: &Array2<f64>,
b: &[f64],
x: &mut Vec<f64>,
block: &[usize],
omega: f64,
) {
let k = block.len();
let n = x.len();
if k == 0 {
return;
}
let mut ab = vec![0.0f64; k * n]; let mut rb = vec![0.0f64; k];
for (bi, &row) in block.iter().enumerate() {
let ai = a.row(row);
for j in 0..n {
ab[bi * n + j] = ai[j];
}
rb[bi] = b[row] - row_dot(a, row, x);
}
let mut gram = vec![0.0f64; k * k];
for i in 0..k {
for j in 0..k {
let mut dot = 0.0;
for l in 0..n {
dot += ab[i * n + l] * ab[j * n + l];
}
gram[i * k + j] = dot;
}
}
let ridge = 1e-12;
for i in 0..k {
gram[i * k + i] += ridge;
}
let alpha = match cholesky_solve(&gram, k, &rb) {
Ok(a) => a,
Err(_) => {
for &row in block {
kaczmarz_update(a, b, x, row, omega);
}
return;
}
};
let mut delta = vec![0.0f64; n];
for j in 0..n {
for bi in 0..k {
delta[j] += ab[bi * n + j] * alpha[bi];
}
}
for (xi, di) in x.iter_mut().zip(delta.iter()) {
*xi += omega * di;
}
}
fn forward_substitution(l: &[f64], n: usize, b: &[f64]) -> Vec<f64> {
let mut y = vec![0.0f64; n];
for i in 0..n {
let mut sum = b[i];
for j in 0..i {
sum -= l[i * n + j] * y[j];
}
let diag = l[i * n + i];
y[i] = if diag.abs() > f64::EPSILON {
sum / diag
} else {
0.0
};
}
y
}
fn back_substitution(l: &[f64], n: usize, z: &[f64]) -> Vec<f64> {
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
let mut sum = z[i];
for j in (i + 1)..n {
sum -= l[j * n + i] * x[j]; }
let diag = l[i * n + i];
x[i] = if diag.abs() > f64::EPSILON {
sum / diag
} else {
0.0
};
}
x
}
fn cholesky_solve(a: &[f64], k: usize, b: &[f64]) -> Result<Vec<f64>, ()> {
let mut l = vec![0.0f64; k * k];
for i in 0..k {
for j in 0..=i {
let mut sum = a[i * k + j];
for p in 0..j {
sum -= l[i * k + p] * l[j * k + p];
}
if i == j {
if sum < 0.0 {
return Err(());
}
l[i * k + j] = sum.sqrt();
} else {
let ljj = l[j * k + j];
if ljj.abs() < f64::EPSILON {
return Err(());
}
l[i * k + j] = sum / ljj;
}
}
}
let y = forward_substitution(&l, k, b);
let x = back_substitution(&l, k, &y);
Ok(x)
}
fn residual_norm(a: &Array2<f64>, b: &[f64], x: &[f64]) -> f64 {
let m = a.nrows();
let mut norm_sq = 0.0;
for i in 0..m {
let r = b[i] - row_dot(a, i, x);
norm_sq += r * r;
}
norm_sq.sqrt()
}
pub fn kaczmarz(
a: &Array2<f64>,
b: &[f64],
x0: Option<&[f64]>,
config: &KaczmarzConfig,
) -> OptimizeResult<KaczmarzResult> {
let m = a.nrows();
let n = a.ncols();
if m == 0 || n == 0 {
return Err(OptimizeError::InvalidInput(
"Matrix A must be non-empty".to_string(),
));
}
if b.len() != m {
return Err(OptimizeError::InvalidInput(format!(
"b has length {} but A has {} rows",
b.len(),
m
)));
}
if let Some(x0_ref) = x0 {
if x0_ref.len() != n {
return Err(OptimizeError::InvalidInput(format!(
"x0 has length {} but A has {} columns",
x0_ref.len(),
n
)));
}
}
if config.relaxation <= 0.0 || config.relaxation >= 2.0 {
return Err(OptimizeError::InvalidParameter(
"relaxation parameter must be in (0, 2)".to_string(),
));
}
let mut x: Vec<f64> = match x0 {
Some(x0_ref) => x0_ref.to_vec(),
None => vec![0.0; n],
};
let omega = config.relaxation;
let mut rng = StdRng::seed_from_u64(config.seed);
match config.variant {
KaczmarzVariant::Randomized => {
let row_norms_sq: Vec<f64> = (0..m).map(|i| row_norm_sq(a, i)).collect();
let frobenius_sq: f64 = row_norms_sq.iter().sum();
if frobenius_sq < f64::EPSILON {
return Err(OptimizeError::ComputationError(
"Matrix A has zero Frobenius norm".to_string(),
));
}
let probs: Vec<f64> = row_norms_sq.iter().map(|rn| rn / frobenius_sq).collect();
for iter in 0..config.max_iter {
if iter % n.max(1) == 0 {
let rn = residual_norm(a, b, &x);
if rn < config.tol {
return Ok(KaczmarzResult {
x,
residual_norm: rn,
n_iter: iter,
converged: true,
});
}
}
let row = sample_row(&probs, &mut rng);
kaczmarz_update(a, b, &mut x, row, omega);
}
}
KaczmarzVariant::Cyclic => {
for iter in 0..config.max_iter {
let row = iter % m;
kaczmarz_update(a, b, &mut x, row, omega);
if (iter + 1) % m == 0 {
let rn = residual_norm(a, b, &x);
if rn < config.tol {
return Ok(KaczmarzResult {
x,
residual_norm: rn,
n_iter: iter + 1,
converged: true,
});
}
}
}
}
KaczmarzVariant::GreedyMaxResidual => {
for iter in 0..config.max_iter {
let max_row = (0..m)
.max_by(|&i, &j| {
let ri = (b[i] - row_dot(a, i, &x)).abs();
let rj = (b[j] - row_dot(a, j, &x)).abs();
ri.partial_cmp(&rj).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
kaczmarz_update(a, b, &mut x, max_row, omega);
if (iter + 1) % m == 0 {
let rn = residual_norm(a, b, &x);
if rn < config.tol {
return Ok(KaczmarzResult {
x,
residual_norm: rn,
n_iter: iter + 1,
converged: true,
});
}
}
}
}
KaczmarzVariant::Block => {
let bs = config.block_size.max(1).min(m);
let num_blocks = (m + bs - 1) / bs;
for iter in 0..config.max_iter {
let block_idx = iter % num_blocks;
let start = block_idx * bs;
let end = (start + bs).min(m);
let block: Vec<usize> = (start..end).collect();
block_kaczmarz_update(a, b, &mut x, &block, omega);
if (iter + 1) % num_blocks == 0 {
let rn = residual_norm(a, b, &x);
if rn < config.tol {
return Ok(KaczmarzResult {
x,
residual_norm: rn,
n_iter: iter + 1,
converged: true,
});
}
}
}
}
_ => {
for iter in 0..config.max_iter {
let row = iter % m;
kaczmarz_update(a, b, &mut x, row, omega);
}
}
}
let rn = residual_norm(a, b, &x);
Ok(KaczmarzResult {
converged: rn < config.tol,
residual_norm: rn,
n_iter: config.max_iter,
x,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn make_test_system() -> (Array2<f64>, Vec<f64>) {
let a = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let b = vec![1.0, 2.0, 3.0];
(a, b)
}
#[test]
fn test_kaczmarz_consistent_system() {
let (a, b) = make_test_system();
let config = KaczmarzConfig {
variant: KaczmarzVariant::Cyclic,
max_iter: 50_000,
tol: 1e-6,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let result = kaczmarz(&a, &b, None, &config).expect("Kaczmarz should succeed");
assert!(
result.converged,
"should converge, residual = {}",
result.residual_norm
);
assert!((result.x[0] - 1.0).abs() < 1e-4, "x[0] ≈ 1");
assert!((result.x[1] - 2.0).abs() < 1e-4, "x[1] ≈ 2");
}
#[test]
fn test_kaczmarz_randomized_overdetermined() {
let (a, b) = make_test_system();
let config = KaczmarzConfig {
variant: KaczmarzVariant::Randomized,
max_iter: 100_000,
tol: 1e-5,
relaxation: 1.0,
seed: 123,
..KaczmarzConfig::default()
};
let result = kaczmarz(&a, &b, None, &config).expect("randomized Kaczmarz should succeed");
assert!(result.converged || result.residual_norm < 1e-3);
assert!((result.x[0] - 1.0).abs() < 0.01, "x[0] ≈ 1");
assert!((result.x[1] - 2.0).abs() < 0.01, "x[1] ≈ 2");
}
#[test]
fn test_kaczmarz_block_vs_single() {
let (a, b) = make_test_system();
let config_single = KaczmarzConfig {
variant: KaczmarzVariant::Cyclic,
max_iter: 100_000,
tol: 1e-6,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let config_block = KaczmarzConfig {
variant: KaczmarzVariant::Block,
max_iter: 100_000,
tol: 1e-6,
block_size: 2,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let r_single = kaczmarz(&a, &b, None, &config_single).expect("single row should work");
let r_block = kaczmarz(&a, &b, None, &config_block).expect("block should work");
assert!(r_single.converged || r_single.residual_norm < 1e-4);
assert!(r_block.converged || r_block.residual_norm < 1e-4);
let diff: f64 = r_single
.x
.iter()
.zip(r_block.x.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
assert!(
diff < 0.1,
"block and single solutions should agree, diff = {}",
diff
);
}
#[test]
fn test_kaczmarz_relaxation() {
let (a, b) = make_test_system();
let config = KaczmarzConfig {
variant: KaczmarzVariant::Cyclic,
max_iter: 100_000,
tol: 1e-5,
relaxation: 1.5,
..KaczmarzConfig::default()
};
let result = kaczmarz(&a, &b, None, &config).expect("relaxed Kaczmarz should succeed");
assert!(result.converged || result.residual_norm < 1e-3);
}
#[test]
fn test_kaczmarz_max_residual() {
let (a, b) = make_test_system();
let config = KaczmarzConfig {
variant: KaczmarzVariant::GreedyMaxResidual,
max_iter: 50_000,
tol: 1e-5,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let result = kaczmarz(&a, &b, None, &config).expect("greedy max residual should succeed");
assert!(result.converged || result.residual_norm < 1e-3);
assert!((result.x[0] - 1.0).abs() < 0.01, "x[0] ≈ 1");
assert!((result.x[1] - 2.0).abs() < 0.01, "x[1] ≈ 2");
}
#[test]
fn test_kaczmarz_with_initial_guess() {
let (a, b) = make_test_system();
let config = KaczmarzConfig {
variant: KaczmarzVariant::Cyclic,
max_iter: 50_000,
tol: 1e-6,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let x0 = vec![1.0, 2.0];
let result = kaczmarz(&a, &b, Some(&x0), &config).expect("Kaczmarz with x0 should succeed");
assert!(
result.residual_norm < 1e-10,
"starting at solution => near-zero residual"
);
}
#[test]
fn test_kaczmarz_invalid_relaxation() {
let (a, b) = make_test_system();
let config_low = KaczmarzConfig {
relaxation: 0.0,
..KaczmarzConfig::default()
};
assert!(kaczmarz(&a, &b, None, &config_low).is_err());
let config_high = KaczmarzConfig {
relaxation: 2.0,
..KaczmarzConfig::default()
};
assert!(kaczmarz(&a, &b, None, &config_high).is_err());
}
#[test]
fn test_kaczmarz_dimension_mismatch() {
let a = array![[1.0, 0.0], [0.0, 1.0]];
let b = vec![1.0]; let result = kaczmarz(&a, &b, None, &KaczmarzConfig::default());
assert!(result.is_err());
}
#[test]
fn test_kaczmarz_square_system() {
let a = array![[2.0, 1.0], [1.0, 3.0]];
let b = vec![5.0, 10.0]; let config = KaczmarzConfig {
variant: KaczmarzVariant::Cyclic,
max_iter: 200_000,
tol: 1e-5,
relaxation: 1.0,
..KaczmarzConfig::default()
};
let result = kaczmarz(&a, &b, None, &config).expect("square system should succeed");
assert!(result.converged || result.residual_norm < 1e-3);
assert!(
(result.x[0] - 1.0).abs() < 0.01,
"x[0] ≈ 1, got {}",
result.x[0]
);
assert!(
(result.x[1] - 3.0).abs() < 0.01,
"x[1] ≈ 3, got {}",
result.x[1]
);
}
}