Skip to main content

scirs2_linalg/factorization/
iterative_refinement.rs

1//! Iterative Refinement for Linear Systems
2//!
3//! Mixed-precision iterative refinement algorithms that improve the accuracy
4//! of a computed solution to Ax = b by repeatedly computing residuals in
5//! higher (working) precision and solving correction equations.
6//!
7//! # Algorithms
8//!
9//! - **LU-based refinement**: Factor A = LU once, then iteratively refine
10//!   using forward/back substitution for each correction step.
11//! - **QR-based refinement**: Factor A = QR once, then iteratively refine
12//!   using the Q and R factors.
13//! - **Generic refinement**: Uses an arbitrary solver for each correction step.
14//!
15//! # References
16//!
17//! - Higham (2002). "Accuracy and Stability of Numerical Algorithms."
18//!   2nd ed., Ch. 12.
19//! - Demmel et al. (2006). "Error bounds from extra-precise iterative
20//!   refinement." LAPACK Working Note 165.
21//! - Wilkinson (1963). "Rounding Errors in Algebraic Processes."
22
23use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, ScalarOperand};
24use scirs2_core::numeric::{Float, NumAssign};
25use std::fmt::Debug;
26use std::iter::Sum;
27
28use crate::error::{LinalgError, LinalgResult};
29
30// ============================================================================
31// Configuration & result types
32// ============================================================================
33
34/// Configuration for iterative refinement.
35#[derive(Debug, Clone)]
36pub struct RefinementConfig<F> {
37    /// Maximum number of refinement iterations.
38    pub max_iterations: usize,
39    /// Convergence tolerance for the relative residual.
40    pub tolerance: F,
41    /// If true, stop when the residual starts increasing (stagnation).
42    pub stop_on_stagnation: bool,
43}
44
45impl<F: Float> Default for RefinementConfig<F> {
46    fn default() -> Self {
47        Self {
48            max_iterations: 10,
49            tolerance: F::from(1e-14).unwrap_or_else(|| F::epsilon()),
50            stop_on_stagnation: true,
51        }
52    }
53}
54
55/// Result of iterative refinement.
56#[derive(Debug, Clone)]
57pub struct IterativeRefinementResult<F> {
58    /// Refined solution vector.
59    pub solution: Array1<F>,
60    /// Number of refinement iterations performed.
61    pub iterations: usize,
62    /// History of residual norms (one per iteration, plus the initial).
63    pub residual_history: Vec<F>,
64    /// Estimated forward error ||x_true - x|| / ||x|| at the final step.
65    pub forward_error: F,
66    /// Estimated backward error at the final step.
67    pub backward_error: F,
68    /// Whether the refinement converged within the tolerance.
69    pub converged: bool,
70}
71
72// ============================================================================
73// LU-based iterative refinement
74// ============================================================================
75
76/// Iterative refinement using a pre-computed LU factorization.
77///
78/// Given the system Ax = b:
79/// 1. Factor A = P * L * U once.
80/// 2. Solve Ly = P^T b (forward), Ux = y (back) for initial x.
81/// 3. Iterate: r = b - Ax, solve LU dx = Pr, x += dx.
82///
83/// Because the factorization is reused, each refinement step costs only
84/// O(n^2) instead of O(n^3).
85///
86/// # Arguments
87///
88/// * `a`      - Coefficient matrix (n x n)
89/// * `b`      - Right-hand side vector (n)
90/// * `config` - Refinement configuration (use `Default::default()` for sane defaults)
91pub fn lu_iterative_refinement<F>(
92    a: &ArrayView2<F>,
93    b: &ArrayView1<F>,
94    config: &RefinementConfig<F>,
95) -> LinalgResult<IterativeRefinementResult<F>>
96where
97    F: Float + NumAssign + Sum + Debug + ScalarOperand + Send + Sync + 'static,
98{
99    let (m, n) = a.dim();
100    if m != n {
101        return Err(LinalgError::DimensionError(
102            "LU refinement: matrix must be square".to_string(),
103        ));
104    }
105    if b.len() != n {
106        return Err(LinalgError::DimensionError(format!(
107            "LU refinement: b length ({}) != matrix dimension ({n})",
108            b.len()
109        )));
110    }
111
112    // Factor A = P * L * U via partial pivoting
113    let (piv, l, u) = lu_factor_partial(a)?;
114
115    // Initial solve: forward then back substitution
116    let pb = apply_perm_vec(b, &piv);
117    let y = forward_solve(&l, &pb.view())?;
118    let mut x = back_solve(&u, &y.view())?;
119
120    // Compute norms for relative error tracking
121    let b_norm = inf_norm_vec(b);
122    let a_norm = inf_norm_mat(a);
123
124    let mut residual_history = Vec::with_capacity(config.max_iterations + 1);
125    let mut converged = false;
126    let mut iterations = 0;
127    let mut prev_res_norm = F::infinity();
128
129    // Initial residual
130    let r0 = compute_residual(a, &x.view(), b);
131    let r0_norm = inf_norm_arr(&r0);
132    residual_history.push(r0_norm);
133
134    if b_norm > F::epsilon() && r0_norm / b_norm < config.tolerance {
135        let x_norm_init = inf_norm_arr(&x);
136        let bw_err = r0_norm / (a_norm * x_norm_init + b_norm);
137        return Ok(IterativeRefinementResult {
138            solution: x,
139            iterations: 0,
140            residual_history,
141            forward_error: F::zero(),
142            backward_error: bw_err,
143            converged: true,
144        });
145    }
146
147    for _it in 0..config.max_iterations {
148        iterations += 1;
149
150        // Compute residual: r = b - A*x
151        let r = compute_residual(a, &x.view(), b);
152        let r_norm = inf_norm_arr(&r);
153        residual_history.push(r_norm);
154
155        // Check convergence
156        let x_norm = inf_norm_arr(&x);
157        let backward_err = if a_norm * x_norm + b_norm > F::epsilon() {
158            r_norm / (a_norm * x_norm + b_norm)
159        } else {
160            r_norm
161        };
162
163        if backward_err < config.tolerance {
164            converged = true;
165            break;
166        }
167
168        // Check stagnation
169        if config.stop_on_stagnation && r_norm >= prev_res_norm {
170            break;
171        }
172        prev_res_norm = r_norm;
173
174        // Solve for correction: L U dx = P r
175        let pr = apply_perm_vec(&r.view(), &piv);
176        let y_corr = forward_solve(&l, &pr.view())?;
177        let dx = back_solve(&u, &y_corr.view())?;
178
179        // Update: x += dx
180        for i in 0..n {
181            x[i] += dx[i];
182        }
183    }
184
185    // Final error estimates
186    let final_r = compute_residual(a, &x.view(), b);
187    let final_r_norm = inf_norm_arr(&final_r);
188    let x_norm = inf_norm_arr(&x);
189    let forward_error = if x_norm > F::epsilon() {
190        // Approximate using last correction magnitude / solution magnitude
191        if residual_history.len() >= 2 {
192            let last_res = residual_history[residual_history.len() - 1];
193            last_res / (a_norm * x_norm)
194        } else {
195            final_r_norm / (a_norm * x_norm)
196        }
197    } else {
198        final_r_norm
199    };
200    let backward_error = if a_norm * x_norm + b_norm > F::epsilon() {
201        final_r_norm / (a_norm * x_norm + b_norm)
202    } else {
203        final_r_norm
204    };
205
206    Ok(IterativeRefinementResult {
207        solution: x,
208        iterations,
209        residual_history,
210        forward_error,
211        backward_error,
212        converged,
213    })
214}
215
216// ============================================================================
217// QR-based iterative refinement
218// ============================================================================
219
220/// Iterative refinement using a pre-computed QR factorization.
221///
222/// This is useful for rectangular or over-determined systems (least-squares).
223/// For square systems, LU-based refinement is typically more efficient.
224///
225/// # Algorithm
226///
227/// 1. Factor A = Q * R.
228/// 2. Initial solve: x = R^{-1} Q^T b.
229/// 3. Iterate: r = b - Ax, dx = R^{-1} Q^T r, x += dx.
230///
231/// # Arguments
232///
233/// * `a`      - Coefficient matrix (m x n, m >= n for least squares)
234/// * `b`      - Right-hand side vector (m)
235/// * `config` - Refinement configuration
236pub fn qr_iterative_refinement<F>(
237    a: &ArrayView2<F>,
238    b: &ArrayView1<F>,
239    config: &RefinementConfig<F>,
240) -> LinalgResult<IterativeRefinementResult<F>>
241where
242    F: Float + NumAssign + Sum + Debug + ScalarOperand + Send + Sync + 'static,
243{
244    let (m, n) = a.dim();
245    if b.len() != m {
246        return Err(LinalgError::DimensionError(format!(
247            "QR refinement: b length ({}) != matrix rows ({m})",
248            b.len()
249        )));
250    }
251    if m < n {
252        return Err(LinalgError::DimensionError(
253            "QR refinement: requires m >= n (overdetermined or square)".to_string(),
254        ));
255    }
256
257    // Factor A = Q * R using Householder
258    let (q, r_mat) = householder_qr_internal(a)?;
259
260    // Initial solve: x = R^{-1} (Q^T b) using only the first n rows of R and n columns of Q
261    let qtb = q.t().dot(b); // m-vector; we need the first n entries
262    let qtb_n = qtb.slice(scirs2_core::ndarray::s![..n]).to_owned();
263    let mut x = back_solve_rect(&r_mat, &qtb_n.view(), n)?;
264
265    let b_norm = inf_norm_vec(b);
266    let a_norm = inf_norm_mat(a);
267
268    let mut residual_history = Vec::with_capacity(config.max_iterations + 1);
269    let mut converged = false;
270    let mut iterations = 0;
271    let mut prev_res_norm = F::infinity();
272
273    // Initial residual
274    let r0 = compute_residual(a, &x.view(), b);
275    let r0_norm = inf_norm_arr(&r0);
276    residual_history.push(r0_norm);
277
278    if b_norm > F::epsilon() && r0_norm / b_norm < config.tolerance {
279        let x_norm_init = inf_norm_arr(&x);
280        let bw_err = r0_norm / (a_norm * x_norm_init + b_norm);
281        return Ok(IterativeRefinementResult {
282            solution: x,
283            iterations: 0,
284            residual_history,
285            forward_error: F::zero(),
286            backward_error: bw_err,
287            converged: true,
288        });
289    }
290
291    for _it in 0..config.max_iterations {
292        iterations += 1;
293
294        let r = compute_residual(a, &x.view(), b);
295        let r_norm = inf_norm_arr(&r);
296        residual_history.push(r_norm);
297
298        let x_norm = inf_norm_arr(&x);
299        let backward_err = if a_norm * x_norm + b_norm > F::epsilon() {
300            r_norm / (a_norm * x_norm + b_norm)
301        } else {
302            r_norm
303        };
304
305        if backward_err < config.tolerance {
306            converged = true;
307            break;
308        }
309
310        if config.stop_on_stagnation && r_norm >= prev_res_norm {
311            break;
312        }
313        prev_res_norm = r_norm;
314
315        // Solve correction: dx = R^{-1} Q^T r
316        let qt_r = q.t().dot(&r);
317        let qt_r_n = qt_r.slice(scirs2_core::ndarray::s![..n]).to_owned();
318        let dx = back_solve_rect(&r_mat, &qt_r_n.view(), n)?;
319
320        for i in 0..n {
321            x[i] += dx[i];
322        }
323    }
324
325    let final_r = compute_residual(a, &x.view(), b);
326    let final_r_norm = inf_norm_arr(&final_r);
327    let x_norm = inf_norm_arr(&x);
328    let forward_error = if x_norm > F::epsilon() {
329        final_r_norm / (a_norm * x_norm)
330    } else {
331        final_r_norm
332    };
333    let backward_error = if a_norm * x_norm + b_norm > F::epsilon() {
334        final_r_norm / (a_norm * x_norm + b_norm)
335    } else {
336        final_r_norm
337    };
338
339    Ok(IterativeRefinementResult {
340        solution: x,
341        iterations,
342        residual_history,
343        forward_error,
344        backward_error,
345        converged,
346    })
347}
348
349// ============================================================================
350// Generic iterative refinement (solver-agnostic)
351// ============================================================================
352
353/// Generic iterative refinement that uses a supplied solver function.
354///
355/// This is the most flexible variant: you provide a closure that solves
356/// A*dx = r for dx, and this function handles the residual computation
357/// and update loop.
358///
359/// # Arguments
360///
361/// * `a`      - Coefficient matrix (m x n)
362/// * `b`      - Right-hand side vector (m)
363/// * `solver` - Closure `|r: &Array1<F>| -> LinalgResult<Array1<F>>` that solves A*dx = r
364/// * `config` - Refinement configuration
365pub fn generic_iterative_refinement<F, S>(
366    a: &ArrayView2<F>,
367    b: &ArrayView1<F>,
368    mut solver: S,
369    config: &RefinementConfig<F>,
370) -> LinalgResult<IterativeRefinementResult<F>>
371where
372    F: Float + NumAssign + Sum + Debug + ScalarOperand + Send + Sync + 'static,
373    S: FnMut(&Array1<F>) -> LinalgResult<Array1<F>>,
374{
375    let (m, _n) = a.dim();
376    if b.len() != m {
377        return Err(LinalgError::DimensionError(format!(
378            "Generic refinement: b length ({}) != matrix rows ({m})",
379            b.len()
380        )));
381    }
382
383    // Initial solve
384    let b_owned = b.to_owned();
385    let mut x = solver(&b_owned)?;
386
387    let b_norm = inf_norm_vec(b);
388    let a_norm = inf_norm_mat(a);
389
390    let mut residual_history = Vec::with_capacity(config.max_iterations + 1);
391    let mut converged = false;
392    let mut iterations = 0;
393    let mut prev_res_norm = F::infinity();
394
395    // Initial residual
396    let r0 = compute_residual(a, &x.view(), b);
397    residual_history.push(inf_norm_arr(&r0));
398
399    for _it in 0..config.max_iterations {
400        iterations += 1;
401
402        let r = compute_residual(a, &x.view(), b);
403        let r_norm = inf_norm_arr(&r);
404        residual_history.push(r_norm);
405
406        let x_norm = inf_norm_arr(&x);
407        let backward_err = if a_norm * x_norm + b_norm > F::epsilon() {
408            r_norm / (a_norm * x_norm + b_norm)
409        } else {
410            r_norm
411        };
412
413        if backward_err < config.tolerance {
414            converged = true;
415            break;
416        }
417
418        if config.stop_on_stagnation && r_norm >= prev_res_norm {
419            break;
420        }
421        prev_res_norm = r_norm;
422
423        let dx = solver(&r)?;
424        let n = x.len();
425        for i in 0..n {
426            x[i] += dx[i];
427        }
428    }
429
430    let final_r = compute_residual(a, &x.view(), b);
431    let final_r_norm = inf_norm_arr(&final_r);
432    let x_norm = inf_norm_arr(&x);
433    let forward_error = if x_norm > F::epsilon() {
434        final_r_norm / (a_norm * x_norm)
435    } else {
436        final_r_norm
437    };
438    let backward_error = if a_norm * x_norm + b_norm > F::epsilon() {
439        final_r_norm / (a_norm * x_norm + b_norm)
440    } else {
441        final_r_norm
442    };
443
444    Ok(IterativeRefinementResult {
445        solution: x,
446        iterations,
447        residual_history,
448        forward_error,
449        backward_error,
450        converged,
451    })
452}
453
454// ============================================================================
455// Internal helpers
456// ============================================================================
457
458/// Compute r = b - A*x
459fn compute_residual<F>(a: &ArrayView2<F>, x: &ArrayView1<F>, b: &ArrayView1<F>) -> Array1<F>
460where
461    F: Float + NumAssign + Sum + ScalarOperand,
462{
463    let ax = a.dot(x);
464    let n = b.len();
465    let mut r = Array1::<F>::zeros(n);
466    for i in 0..n {
467        r[i] = b[i] - ax[i];
468    }
469    r
470}
471
472/// Infinity norm of an ArrayView1
473fn inf_norm_vec<F: Float>(v: &ArrayView1<F>) -> F {
474    v.iter().fold(F::zero(), |acc, &x| acc.max(x.abs()))
475}
476
477/// Infinity norm of an owned Array1
478fn inf_norm_arr<F: Float>(v: &Array1<F>) -> F {
479    v.iter().fold(F::zero(), |acc, &x| acc.max(x.abs()))
480}
481
482/// Infinity norm of a matrix (max row sum of absolute values)
483fn inf_norm_mat<F: Float + Sum>(a: &ArrayView2<F>) -> F {
484    let (m, n) = a.dim();
485    let mut max_row = F::zero();
486    for i in 0..m {
487        let mut row_sum = F::zero();
488        for j in 0..n {
489            row_sum = row_sum + a[[i, j]].abs();
490        }
491        if row_sum > max_row {
492            max_row = row_sum;
493        }
494    }
495    max_row
496}
497
498/// LU factorization with partial pivoting (Doolittle form).
499///
500/// Returns (piv, L, U) where piv[i] is the pivot row for step i.
501fn lu_factor_partial<F>(a: &ArrayView2<F>) -> LinalgResult<(Vec<usize>, Array2<F>, Array2<F>)>
502where
503    F: Float + NumAssign + Debug,
504{
505    let n = a.nrows();
506    let mut lu = a.to_owned();
507    let mut piv: Vec<usize> = (0..n).collect();
508
509    for k in 0..n {
510        // Find pivot
511        let mut max_val = lu[[k, k]].abs();
512        let mut max_row = k;
513        for i in (k + 1)..n {
514            let v = lu[[i, k]].abs();
515            if v > max_val {
516                max_val = v;
517                max_row = i;
518            }
519        }
520
521        if max_val <= F::epsilon() {
522            return Err(LinalgError::SingularMatrixError(
523                "LU factorization: matrix is singular or nearly singular".to_string(),
524            ));
525        }
526
527        // Swap rows
528        if max_row != k {
529            piv.swap(k, max_row);
530            for j in 0..n {
531                let tmp = lu[[k, j]];
532                lu[[k, j]] = lu[[max_row, j]];
533                lu[[max_row, j]] = tmp;
534            }
535        }
536
537        // Eliminate below
538        for i in (k + 1)..n {
539            lu[[i, k]] = lu[[i, k]] / lu[[k, k]];
540            for j in (k + 1)..n {
541                let lik = lu[[i, k]];
542                let ukj = lu[[k, j]];
543                lu[[i, j]] -= lik * ukj;
544            }
545        }
546    }
547
548    // Extract L and U
549    let mut l = Array2::<F>::eye(n);
550    let mut u = Array2::<F>::zeros((n, n));
551    for i in 0..n {
552        for j in 0..n {
553            if j < i {
554                l[[i, j]] = lu[[i, j]];
555            } else {
556                u[[i, j]] = lu[[i, j]];
557            }
558        }
559    }
560
561    Ok((piv, l, u))
562}
563
564/// Apply a row permutation to a vector.
565fn apply_perm_vec<F: Float>(b: &ArrayView1<F>, piv: &[usize]) -> Array1<F> {
566    let n = b.len();
567    let mut result = Array1::<F>::zeros(n);
568    for i in 0..n {
569        result[i] = b[piv[i]];
570    }
571    result
572}
573
574/// Forward substitution: solve Ly = b where L is unit lower triangular.
575fn forward_solve<F: Float + NumAssign>(
576    l: &Array2<F>,
577    b: &ArrayView1<F>,
578) -> LinalgResult<Array1<F>> {
579    let n = l.nrows();
580    let mut y = Array1::<F>::zeros(n);
581    for i in 0..n {
582        let mut s = b[i];
583        for j in 0..i {
584            s -= l[[i, j]] * y[j];
585        }
586        y[i] = s; // L has unit diagonal
587    }
588    Ok(y)
589}
590
591/// Back substitution: solve Ux = y where U is upper triangular.
592fn back_solve<F: Float + NumAssign + Debug>(
593    u: &Array2<F>,
594    y: &ArrayView1<F>,
595) -> LinalgResult<Array1<F>> {
596    let n = u.nrows();
597    let mut x = Array1::<F>::zeros(n);
598    for i in (0..n).rev() {
599        let mut s = y[i];
600        for j in (i + 1)..n {
601            s -= u[[i, j]] * x[j];
602        }
603        let diag = u[[i, i]];
604        if diag.abs() <= F::epsilon() {
605            return Err(LinalgError::SingularMatrixError(format!(
606                "Back substitution: zero diagonal at index {i}"
607            )));
608        }
609        x[i] = s / diag;
610    }
611    Ok(x)
612}
613
614/// Back substitution on the top-left k x k block of an m x n upper triangular R.
615fn back_solve_rect<F: Float + NumAssign + Debug>(
616    r: &Array2<F>,
617    y: &ArrayView1<F>,
618    k: usize,
619) -> LinalgResult<Array1<F>> {
620    let mut x = Array1::<F>::zeros(k);
621    for i in (0..k).rev() {
622        let mut s = y[i];
623        for j in (i + 1)..k {
624            s -= r[[i, j]] * x[j];
625        }
626        let diag = r[[i, i]];
627        if diag.abs() <= F::epsilon() {
628            return Err(LinalgError::SingularMatrixError(format!(
629                "Back substitution: zero diagonal at index {i}"
630            )));
631        }
632        x[i] = s / diag;
633    }
634    Ok(x)
635}
636
637/// Householder QR factorization (non-pivoted).
638fn householder_qr_internal<F>(a: &ArrayView2<F>) -> LinalgResult<(Array2<F>, Array2<F>)>
639where
640    F: Float + NumAssign + Sum + Debug + ScalarOperand + 'static,
641{
642    let (m, n) = a.dim();
643    let min_dim = m.min(n);
644    let mut r = a.to_owned();
645    let mut q = Array2::<F>::eye(m);
646    let two = F::from(2.0).unwrap_or_else(|| F::one() + F::one());
647
648    for k in 0..min_dim {
649        let mut x = Array1::<F>::zeros(m - k);
650        for i in k..m {
651            x[i - k] = r[[i, k]];
652        }
653        let x_norm = x.iter().fold(F::zero(), |acc, &v| acc + v * v).sqrt();
654        if x_norm <= F::epsilon() {
655            continue;
656        }
657        let alpha = if x[0] >= F::zero() { -x_norm } else { x_norm };
658        let mut v = x;
659        v[0] -= alpha;
660        let v_norm_sq = v.iter().fold(F::zero(), |acc, &val| acc + val * val);
661        if v_norm_sq <= F::epsilon() {
662            continue;
663        }
664        let beta = two / v_norm_sq;
665
666        for j in k..n {
667            let mut dot = F::zero();
668            for i in 0..(m - k) {
669                dot += v[i] * r[[i + k, j]];
670            }
671            for i in 0..(m - k) {
672                r[[i + k, j]] -= beta * v[i] * dot;
673            }
674        }
675        for row in 0..m {
676            let mut dot = F::zero();
677            for jj in 0..(m - k) {
678                dot += q[[row, jj + k]] * v[jj];
679            }
680            for jj in 0..(m - k) {
681                q[[row, jj + k]] -= beta * dot * v[jj];
682            }
683        }
684    }
685
686    Ok((q, r))
687}
688
689// ============================================================================
690// Tests
691// ============================================================================
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use scirs2_core::ndarray::array;
697
698    #[test]
699    fn test_lu_refinement_well_conditioned() {
700        let a = array![[4.0, 1.0], [1.0, 3.0]];
701        let x_true = array![1.0, 2.0];
702        let b = a.dot(&x_true);
703
704        let config = RefinementConfig {
705            max_iterations: 10,
706            tolerance: 1e-14,
707            stop_on_stagnation: true,
708        };
709        let result =
710            lu_iterative_refinement(&a.view(), &b.view(), &config).expect("LU refinement failed");
711
712        assert!(
713            result.converged,
714            "should converge for well-conditioned system"
715        );
716        for i in 0..2 {
717            assert!(
718                (result.solution[i] - x_true[i]).abs() < 1e-12,
719                "solution[{i}] = {} != {}",
720                result.solution[i],
721                x_true[i]
722            );
723        }
724    }
725
726    #[test]
727    fn test_lu_refinement_ill_conditioned() {
728        // Hilbert 3x3 matrix (cond ~ 524)
729        let a = array![
730            [1.0, 0.5, 1.0 / 3.0],
731            [0.5, 1.0 / 3.0, 0.25],
732            [1.0 / 3.0, 0.25, 0.2]
733        ];
734        let x_true = array![1.0, 1.0, 1.0];
735        let b = a.dot(&x_true);
736
737        let config = RefinementConfig {
738            max_iterations: 20,
739            tolerance: 1e-12,
740            stop_on_stagnation: true,
741        };
742        let result =
743            lu_iterative_refinement(&a.view(), &b.view(), &config).expect("LU refinement failed");
744
745        // Even for ill-conditioned systems, refinement should improve accuracy
746        let residual = &a.dot(&result.solution) - &b;
747        let res_norm: f64 = residual.iter().map(|&v| v.abs()).fold(0.0, f64::max);
748        assert!(
749            res_norm < 1e-10,
750            "residual should be small after refinement, got {res_norm}"
751        );
752    }
753
754    #[test]
755    fn test_lu_refinement_convergence_tracking() {
756        let a = array![[2.0, 1.0], [1.0, 3.0]];
757        let b = array![3.0, 4.0];
758
759        let config = RefinementConfig {
760            max_iterations: 5,
761            tolerance: 1e-14,
762            stop_on_stagnation: false,
763        };
764        let result =
765            lu_iterative_refinement(&a.view(), &b.view(), &config).expect("LU refinement failed");
766
767        // Residual history should be non-empty
768        assert!(
769            !result.residual_history.is_empty(),
770            "should have residual history"
771        );
772        // Forward and backward errors should be finite
773        assert!(result.forward_error.is_finite());
774        assert!(result.backward_error.is_finite());
775    }
776
777    #[test]
778    fn test_lu_refinement_singular_error() {
779        let a = array![[1.0, 2.0], [2.0, 4.0]]; // singular
780        let b = array![1.0, 2.0];
781
782        let config = RefinementConfig::default();
783        let result = lu_iterative_refinement(&a.view(), &b.view(), &config);
784        assert!(result.is_err(), "should fail on singular matrix");
785    }
786
787    #[test]
788    fn test_lu_refinement_dimension_mismatch() {
789        let a = array![[1.0, 2.0], [3.0, 4.0]];
790        let b = array![1.0, 2.0, 3.0]; // wrong size
791
792        let config = RefinementConfig::default();
793        assert!(lu_iterative_refinement(&a.view(), &b.view(), &config).is_err());
794    }
795
796    #[test]
797    fn test_qr_refinement_square() {
798        let a = array![[4.0, 1.0], [1.0, 3.0]];
799        let x_true = array![1.0, 2.0];
800        let b = a.dot(&x_true);
801
802        let config = RefinementConfig {
803            max_iterations: 10,
804            tolerance: 1e-14,
805            stop_on_stagnation: true,
806        };
807        let result =
808            qr_iterative_refinement(&a.view(), &b.view(), &config).expect("QR refinement failed");
809
810        for i in 0..2 {
811            assert!(
812                (result.solution[i] - x_true[i]).abs() < 1e-10,
813                "QR solution[{i}] = {} != {}",
814                result.solution[i],
815                x_true[i]
816            );
817        }
818    }
819
820    #[test]
821    fn test_qr_refinement_overdetermined() {
822        // 3 x 2 overdetermined system
823        let a = array![[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]];
824        let b = array![1.0, 2.0, 3.0]; // b is in the column space, exact LS solution is [0, 1]
825
826        let config = RefinementConfig {
827            max_iterations: 10,
828            tolerance: 1e-12,
829            stop_on_stagnation: true,
830        };
831        let result =
832            qr_iterative_refinement(&a.view(), &b.view(), &config).expect("QR refinement failed");
833
834        // Check that residual is small
835        let res = &a.dot(&result.solution) - &b;
836        let res_norm: f64 = res.iter().map(|&v| v * v).sum::<f64>().sqrt();
837        assert!(res_norm < 1e-8, "overdetermined LS residual = {res_norm}");
838    }
839
840    #[test]
841    fn test_qr_refinement_dimension_error() {
842        let a = array![[1.0, 2.0], [3.0, 4.0]];
843        let b = array![1.0, 2.0, 3.0]; // wrong
844        let config = RefinementConfig::default();
845        assert!(qr_iterative_refinement(&a.view(), &b.view(), &config).is_err());
846    }
847
848    #[test]
849    fn test_generic_refinement() {
850        let a = array![[4.0, 1.0], [1.0, 3.0]];
851        let x_true = array![1.0, 2.0];
852        let b = a.dot(&x_true);
853
854        let a_clone = a.clone();
855        let solver = move |rhs: &Array1<f64>| -> LinalgResult<Array1<f64>> {
856            // Simple solve using our LU
857            let (piv, l, u) = lu_factor_partial(&a_clone.view())?;
858            let pb = apply_perm_vec(&rhs.view(), &piv);
859            let y = forward_solve(&l, &pb.view())?;
860            back_solve(&u, &y.view())
861        };
862
863        let config = RefinementConfig {
864            max_iterations: 5,
865            tolerance: 1e-14,
866            stop_on_stagnation: true,
867        };
868        let result = generic_iterative_refinement(&a.view(), &b.view(), solver, &config)
869            .expect("generic refinement failed");
870
871        for i in 0..2 {
872            assert!(
873                (result.solution[i] - x_true[i]).abs() < 1e-10,
874                "generic solution[{i}] wrong"
875            );
876        }
877    }
878
879    #[test]
880    fn test_refinement_config_default() {
881        let config = RefinementConfig::<f64>::default();
882        assert_eq!(config.max_iterations, 10);
883        assert!(config.tolerance < 1e-10);
884        assert!(config.stop_on_stagnation);
885    }
886
887    #[test]
888    fn test_lu_refinement_identity() {
889        let a = Array2::<f64>::eye(3);
890        let b = array![1.0, 2.0, 3.0];
891
892        let config = RefinementConfig::default();
893        let result =
894            lu_iterative_refinement(&a.view(), &b.view(), &config).expect("identity solve failed");
895
896        for i in 0..3 {
897            assert!(
898                (result.solution[i] - b[i]).abs() < 1e-14,
899                "identity system wrong at {i}"
900            );
901        }
902        assert!(result.converged);
903    }
904
905    #[test]
906    fn test_lu_refinement_non_square_error() {
907        let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
908        let b = array![1.0, 2.0];
909        let config = RefinementConfig::default();
910        assert!(lu_iterative_refinement(&a.view(), &b.view(), &config).is_err());
911    }
912}