Skip to main content

scirs2_linalg/randomized/
preconditioning.rs

1//! Randomized preconditioning for overdetermined least-squares systems
2//!
3//! This module implements randomized preconditioning techniques that dramatically
4//! accelerate the solution of overdetermined least-squares problems by constructing
5//! well-conditioned preconditioned systems via random sketching:
6//!
7//! - **SparseSign**: Sparse ±1 random matrix with fast application
8//! - **SubsampledRandomizedHadamard (SRHT)**: Fast structured random projection
9//! - **Blendenpik**: Randomized preconditioning for overdetermined LS (Avron et al. 2010)
10//! - **LSRN**: Least-Squares via Random Normal preconditioning (Meng et al. 2014)
11//! - **IterativeRefinementLS**: Mixed-precision iterative refinement for LS
12//!
13//! ## Mathematical Foundation
14//!
15//! For an overdetermined system A x ≈ b (m × n, m >> n), a right preconditioner R
16//! is constructed so that A R⁻¹ has a small condition number. The preconditioned
17//! system (A R⁻¹)(R x) = b is then solved by an iterative method such as LSQR.
18//!
19//! Blendenpik constructs R via QR of a random sketch S A, where S is a SRHT or
20//! sparse sign matrix with O(n log n) application cost.
21//!
22//! ## References
23//!
24//! - Avron, Maymounkov, Toledo (2010). "Blendenpik: Supercharging LAPACK's least-squares solver"
25//! - Meng, Saunders, Mahoney (2014). "LSRN: A parallel iterative solver for strongly
26//!   over- or under-determined systems"
27//! - Rokhlin, Tygert (2008). "A fast randomized algorithm for overdetermined linear
28//!   least-squares regression"
29
30use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
31use scirs2_core::numeric::{Float, NumAssign, One, Zero};
32use scirs2_core::random::prelude::*;
33use scirs2_core::random::{Distribution, Normal, Uniform};
34use std::fmt::Debug;
35use std::iter::Sum;
36
37use crate::decomposition::qr;
38use crate::error::{LinalgError, LinalgResult};
39
40// ============================================================================
41// SparseSign: Sparse ±1 random matrix
42// ============================================================================
43
44/// Sparse sign random matrix: each column has exactly `s` non-zeros, each ±1.
45///
46/// This provides an efficient O(s·n) matrix-vector product compared to O(m·n)
47/// for dense random matrices. Typically s = O(log(m)) suffices for good
48/// embedding properties.
49#[derive(Debug, Clone)]
50pub struct SparseSign {
51    /// Number of rows (sketch dimension)
52    pub rows: usize,
53    /// Number of columns (original dimension)
54    pub cols: usize,
55    /// Sparsity: number of non-zeros per column
56    pub sparsity: usize,
57    /// Row indices of non-zeros (length cols × sparsity)
58    row_indices: Vec<usize>,
59    /// Signs (±1) for each non-zero (length cols × sparsity)
60    signs: Vec<f64>,
61    /// Scaling factor 1/sqrt(sparsity)
62    scale: f64,
63}
64
65impl SparseSign {
66    /// Construct a sparse sign matrix with given dimensions and sparsity.
67    ///
68    /// # Arguments
69    ///
70    /// * `rows` - Sketch dimension (number of rows)
71    /// * `cols` - Input dimension (number of columns)
72    /// * `sparsity` - Non-zeros per column (default: max(1, ceil(log2(rows))))
73    /// * `seed` - Optional RNG seed for reproducibility
74    pub fn new(
75        rows: usize,
76        cols: usize,
77        sparsity: Option<usize>,
78        seed: Option<u64>,
79    ) -> LinalgResult<Self> {
80        if rows == 0 || cols == 0 {
81            return Err(LinalgError::ShapeError(
82                "SparseSign: rows and cols must be positive".to_string(),
83            ));
84        }
85
86        let s = sparsity.unwrap_or_else(|| {
87            let log2r = (rows as f64).log2().ceil() as usize;
88            log2r.max(1).min(rows)
89        });
90
91        if s > rows {
92            return Err(LinalgError::ShapeError(format!(
93                "SparseSign: sparsity {s} cannot exceed rows {rows}"
94            )));
95        }
96
97        let mut rng = {
98            let s = seed.unwrap_or_else(|| {
99                let mut tr = scirs2_core::random::rng();
100                scirs2_core::random::RngExt::random::<u64>(&mut tr)
101            });
102            scirs2_core::random::seeded_rng(s)
103        };
104
105        let mut row_indices = vec![0usize; cols * s];
106        let mut signs = vec![0.0f64; cols * s];
107
108        let uniform_sign = Uniform::new(0u8, 2).map_err(|e| {
109            LinalgError::ComputationError(format!("Failed to create uniform distribution: {e}"))
110        })?;
111
112        for col in 0..cols {
113            // Sample s distinct row indices without replacement (Fisher-Yates)
114            let mut pool: Vec<usize> = (0..rows).collect();
115            for k in 0..s {
116                let j = (Uniform::new(k, rows)
117                    .map_err(|e| {
118                        LinalgError::ComputationError(format!("Failed to sample index: {e}"))
119                    })?
120                    .sample(&mut rng)) as usize;
121                pool.swap(k, j);
122                let row = pool[k];
123                row_indices[col * s + k] = row;
124                let sign_bit = uniform_sign.sample(&mut rng);
125                signs[col * s + k] = if sign_bit == 0 { 1.0 } else { -1.0 };
126            }
127        }
128
129        let scale = 1.0 / (s as f64).sqrt();
130
131        Ok(Self {
132            rows,
133            cols,
134            sparsity: s,
135            row_indices,
136            signs,
137            scale,
138        })
139    }
140
141    /// Apply the sparse sign matrix: y = S * x, result is `rows`-vector.
142    ///
143    /// # Arguments
144    ///
145    /// * `x` - Input vector of length `self.cols`
146    pub fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
147        if x.len() != self.cols {
148            return Err(LinalgError::DimensionError(format!(
149                "SparseSign::apply: x has length {} but cols={}",
150                x.len(),
151                self.cols
152            )));
153        }
154
155        let mut y = Array1::zeros(self.rows);
156        let s = self.sparsity;
157
158        for col in 0..self.cols {
159            let xval = x[col];
160            for k in 0..s {
161                let row = self.row_indices[col * s + k];
162                let sign = self.signs[col * s + k];
163                y[row] += self.scale * sign * xval;
164            }
165        }
166
167        Ok(y)
168    }
169
170    /// Apply S to each column of matrix A (result: rows × n).
171    ///
172    /// Computes S * A for a `cols × n` matrix A.
173    pub fn apply_matrix(&self, a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
174        let (nrows_a, ncols_a) = a.dim();
175        if nrows_a != self.cols {
176            return Err(LinalgError::DimensionError(format!(
177                "SparseSign::apply_matrix: A has {} rows but cols={}",
178                nrows_a, self.cols
179            )));
180        }
181
182        let mut y = Array2::zeros((self.rows, ncols_a));
183        let s = self.sparsity;
184
185        for col in 0..self.cols {
186            for k in 0..s {
187                let row = self.row_indices[col * s + k];
188                let sign = self.signs[col * s + k];
189                let coeff = self.scale * sign;
190                for j in 0..ncols_a {
191                    y[[row, j]] += coeff * a[[col, j]];
192                }
193            }
194        }
195
196        Ok(y)
197    }
198
199    /// Apply S^T (transpose): y = S^T * x, result is `cols`-vector.
200    pub fn apply_transpose(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
201        if x.len() != self.rows {
202            return Err(LinalgError::DimensionError(format!(
203                "SparseSign::apply_transpose: x has length {} but rows={}",
204                x.len(),
205                self.rows
206            )));
207        }
208
209        let mut y = Array1::zeros(self.cols);
210        let s = self.sparsity;
211
212        for col in 0..self.cols {
213            let mut val = 0.0f64;
214            for k in 0..s {
215                let row = self.row_indices[col * s + k];
216                let sign = self.signs[col * s + k];
217                val += sign * x[row];
218            }
219            y[col] = self.scale * val;
220        }
221
222        Ok(y)
223    }
224}
225
226// ============================================================================
227// SubsampledRandomizedHadamard (SRHT)
228// ============================================================================
229
230/// Subsampled Randomized Hadamard Transform (SRHT) for fast preconditioning.
231///
232/// The SRHT maps an n-vector to a k-vector via:
233///   y = sqrt(n/k) * R * H * D * x
234/// where:
235///   - D is a random diagonal ±1 matrix
236///   - H is the normalized Walsh-Hadamard transform (n must be a power of 2)
237///   - R is a random row-sampling operator selecting k rows uniformly
238///
239/// Cost: O(n log n) per matrix-vector product.
240#[derive(Debug, Clone)]
241pub struct SubsampledRandomizedHadamard {
242    /// Input dimension (must be power of 2)
243    pub n: usize,
244    /// Sketch dimension k ≤ n
245    pub k: usize,
246    /// Random diagonal signs (length n)
247    diag_signs: Vec<f64>,
248    /// Sampled row indices (length k)
249    sampled_rows: Vec<usize>,
250    /// Scaling factor sqrt(n / k)
251    scale: f64,
252}
253
254impl SubsampledRandomizedHadamard {
255    /// Construct an SRHT with sketch dimension k.
256    ///
257    /// # Arguments
258    ///
259    /// * `n` - Input dimension (must be a power of 2, or will be padded)
260    /// * `k` - Sketch dimension (k ≤ n)
261    /// * `seed` - Optional RNG seed
262    pub fn new(n: usize, k: usize, seed: Option<u64>) -> LinalgResult<Self> {
263        if n == 0 {
264            return Err(LinalgError::ShapeError(
265                "SRHT: n must be positive".to_string(),
266            ));
267        }
268        if k == 0 || k > n {
269            return Err(LinalgError::ShapeError(format!(
270                "SRHT: k must satisfy 1 ≤ k ≤ n={n}, got k={k}"
271            )));
272        }
273
274        // Round n up to next power of 2
275        let n_padded = n.next_power_of_two();
276
277        let mut rng = {
278            let s = seed.unwrap_or_else(|| {
279                let mut tr = scirs2_core::random::rng();
280                scirs2_core::random::RngExt::random::<u64>(&mut tr)
281            });
282            scirs2_core::random::seeded_rng(s)
283        };
284
285        // Random diagonal signs ±1
286        let uniform_bit = Uniform::new(0u8, 2).map_err(|e| {
287            LinalgError::ComputationError(format!("Failed to create uniform distribution: {e}"))
288        })?;
289        let diag_signs: Vec<f64> = (0..n_padded)
290            .map(|_| {
291                if uniform_bit.sample(&mut rng) == 0 {
292                    1.0
293                } else {
294                    -1.0
295                }
296            })
297            .collect();
298
299        // Sample k distinct rows uniformly from {0..n_padded}
300        let mut pool: Vec<usize> = (0..n_padded).collect();
301        let k_actual = k.min(n_padded);
302        for i in 0..k_actual {
303            let j_range = Uniform::new(i, n_padded).map_err(|e| {
304                LinalgError::ComputationError(format!("Failed to sample row index: {e}"))
305            })?;
306            let j = j_range.sample(&mut rng);
307            pool.swap(i, j);
308        }
309        let sampled_rows: Vec<usize> = pool[..k_actual].to_vec();
310
311        let scale = (n_padded as f64 / k_actual as f64).sqrt();
312
313        Ok(Self {
314            n: n_padded,
315            k: k_actual,
316            diag_signs,
317            sampled_rows,
318            scale,
319        })
320    }
321
322    /// Apply the SRHT: y = scale * R * H * D * x
323    ///
324    /// Returns a k-vector.
325    pub fn apply(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
326        if x.len() > self.n {
327            return Err(LinalgError::DimensionError(format!(
328                "SRHT::apply: x has length {} but n={}",
329                x.len(),
330                self.n
331            )));
332        }
333
334        // Pad x to length n with zeros
335        let mut buf = vec![0.0f64; self.n];
336        for i in 0..x.len() {
337            buf[i] = self.diag_signs[i] * x[i];
338        }
339
340        // In-place Walsh-Hadamard transform
341        Self::fwht(&mut buf);
342
343        // Sample k rows and scale
344        let scale = self.scale / (self.n as f64).sqrt();
345        let y: Array1<f64> = self.sampled_rows.iter().map(|&r| scale * buf[r]).collect();
346
347        Ok(y)
348    }
349
350    /// Apply SRHT to each column of matrix A (applies SRHT row-wise to A).
351    ///
352    /// For A of shape (n, p), computes S * A of shape (k, p).
353    pub fn apply_matrix(&self, a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
354        let (nrows, ncols) = a.dim();
355        if nrows > self.n {
356            return Err(LinalgError::DimensionError(format!(
357                "SRHT::apply_matrix: A has {} rows but n={}",
358                nrows, self.n
359            )));
360        }
361
362        let scale = self.scale / (self.n as f64).sqrt();
363        let mut result = Array2::zeros((self.k, ncols));
364
365        for j in 0..ncols {
366            // Apply D to column j (pad to n)
367            let mut buf = vec![0.0f64; self.n];
368            for i in 0..nrows {
369                buf[i] = self.diag_signs[i] * a[[i, j]];
370            }
371            // FWHT
372            Self::fwht(&mut buf);
373            // Sample rows
374            for (ki, &row) in self.sampled_rows.iter().enumerate() {
375                result[[ki, j]] = scale * buf[row];
376            }
377        }
378
379        Ok(result)
380    }
381
382    /// Fast Walsh-Hadamard Transform (in-place, unnormalized).
383    ///
384    /// Requires `data.len()` to be a power of 2.
385    fn fwht(data: &mut [f64]) {
386        let n = data.len();
387        let mut h = 1;
388        while h < n {
389            let mut i = 0;
390            while i < n {
391                for j in i..(i + h) {
392                    let u = data[j];
393                    let v = data[j + h];
394                    data[j] = u + v;
395                    data[j + h] = u - v;
396                }
397                i += 2 * h;
398            }
399            h *= 2;
400        }
401    }
402}
403
404// ============================================================================
405// Result type
406// ============================================================================
407
408/// Result of a randomized preconditioned solve
409#[derive(Debug, Clone)]
410pub struct RandomizedPrecondResult {
411    /// Computed solution vector x
412    pub solution: Array1<f64>,
413    /// Relative residual ‖Ax - b‖₂ / ‖b‖₂
414    pub relative_residual: f64,
415    /// Number of iterations used
416    pub iterations: usize,
417    /// Whether the solver converged
418    pub converged: bool,
419    /// Condition number estimate of preconditioned system
420    pub preconditioned_cond_estimate: Option<f64>,
421}
422
423// ============================================================================
424// Blendenpik Preconditioner
425// ============================================================================
426
427/// Blendenpik randomized preconditioner for overdetermined least squares.
428///
429/// Constructs an upper triangular preconditioner R such that A R⁻¹ is
430/// well-conditioned, then solves min ‖A x - b‖ by iterating on
431/// min ‖(A R⁻¹) y - b‖ with y = R x.
432///
433/// Algorithm:
434/// 1. Sketch: B = S A, where S is a SRHT or sparse sign matrix, B is (k × n)
435/// 2. QR: B = Q R (thin QR of k × n matrix)
436/// 3. Solve the preconditioned system (A R⁻¹) y ≈ b using LSQR / CG-normal
437/// 4. Recover x = R⁻¹ y
438#[derive(Debug, Clone)]
439pub struct BlendenpikPreconditioner {
440    /// Upper triangular preconditioner R (n × n)
441    pub r_factor: Array2<f64>,
442    /// Size n (number of columns of A)
443    pub n: usize,
444    /// Sketch dimension used
445    pub sketch_dim: usize,
446    /// Condition number estimate
447    pub cond_estimate: f64,
448}
449
450impl BlendenpikPreconditioner {
451    /// Build the Blendenpik preconditioner from matrix A.
452    ///
453    /// # Arguments
454    ///
455    /// * `a` - Overdetermined matrix (m × n, m ≥ n)
456    /// * `oversampling` - Sketch oversampling factor (default: 2, sketch_dim = oversampling * n)
457    /// * `use_srht` - Use SRHT if true, sparse sign otherwise
458    /// * `seed` - Optional RNG seed
459    pub fn new(
460        a: &ArrayView2<f64>,
461        oversampling: Option<f64>,
462        use_srht: bool,
463        seed: Option<u64>,
464    ) -> LinalgResult<Self> {
465        let (m, n) = a.dim();
466        if m < n {
467            return Err(LinalgError::ShapeError(format!(
468                "Blendenpik requires m ≥ n, got {}×{}",
469                m, n
470            )));
471        }
472
473        let alpha = oversampling.unwrap_or(2.0).max(1.0);
474        let sketch_dim = ((alpha * n as f64) as usize).max(n + 1).min(m);
475
476        // Sketch A
477        let sketch_a = if use_srht {
478            let srht = SubsampledRandomizedHadamard::new(m, sketch_dim, seed)?;
479            srht.apply_matrix(a)?
480        } else {
481            let sparse = SparseSign::new(sketch_dim, m, None, seed)?;
482            sparse.apply_matrix(a)?
483        };
484
485        // Thin QR of sketch: sketch_a = Q R
486        let (_, r_factor) = qr(&sketch_a.view(), None)?;
487
488        // Truncate R to n × n
489        let r_n = r_factor.nrows().min(n);
490        let r_square = r_factor
491            .slice(scirs2_core::ndarray::s![..r_n, ..n])
492            .to_owned();
493
494        // Estimate condition number via ratio of largest/smallest |diagonal|
495        let diag_abs: Vec<f64> = (0..r_n).map(|i| r_square[[i, i]].abs()).collect();
496        let r_max = diag_abs.iter().cloned().fold(0.0_f64, f64::max);
497        let r_min = diag_abs
498            .iter()
499            .cloned()
500            .fold(f64::INFINITY, f64::min)
501            .max(1e-300);
502        let cond_estimate = if r_max > 0.0 { r_max / r_min } else { 1e16 };
503
504        Ok(Self {
505            r_factor: r_square,
506            n: r_n,
507            sketch_dim,
508            cond_estimate,
509        })
510    }
511
512    /// Apply R⁻¹ to a vector: solve R x = v via back substitution.
513    pub fn apply_r_inverse(&self, v: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
514        let n = self.n;
515        if v.len() != n {
516            return Err(LinalgError::DimensionError(format!(
517                "BlendenpikPreconditioner::apply_r_inverse: v has {} elements but n={}",
518                v.len(),
519                n
520            )));
521        }
522
523        // Back substitution: R x = v
524        let mut x = Array1::zeros(n);
525        for i in (0..n).rev() {
526            let mut sum = v[i];
527            for j in (i + 1)..n {
528                sum -= self.r_factor[[i, j]] * x[j];
529            }
530            let diag = self.r_factor[[i, i]];
531            if diag.abs() < 1e-300 {
532                return Err(LinalgError::SingularMatrixError(format!(
533                    "Blendenpik R is singular at diagonal index {i}"
534                )));
535            }
536            x[i] = sum / diag;
537        }
538
539        Ok(x)
540    }
541
542    /// Apply R (forward multiplication): y = R * x
543    pub fn apply_r(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
544        let n = self.n;
545        if x.len() != n {
546            return Err(LinalgError::DimensionError(format!(
547                "BlendenpikPreconditioner::apply_r: x has {} elements but n={}",
548                x.len(),
549                n
550            )));
551        }
552
553        let mut y = Array1::zeros(n);
554        for i in 0..n {
555            let mut val = 0.0f64;
556            for j in i..n {
557                val += self.r_factor[[i, j]] * x[j];
558            }
559            y[i] = val;
560        }
561
562        Ok(y)
563    }
564
565    /// Solve the preconditioned normal equations using LSQR-style CG on normal equations.
566    ///
567    /// Minimizes ‖A x - b‖ using R as right preconditioner.
568    ///
569    /// # Arguments
570    ///
571    /// * `a` - Matrix A (m × n)
572    /// * `b` - Right-hand side (m-vector)
573    /// * `max_iter` - Maximum iterations
574    /// * `tol` - Convergence tolerance
575    pub fn solve(
576        &self,
577        a: &ArrayView2<f64>,
578        b: &ArrayView1<f64>,
579        max_iter: Option<usize>,
580        tol: Option<f64>,
581    ) -> LinalgResult<RandomizedPrecondResult> {
582        let (m, n_a) = a.dim();
583        if m != b.len() {
584            return Err(LinalgError::DimensionError(format!(
585                "BlendenpikPreconditioner::solve: A has {} rows but b has {} elements",
586                m,
587                b.len()
588            )));
589        }
590
591        let max_it = max_iter.unwrap_or(200);
592        let tolerance = tol.unwrap_or(1e-10);
593
594        // Solve preconditioned normal equations via LSQR on (A R⁻¹) y = b
595        // Using CG on normal equations: (R⁻ᵀ Aᵀ A R⁻¹) y = R⁻ᵀ Aᵀ b
596        let n = self.n.min(n_a);
597
598        // Compute Aᵀb
599        let mut atb = Array1::zeros(n);
600        for i in 0..n {
601            let mut val = 0.0f64;
602            for k in 0..m {
603                val += a[[k, i]] * b[k];
604            }
605            atb[i] = val;
606        }
607
608        // Initial solution: y = 0, x = R⁻¹ y = 0
609        let mut y = Array1::zeros(n);
610
611        // Compute R⁻ᵀ Aᵀ b = gradient at y=0
612        // R⁻ᵀ z means solve Rᵀ g = z
613        let mut g = self.apply_r_transpose_inverse(&atb.view())?;
614
615        // Preconditioned residual: r = R⁻ᵀ Aᵀ b - R⁻ᵀ Aᵀ A R⁻¹ y = g (since y=0)
616        let mut p = g.clone();
617        let mut rr = dot_product(&g.view(), &g.view());
618
619        let mut converged = false;
620        let mut iterations = 0;
621
622        for iter in 0..max_it {
623            iterations = iter + 1;
624
625            // Compute A R⁻¹ p
626            let rp = self.apply_r_inverse(&p.view())?;
627            // A * rp
628            let mut arp = Array1::zeros(m);
629            for i in 0..m {
630                let mut val = 0.0f64;
631                for j in 0..n.min(n_a) {
632                    val += a[[i, j]] * rp[j];
633                }
634                arp[i] = val;
635            }
636            // Aᵀ * arp
637            let mut aarp = Array1::zeros(n);
638            for i in 0..n {
639                let mut val = 0.0f64;
640                for k in 0..m {
641                    val += a[[k, i]] * arp[k];
642                }
643                aarp[i] = val;
644            }
645            // R⁻ᵀ * aarp
646            let q = self.apply_r_transpose_inverse(&aarp.view())?;
647
648            let pq = dot_product(&p.view(), &q.view());
649            if pq.abs() < 1e-300 {
650                break;
651            }
652
653            let alpha = rr / pq;
654            y = y + alpha * &p;
655            g = g - alpha * &q;
656
657            let rr_new = dot_product(&g.view(), &g.view());
658            let beta = rr_new / rr.max(1e-300);
659            rr = rr_new;
660
661            if rr.sqrt() < tolerance {
662                converged = true;
663                break;
664            }
665
666            p = &g + beta * &p;
667        }
668
669        // Recover x = R⁻¹ y
670        let solution = self.apply_r_inverse(&y.view())?;
671
672        // Compute residual
673        let mut ax = Array1::zeros(m);
674        for i in 0..m {
675            let mut val = 0.0f64;
676            for j in 0..n.min(n_a) {
677                val += a[[i, j]] * solution[j];
678            }
679            ax[i] = val;
680        }
681        let residual = (&ax - b).mapv(|v: f64| v * v).sum().sqrt();
682        let b_norm = b.mapv(|v: f64| v * v).sum().sqrt().max(1e-300);
683        let relative_residual = residual / b_norm;
684
685        Ok(RandomizedPrecondResult {
686            solution,
687            relative_residual,
688            iterations,
689            converged,
690            preconditioned_cond_estimate: Some(self.cond_estimate),
691        })
692    }
693
694    /// Apply R⁻ᵀ (solve Rᵀ x = v via forward substitution).
695    fn apply_r_transpose_inverse(&self, v: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
696        let n = self.n;
697        if v.len() != n {
698            return Err(LinalgError::DimensionError(format!(
699                "apply_r_transpose_inverse: v has {} elements but n={}",
700                v.len(),
701                n
702            )));
703        }
704
705        let mut x = Array1::zeros(n);
706        // Rᵀ is lower triangular; forward substitution
707        for i in 0..n {
708            let mut sum = v[i];
709            for j in 0..i {
710                sum -= self.r_factor[[j, i]] * x[j];
711            }
712            let diag = self.r_factor[[i, i]];
713            if diag.abs() < 1e-300 {
714                return Err(LinalgError::SingularMatrixError(format!(
715                    "Blendenpik R^T is singular at index {i}"
716                )));
717            }
718            x[i] = sum / diag;
719        }
720
721        Ok(x)
722    }
723}
724
725// ============================================================================
726// LSRN: Least Squares via Random Normal preconditioning
727// ============================================================================
728
729/// LSRN: Least-Squares via Random Normal preconditioning.
730///
731/// Uses a standard Gaussian sketch S (k × m, k = oversampling * n) to construct
732/// a preconditioner via SVD of S*A, then solves the preconditioned system
733/// iteratively. Particularly suited for highly overdetermined systems.
734///
735/// Algorithm:
736/// 1. Sample S ~ N(0, 1/k) of shape (k × m)
737/// 2. Compute B = S A (k × n)
738/// 3. SVD: B = U Σ Vᵀ
739/// 4. Preconditioner: N = V Σ⁻¹ (n × k, effective right preconditioner)
740/// 5. Solve preconditioned system (A N) y = b via LSQR
741/// 6. Recover x = N y
742#[derive(Debug, Clone)]
743pub struct LSRNPreconditioner {
744    /// Right preconditioner factor V (n × rank)
745    pub v_factor: Array2<f64>,
746    /// Inverse singular values (length rank)
747    pub sigma_inv: Array1<f64>,
748    /// Number of columns of A
749    pub n: usize,
750    /// Effective rank used
751    pub rank: usize,
752    /// Sketch dimension k
753    pub sketch_dim: usize,
754}
755
756impl LSRNPreconditioner {
757    /// Build the LSRN preconditioner from matrix A.
758    ///
759    /// # Arguments
760    ///
761    /// * `a` - Overdetermined matrix (m × n, m ≥ n)
762    /// * `oversampling` - Sketch oversampling factor (default: 2)
763    /// * `rcond` - Relative threshold for truncated SVD (default: 1e-12)
764    /// * `seed` - Optional RNG seed
765    pub fn new(
766        a: &ArrayView2<f64>,
767        oversampling: Option<f64>,
768        rcond: Option<f64>,
769        seed: Option<u64>,
770    ) -> LinalgResult<Self> {
771        let (m, n) = a.dim();
772        if m < n {
773            return Err(LinalgError::ShapeError(format!(
774                "LSRN requires m ≥ n, got {}×{}",
775                m, n
776            )));
777        }
778
779        let alpha = oversampling.unwrap_or(2.0).max(1.0);
780        let k = ((alpha * n as f64) as usize).max(n + 1).min(m);
781        let threshold = rcond.unwrap_or(1e-12);
782
783        // Generate Gaussian sketch S of shape (k × m)
784        let s_matrix = gaussian_random_matrix(k, m, seed)?;
785
786        // Compute B = S * A (k × n)
787        let mut b_sketch = Array2::zeros((k, n));
788        let scale = 1.0 / (k as f64).sqrt();
789        for i in 0..k {
790            for j in 0..n {
791                let mut val = 0.0f64;
792                for l in 0..m {
793                    val += s_matrix[[i, l]] * a[[l, j]];
794                }
795                b_sketch[[i, j]] = scale * val;
796            }
797        }
798
799        // Thin SVD of B
800        let (_, sigma, vt) = crate::decomposition::svd(&b_sketch.view(), true, None)?;
801
802        let sigma_max = sigma[0].max(1e-300);
803        let tol = threshold * sigma_max;
804
805        // Determine effective rank
806        let rank = sigma.iter().filter(|&&s| s > tol).count().max(1);
807        let rank = rank.min(n);
808
809        // Build preconditioner: N = V * Sigma^{-1}
810        // Vᵀ has shape (rank × n); V has shape (n × rank)
811        let v_factor = vt
812            .slice(scirs2_core::ndarray::s![..rank, ..])
813            .t()
814            .to_owned();
815        let sigma_inv: Array1<f64> =
816            sigma
817                .slice(scirs2_core::ndarray::s![..rank])
818                .mapv(|s| if s > tol { 1.0 / s } else { 0.0 });
819
820        Ok(Self {
821            v_factor,
822            sigma_inv,
823            n,
824            rank,
825            sketch_dim: k,
826        })
827    }
828
829    /// Apply the preconditioner N = V Σ⁻¹: y = N * x (n-vector output from rank-vector input).
830    pub fn apply_n(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
831        if x.len() != self.rank {
832            return Err(LinalgError::DimensionError(format!(
833                "LSRN::apply_n: x has {} elements but rank={}",
834                x.len(),
835                self.rank
836            )));
837        }
838
839        // N = V * Sigma^{-1}; apply: first scale by sigma_inv, then multiply by V
840        let mut scaled = Array1::zeros(self.rank);
841        for i in 0..self.rank {
842            scaled[i] = self.sigma_inv[i] * x[i];
843        }
844
845        let mut result = Array1::zeros(self.n);
846        for i in 0..self.n {
847            let mut val = 0.0f64;
848            for j in 0..self.rank {
849                val += self.v_factor[[i, j]] * scaled[j];
850            }
851            result[i] = val;
852        }
853
854        Ok(result)
855    }
856
857    /// Apply Nᵀ = Σ⁻¹ Vᵀ: y = Nᵀ * x (rank-vector output from n-vector input).
858    pub fn apply_n_transpose(&self, x: &ArrayView1<f64>) -> LinalgResult<Array1<f64>> {
859        if x.len() != self.n {
860            return Err(LinalgError::DimensionError(format!(
861                "LSRN::apply_n_transpose: x has {} elements but n={}",
862                x.len(),
863                self.n
864            )));
865        }
866
867        // Nᵀ x = Σ⁻¹ Vᵀ x
868        let mut vt_x = Array1::zeros(self.rank);
869        for i in 0..self.rank {
870            let mut val = 0.0f64;
871            for j in 0..self.n {
872                val += self.v_factor[[j, i]] * x[j];
873            }
874            vt_x[i] = self.sigma_inv[i] * val;
875        }
876
877        Ok(vt_x)
878    }
879
880    /// Solve the least-squares problem min ‖A x - b‖ using LSRN preconditioning.
881    ///
882    /// Uses LSQR on the preconditioned system min ‖(A N) y - b‖, then x = N y.
883    pub fn solve(
884        &self,
885        a: &ArrayView2<f64>,
886        b: &ArrayView1<f64>,
887        max_iter: Option<usize>,
888        tol: Option<f64>,
889    ) -> LinalgResult<RandomizedPrecondResult> {
890        let (m, n) = a.dim();
891        if b.len() != m {
892            return Err(LinalgError::DimensionError(format!(
893                "LSRN::solve: A has {} rows but b has {} elements",
894                m,
895                b.len()
896            )));
897        }
898
899        let max_it = max_iter.unwrap_or(300);
900        let tolerance = tol.unwrap_or(1e-10);
901        let rank = self.rank;
902
903        // Compute Aᵀb
904        let mut atb = Array1::zeros(n);
905        for i in 0..n {
906            let mut val = 0.0f64;
907            for k in 0..m {
908                val += a[[k, i]] * b[k];
909            }
910            atb[i] = val;
911        }
912
913        // Transform: Nᵀ Aᵀ b
914        let nt_atb = self.apply_n_transpose(&atb.view())?;
915
916        // CG on normal equations in y-space: (Nᵀ Aᵀ A N) y = Nᵀ Aᵀ b
917        let mut y = Array1::zeros(rank);
918        let mut g = nt_atb.clone();
919        let mut p = g.clone();
920        let mut rr = dot_product(&g.view(), &g.view());
921
922        let mut converged = false;
923        let mut iterations = 0;
924
925        for iter in 0..max_it {
926            iterations = iter + 1;
927
928            // q = Nᵀ Aᵀ A N p
929            let np = self.apply_n(&p.view())?;
930            // A * np
931            let mut anp = Array1::zeros(m);
932            for i in 0..m {
933                let mut val = 0.0f64;
934                for j in 0..n.min(self.n) {
935                    val += a[[i, j]] * np[j];
936                }
937                anp[i] = val;
938            }
939            // Aᵀ * anp
940            let mut at_anp = Array1::zeros(n);
941            for i in 0..n {
942                let mut val = 0.0f64;
943                for k in 0..m {
944                    val += a[[k, i]] * anp[k];
945                }
946                at_anp[i] = val;
947            }
948            // Nᵀ * at_anp
949            let q = self.apply_n_transpose(&at_anp.view())?;
950
951            let pq = dot_product(&p.view(), &q.view());
952            if pq.abs() < 1e-300 {
953                break;
954            }
955
956            let alpha = rr / pq;
957            y = y + alpha * &p;
958            g = g - alpha * &q;
959
960            let rr_new = dot_product(&g.view(), &g.view());
961            let beta = rr_new / rr.max(1e-300);
962            rr = rr_new;
963
964            if rr.sqrt() < tolerance {
965                converged = true;
966                break;
967            }
968
969            p = &g + beta * &p;
970        }
971
972        // Recover x = N y
973        let solution = self.apply_n(&y.view())?;
974
975        // Compute residual
976        let mut ax = Array1::zeros(m);
977        for i in 0..m {
978            let mut val = 0.0f64;
979            for j in 0..n.min(self.n) {
980                val += a[[i, j]] * solution[j];
981            }
982            ax[i] = val;
983        }
984        let residual = (&ax - b).mapv(|v: f64| v * v).sum().sqrt();
985        let b_norm = b.mapv(|v: f64| v * v).sum().sqrt().max(1e-300);
986        let relative_residual = residual / b_norm;
987
988        Ok(RandomizedPrecondResult {
989            solution,
990            relative_residual,
991            iterations,
992            converged,
993            preconditioned_cond_estimate: None,
994        })
995    }
996}
997
998// ============================================================================
999// IterativeRefinementLS
1000// ============================================================================
1001
1002/// Iterative refinement for least-squares with a random preconditioner.
1003///
1004/// Combines Blendenpik/LSRN preconditioning with iterative refinement steps
1005/// to achieve high-accuracy solutions even for ill-conditioned problems.
1006///
1007/// Algorithm:
1008/// 1. Compute initial solution x₀ using Blendenpik/LSRN
1009/// 2. Compute residual r = b - A x₀
1010/// 3. Solve the correction problem: x₁ = x₀ + δ where δ minimizes ‖A δ - r‖
1011/// 4. Repeat until residual tolerance is met
1012#[derive(Debug, Clone)]
1013pub struct IterativeRefinementLS {
1014    /// Maximum number of refinement steps
1015    pub max_refinement_steps: usize,
1016    /// Convergence tolerance for each inner solve
1017    pub inner_tol: f64,
1018    /// Overall convergence tolerance
1019    pub outer_tol: f64,
1020    /// Use Blendenpik (true) or LSRN (false) as inner preconditioner
1021    pub use_blendenpik: bool,
1022    /// Oversampling factor for the preconditioner
1023    pub oversampling: f64,
1024    /// Random seed
1025    pub seed: Option<u64>,
1026}
1027
1028impl IterativeRefinementLS {
1029    /// Create with default parameters.
1030    pub fn new() -> Self {
1031        Self {
1032            max_refinement_steps: 3,
1033            inner_tol: 1e-8,
1034            outer_tol: 1e-12,
1035            use_blendenpik: true,
1036            oversampling: 2.0,
1037            seed: None,
1038        }
1039    }
1040
1041    /// Set the maximum number of refinement steps.
1042    pub fn with_max_steps(mut self, steps: usize) -> Self {
1043        self.max_refinement_steps = steps;
1044        self
1045    }
1046
1047    /// Set inner solver tolerance.
1048    pub fn with_inner_tol(mut self, tol: f64) -> Self {
1049        self.inner_tol = tol;
1050        self
1051    }
1052
1053    /// Set outer convergence tolerance.
1054    pub fn with_outer_tol(mut self, tol: f64) -> Self {
1055        self.outer_tol = tol;
1056        self
1057    }
1058
1059    /// Use LSRN instead of Blendenpik.
1060    pub fn with_lsrn(mut self) -> Self {
1061        self.use_blendenpik = false;
1062        self
1063    }
1064
1065    /// Set oversampling factor.
1066    pub fn with_oversampling(mut self, alpha: f64) -> Self {
1067        self.oversampling = alpha;
1068        self
1069    }
1070
1071    /// Set random seed.
1072    pub fn with_seed(mut self, seed: u64) -> Self {
1073        self.seed = Some(seed);
1074        self
1075    }
1076
1077    /// Solve min ‖A x - b‖ with iterative refinement.
1078    pub fn solve(
1079        &self,
1080        a: &ArrayView2<f64>,
1081        b: &ArrayView1<f64>,
1082    ) -> LinalgResult<RandomizedPrecondResult> {
1083        let (m, n) = a.dim();
1084        if b.len() != m {
1085            return Err(LinalgError::DimensionError(format!(
1086                "IterativeRefinementLS: A has {} rows but b has {} elements",
1087                m,
1088                b.len()
1089            )));
1090        }
1091
1092        // Compute initial solution
1093        let mut result = if self.use_blendenpik {
1094            let precond =
1095                BlendenpikPreconditioner::new(a, Some(self.oversampling), true, self.seed)?;
1096            precond.solve(a, b, Some(200), Some(self.inner_tol))?
1097        } else {
1098            let precond = LSRNPreconditioner::new(a, Some(self.oversampling), None, self.seed)?;
1099            precond.solve(a, b, Some(300), Some(self.inner_tol))?
1100        };
1101
1102        let mut x = result.solution.clone();
1103        let mut total_iters = result.iterations;
1104
1105        // Iterative refinement loop
1106        for step in 0..self.max_refinement_steps {
1107            let rel_res = result.relative_residual;
1108            if rel_res < self.outer_tol {
1109                result.converged = true;
1110                break;
1111            }
1112
1113            // Compute residual r = b - A x
1114            let mut r = Array1::zeros(m);
1115            for i in 0..m {
1116                let mut ax_i = 0.0f64;
1117                for j in 0..n {
1118                    ax_i += a[[i, j]] * x[j];
1119                }
1120                r[i] = b[i] - ax_i;
1121            }
1122
1123            // Solve correction: min ‖A δ - r‖
1124            let correction_result = if self.use_blendenpik {
1125                // Reuse the preconditioner with a different seed
1126                let new_seed = self.seed.map(|s| s + step as u64 + 1);
1127                let precond =
1128                    BlendenpikPreconditioner::new(a, Some(self.oversampling), true, new_seed)?;
1129                precond.solve(a, &r.view(), Some(100), Some(self.inner_tol * 0.1))?
1130            } else {
1131                let new_seed = self.seed.map(|s| s + step as u64 + 1);
1132                let precond = LSRNPreconditioner::new(a, Some(self.oversampling), None, new_seed)?;
1133                precond.solve(a, &r.view(), Some(150), Some(self.inner_tol * 0.1))?
1134            };
1135
1136            x += &correction_result.solution;
1137            total_iters += correction_result.iterations;
1138
1139            // Recompute relative residual
1140            let mut ax = Array1::zeros(m);
1141            for i in 0..m {
1142                let mut val = 0.0f64;
1143                for j in 0..n {
1144                    val += a[[i, j]] * x[j];
1145                }
1146                ax[i] = val;
1147            }
1148            let res_norm = (&ax - b).mapv(|v: f64| v * v).sum().sqrt();
1149            let b_norm = b.mapv(|v: f64| v * v).sum().sqrt().max(1e-300);
1150            result.relative_residual = res_norm / b_norm;
1151        }
1152
1153        result.solution = x;
1154        result.iterations = total_iters;
1155
1156        Ok(result)
1157    }
1158}
1159
1160// ============================================================================
1161// Helper functions
1162// ============================================================================
1163
1164/// Dot product of two vectors
1165fn dot_product(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
1166    a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum()
1167}
1168
1169/// Generate a standard Gaussian random matrix (rows × cols)
1170fn gaussian_random_matrix(
1171    rows: usize,
1172    cols: usize,
1173    seed: Option<u64>,
1174) -> LinalgResult<Array2<f64>> {
1175    let mut rng = {
1176        let seed_val = seed.unwrap_or_else(|| {
1177            let mut tr = scirs2_core::random::rng();
1178            scirs2_core::random::RngExt::random::<u64>(&mut tr)
1179        });
1180        scirs2_core::random::seeded_rng(seed_val)
1181    };
1182
1183    let normal = Normal::new(0.0, 1.0).map_err(|e| {
1184        LinalgError::ComputationError(format!("Failed to create Normal distribution: {e}"))
1185    })?;
1186
1187    let mut matrix = Array2::zeros((rows, cols));
1188    for i in 0..rows {
1189        for j in 0..cols {
1190            matrix[[i, j]] = normal.sample(&mut rng);
1191        }
1192    }
1193
1194    Ok(matrix)
1195}
1196
1197// ============================================================================
1198// Tests
1199// ============================================================================
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204    use scirs2_core::ndarray::array;
1205
1206    #[test]
1207    fn test_sparse_sign_dimensions() {
1208        let ss = SparseSign::new(20, 10, Some(3), Some(42)).expect("SparseSign::new failed");
1209        assert_eq!(ss.rows, 20);
1210        assert_eq!(ss.cols, 10);
1211        assert_eq!(ss.sparsity, 3);
1212
1213        let x = Array1::ones(10);
1214        let y = ss.apply(&x.view()).expect("apply failed");
1215        assert_eq!(y.len(), 20);
1216    }
1217
1218    #[test]
1219    fn test_sparse_sign_linearity() {
1220        let ss = SparseSign::new(30, 15, Some(4), Some(7)).expect("SparseSign::new failed");
1221        let x1 = Array1::from_vec((0..15).map(|i| i as f64).collect());
1222        let x2 = Array1::from_vec((0..15).map(|i| (i + 1) as f64).collect());
1223
1224        let y1 = ss.apply(&x1.view()).expect("apply x1 failed");
1225        let y2 = ss.apply(&x2.view()).expect("apply x2 failed");
1226        let y12 = ss.apply(&(&x1 + &x2).view()).expect("apply x1+x2 failed");
1227
1228        for i in 0..30 {
1229            assert!((y12[i] - y1[i] - y2[i]).abs() < 1e-12);
1230        }
1231    }
1232
1233    #[test]
1234    fn test_srht_dimensions() {
1235        let srht = SubsampledRandomizedHadamard::new(64, 16, Some(99)).expect("SRHT::new failed");
1236        let x = Array1::ones(64);
1237        let y = srht.apply(&x.view()).expect("SRHT apply failed");
1238        assert_eq!(y.len(), 16);
1239    }
1240
1241    #[test]
1242    fn test_blendenpik_overdetermined() {
1243        // Build a well-conditioned overdetermined system
1244        let m = 20;
1245        let n = 5;
1246        let mut a = Array2::zeros((m, n));
1247        for i in 0..m {
1248            for j in 0..n {
1249                a[[i, j]] = ((i + j + 1) as f64).cos() + if i == j { 2.0 } else { 0.0 };
1250            }
1251        }
1252        let x_true = array![1.0, -1.0, 2.0, 0.5, -0.5];
1253        let mut b = Array1::zeros(m);
1254        for i in 0..m {
1255            for j in 0..n {
1256                b[i] += a[[i, j]] * x_true[j];
1257            }
1258        }
1259
1260        let precond = BlendenpikPreconditioner::new(&a.view(), Some(2.0), false, Some(1))
1261            .expect("Blendenpik::new failed");
1262        let result = precond
1263            .solve(&a.view(), &b.view(), Some(100), Some(1e-8))
1264            .expect("Blendenpik::solve failed");
1265
1266        assert!(
1267            result.relative_residual < 1e-4,
1268            "Blendenpik residual too large: {}",
1269            result.relative_residual
1270        );
1271    }
1272
1273    #[test]
1274    fn test_lsrn_overdetermined() {
1275        let m = 30;
1276        let n = 6;
1277        let mut a = Array2::zeros((m, n));
1278        for i in 0..m {
1279            for j in 0..n {
1280                a[[i, j]] = 0.5 * ((i * n + j) as f64 / (m * n) as f64);
1281                if i == j {
1282                    a[[i, j]] += 3.0;
1283                }
1284            }
1285        }
1286        let x_true = array![1.0, 2.0, -1.0, 0.5, -2.0, 0.0];
1287        let mut b = Array1::zeros(m);
1288        for i in 0..m {
1289            for j in 0..n {
1290                b[i] += a[[i, j]] * x_true[j];
1291            }
1292        }
1293
1294        let precond = LSRNPreconditioner::new(&a.view(), Some(2.0), Some(1e-12), Some(42))
1295            .expect("LSRN::new failed");
1296        let result = precond
1297            .solve(&a.view(), &b.view(), Some(200), Some(1e-8))
1298            .expect("LSRN::solve failed");
1299
1300        assert!(
1301            result.relative_residual < 1e-3,
1302            "LSRN residual too large: {}",
1303            result.relative_residual
1304        );
1305    }
1306
1307    #[test]
1308    fn test_iterative_refinement_ls() {
1309        let m = 25;
1310        let n = 5;
1311        let mut a = Array2::zeros((m, n));
1312        for i in 0..m {
1313            for j in 0..n {
1314                a[[i, j]] = (i as f64 * 0.1 + j as f64 * 0.3 + 1.0).sin();
1315                if i == j && j < n {
1316                    a[[i, j]] += 2.0;
1317                }
1318            }
1319        }
1320        let x_true = Array1::from_vec(vec![1.0, -1.0, 0.5, -0.5, 2.0]);
1321        let mut b = Array1::zeros(m);
1322        for i in 0..m {
1323            for j in 0..n {
1324                b[i] += a[[i, j]] * x_true[j];
1325            }
1326        }
1327
1328        let solver = IterativeRefinementLS::new()
1329            .with_max_steps(2)
1330            .with_outer_tol(1e-6)
1331            .with_seed(123);
1332
1333        let result = solver
1334            .solve(&a.view(), &b.view())
1335            .expect("IterativeRefinementLS::solve failed");
1336
1337        assert!(
1338            result.relative_residual < 1e-3,
1339            "IterativeRefinementLS residual too large: {}",
1340            result.relative_residual
1341        );
1342    }
1343}