advanced_algorithms/numerical/
qr_decomposition.rs

1//! QR Decomposition via Householder Transformations
2//!
3//! QR decomposition factors a matrix A into the product of an orthogonal matrix Q
4//! and an upper triangular matrix R. This decomposition is more numerically stable
5//! than Gaussian elimination and is used for:
6//! - Solving linear least squares problems
7//! - Finding eigenvalues (via QR algorithm)
8//! - Computing matrix inverses
9//!
10//! # Examples
11//!
12//! ```
13//! use advanced_algorithms::numerical::qr_decomposition::QRDecomposition;
14//!
15//! let matrix = vec![
16//!     vec![12.0, -51.0, 4.0],
17//!     vec![6.0, 167.0, -68.0],
18//!     vec![-4.0, 24.0, -41.0],
19//! ];
20//!
21//! let qr = QRDecomposition::decompose(&matrix).unwrap();
22//! let q = qr.q();
23//! let r = qr.r();
24//! ```
25
26use crate::{AlgorithmError, Result};
27
28/// QR Decomposition result
29///
30/// Contains the Q (orthogonal) and R (upper triangular) matrices
31pub struct QRDecomposition {
32    q: Vec<Vec<f64>>,
33    r: Vec<Vec<f64>>,
34}
35
36impl QRDecomposition {
37    /// Performs QR decomposition using Householder transformations
38    ///
39    /// # Arguments
40    ///
41    /// * `matrix` - An m×n matrix where m >= n
42    ///
43    /// # Returns
44    ///
45    /// A QRDecomposition containing Q and R matrices
46    ///
47    /// # Errors
48    ///
49    /// Returns error if:
50    /// - Matrix is empty
51    /// - Matrix rows have inconsistent lengths
52    /// - m < n (more columns than rows)
53    pub fn decompose(matrix: &[Vec<f64>]) -> Result<Self> {
54        if matrix.is_empty() || matrix[0].is_empty() {
55            return Err(AlgorithmError::InvalidInput(
56                "Matrix cannot be empty".to_string()
57            ));
58        }
59        
60        let m = matrix.len();
61        let n = matrix[0].len();
62        
63        // Validate matrix dimensions
64        for row in matrix.iter() {
65            if row.len() != n {
66                return Err(AlgorithmError::InvalidInput(
67                    "All rows must have the same length".to_string()
68                ));
69            }
70        }
71        
72        if m < n {
73            return Err(AlgorithmError::InvalidInput(
74                "Matrix must have at least as many rows as columns".to_string()
75            ));
76        }
77        
78        // Initialize R with a copy of the input matrix
79        let mut r = matrix.to_vec();
80        
81        // Initialize Q as identity matrix
82        let mut q = identity_matrix(m);
83        
84        // Perform Householder transformations
85        for k in 0..n.min(m - 1) {
86            let h = householder_matrix(&r, k, m)?;
87            r = matrix_multiply(&h, &r)?;
88            q = matrix_multiply(&h, &q)?;
89        }
90        
91        // Transpose Q to get the final result
92        q = transpose(&q);
93        
94        Ok(QRDecomposition { q, r })
95    }
96    
97    /// Returns the Q matrix (orthogonal matrix)
98    pub fn q(&self) -> &[Vec<f64>] {
99        &self.q
100    }
101    
102    /// Returns the R matrix (upper triangular matrix)
103    pub fn r(&self) -> &[Vec<f64>] {
104        &self.r
105    }
106    
107    /// Solves the system Ax = b using QR decomposition
108    ///
109    /// # Arguments
110    ///
111    /// * `b` - Right-hand side vector
112    ///
113    /// # Returns
114    ///
115    /// Solution vector x
116    pub fn solve(&self, b: &[f64]) -> Result<Vec<f64>> {
117        if b.len() != self.q.len() {
118            return Err(AlgorithmError::DimensionMismatch(
119                format!("Vector length {} doesn't match matrix dimension {}", 
120                        b.len(), self.q.len())
121            ));
122        }
123        
124        // Compute Q^T * b
125        let qt_b = matrix_vector_multiply(&transpose(&self.q), b)?;
126        
127        // Solve Rx = Q^T*b by back substitution
128        back_substitution(&self.r, &qt_b)
129    }
130}
131
132/// Creates a Householder matrix for the k-th column
133fn householder_matrix(a: &[Vec<f64>], k: usize, m: usize) -> Result<Vec<Vec<f64>>> {
134    let _n = a[0].len();
135    
136    // Extract the k-th column from row k onwards
137    let mut x: Vec<f64> = (k..m).map(|i| a[i][k]).collect();
138    
139    // Compute the norm of x
140    let norm_x: f64 = x.iter().map(|&val| val * val).sum::<f64>().sqrt();
141    
142    if norm_x < 1e-10 {
143        // Column is already zeros below diagonal
144        return Ok(identity_matrix(m));
145    }
146    
147    // Compute the Householder vector
148    x[0] += if x[0] >= 0.0 { norm_x } else { -norm_x };
149    let norm_v: f64 = x.iter().map(|&val| val * val).sum::<f64>().sqrt();
150    
151    if norm_v < 1e-10 {
152        return Ok(identity_matrix(m));
153    }
154    
155    // Normalize
156    for val in x.iter_mut() {
157        *val /= norm_v;
158    }
159    
160    // Construct H = I - 2vv^T
161    let mut h = identity_matrix(m);
162    
163    for i in 0..(m - k) {
164        for j in 0..(m - k) {
165            h[i + k][j + k] -= 2.0 * x[i] * x[j];
166        }
167    }
168    
169    Ok(h)
170}
171
172/// Creates an identity matrix of size n×n
173fn identity_matrix(n: usize) -> Vec<Vec<f64>> {
174    let mut matrix = vec![vec![0.0; n]; n];
175    for (i, row) in matrix.iter_mut().enumerate() {
176        row[i] = 1.0;
177    }
178    matrix
179}
180
181/// Multiplies two matrices
182fn matrix_multiply(a: &[Vec<f64>], b: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
183    let m = a.len();
184    let n = b[0].len();
185    let p = a[0].len();
186    
187    if p != b.len() {
188        return Err(AlgorithmError::DimensionMismatch(
189            "Matrix dimensions incompatible for multiplication".to_string()
190        ));
191    }
192    
193    let mut result = vec![vec![0.0; n]; m];
194    
195    for i in 0..m {
196        for j in 0..n {
197            for k in 0..p {
198                result[i][j] += a[i][k] * b[k][j];
199            }
200        }
201    }
202    
203    Ok(result)
204}
205
206/// Multiplies a matrix by a vector
207fn matrix_vector_multiply(a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>> {
208    let m = a.len();
209    let n = a[0].len();
210    
211    if n != b.len() {
212        return Err(AlgorithmError::DimensionMismatch(
213            "Matrix and vector dimensions incompatible".to_string()
214        ));
215    }
216    
217    let mut result = vec![0.0; m];
218    
219    for i in 0..m {
220        for j in 0..n {
221            result[i] += a[i][j] * b[j];
222        }
223    }
224    
225    Ok(result)
226}
227
228/// Transposes a matrix
229fn transpose(matrix: &[Vec<f64>]) -> Vec<Vec<f64>> {
230    let m = matrix.len();
231    let n = matrix[0].len();
232    
233    let mut result = vec![vec![0.0; m]; n];
234    
235    for i in 0..m {
236        for j in 0..n {
237            result[j][i] = matrix[i][j];
238        }
239    }
240    
241    result
242}
243
244/// Solves an upper triangular system using back substitution
245fn back_substitution(r: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>> {
246    let n = r[0].len();
247    let mut x = vec![0.0; n];
248    
249    for i in (0..n).rev() {
250        if r[i][i].abs() < 1e-10 {
251            return Err(AlgorithmError::NumericalInstability(
252                "Matrix is singular or nearly singular".to_string()
253            ));
254        }
255        
256        let mut sum = b[i];
257        for j in (i + 1)..n {
258            sum -= r[i][j] * x[j];
259        }
260        x[i] = sum / r[i][i];
261    }
262    
263    Ok(x)
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    
270    #[test]
271    fn test_qr_decomposition() {
272        let matrix = vec![
273            vec![12.0, -51.0, 4.0],
274            vec![6.0, 167.0, -68.0],
275            vec![-4.0, 24.0, -41.0],
276        ];
277        
278        let qr = QRDecomposition::decompose(&matrix).unwrap();
279        let q = qr.q();
280        let r = qr.r();
281        
282        // Verify Q is orthogonal (Q^T * Q = I)
283        let qt = transpose(q);
284        let qtq = matrix_multiply(&qt, q).unwrap();
285        
286        for i in 0..3 {
287            for j in 0..3 {
288                let expected = if i == j { 1.0 } else { 0.0 };
289                assert!((qtq[i][j] - expected).abs() < 1e-10);
290            }
291        }
292        
293        // Verify R is upper triangular
294        for i in 0..3 {
295            for j in 0..i {
296                assert!(r[i][j].abs() < 1e-10);
297            }
298        }
299    }
300    
301    #[test]
302    fn test_solve() {
303        let matrix = vec![
304            vec![1.0, 2.0],
305            vec![3.0, 4.0],
306            vec![5.0, 6.0],
307        ];
308        
309        let b = vec![1.0, 2.0, 3.0];
310        
311        let qr = QRDecomposition::decompose(&matrix).unwrap();
312        let x = qr.solve(&b).unwrap();
313        
314        // Solution should minimize ||Ax - b||
315        assert_eq!(x.len(), 2);
316    }
317}