scirs2_linalg/
lapack.rs

1//! LAPACK (Linear Algebra Package) interface
2//!
3//! This module provides interfaces to LAPACK functions.
4//!
5//! LAPACK (Linear Algebra Package) provides routines for solving systems of
6//! linear equations, least-squares solutions of linear systems, eigenvalue
7//! problems, and singular value decomposition.
8
9use crate::error::{LinalgError, LinalgResult};
10use scirs2_core::ndarray::{array, Array1, Array2, ArrayView2, ScalarOperand};
11use scirs2_core::numeric::{Float, NumAssign};
12use std::iter::Sum;
13
14/// LU decomposition structure
15pub struct LUDecomposition<F: Float> {
16    /// LU decomposition result (combined L and U matrices)
17    pub lu: Array2<F>,
18    /// Permutation indices
19    pub piv: Vec<usize>,
20    /// Permutation sign (+1 or -1)
21    pub sign: F,
22}
23
24/// QR decomposition structure
25pub struct QRDecomposition<F: Float> {
26    /// Q matrix (orthogonal)
27    pub q: Array2<F>,
28    /// R matrix (upper triangular)
29    pub r: Array2<F>,
30}
31
32/// SVD decomposition structure
33pub struct SVDDecomposition<F: Float> {
34    /// U matrix (left singular vectors)
35    pub u: Array2<F>,
36    /// Singular values
37    pub s: Array1<F>,
38    /// V^T matrix (right singular vectors)
39    pub vt: Array2<F>,
40}
41
42/// Eigenvalue decomposition structure
43pub struct EigDecomposition<F: Float> {
44    /// Eigenvalues
45    pub eigenvalues: Array1<F>,
46    /// Eigenvectors (column-wise)
47    pub eigenvectors: Array2<F>,
48}
49
50/// Performs LU decomposition with partial pivoting.
51///
52/// # Arguments
53///
54/// * `a` - Input matrix
55///
56/// # Returns
57///
58/// * LU decomposition result
59///
60/// # Examples
61///
62/// ```
63/// use scirs2_core::ndarray::{array, ScalarOperand};
64/// use scirs2_linalg::lapack::lu_factor;
65///
66/// let a = array![[2.0, 1.0, 1.0], [4.0, 3.0, 3.0], [8.0, 7.0, 9.0]];
67/// let lu_result = lu_factor(&a.view()).unwrap();
68///
69/// // Check that P*A = L*U
70/// // (implementation dependent, so not shown here)
71/// ```
72#[allow(dead_code)]
73pub fn lu_factor<F>(a: &ArrayView2<F>) -> LinalgResult<LUDecomposition<F>>
74where
75    F: Float + NumAssign,
76{
77    let n = a.nrows();
78    let m = a.ncols();
79
80    if n == 0 || m == 0 {
81        return Err(LinalgError::ComputationError(
82            "Empty matrix provided".to_string(),
83        ));
84    }
85
86    // Create a copy of the input matrix that we'll update in-place
87    let mut lu = a.to_owned();
88
89    // Initialize permutation vector
90    let mut piv = (0..n).collect::<Vec<usize>>();
91    let mut sign = F::one(); // Keeps track of the permutation sign
92
93    // Gaussian elimination with partial pivoting
94    for k in 0..n.min(m) {
95        // Find pivot
96        let mut p = k;
97        let mut max_val = lu[[k, k]].abs();
98
99        for i in k + 1..n {
100            let abs_val = lu[[i, k]].abs();
101            if abs_val > max_val {
102                max_val = abs_val;
103                p = i;
104            }
105        }
106
107        // Check for singularity
108        if max_val < F::epsilon() {
109            // Calculate condition number estimate based on pivot ratio
110            let condition_estimate = if max_val > F::zero() {
111                Some((F::one() / max_val).to_f64().unwrap_or(1e16))
112            } else {
113                None
114            };
115
116            return Err(LinalgError::singularmatrix_with_suggestions(
117                "LU decomposition",
118                (n, m),
119                condition_estimate,
120            ));
121        }
122
123        // Swap rows if necessary
124        if p != k {
125            for j in 0..m {
126                let temp = lu[[k, j]];
127                lu[[k, j]] = lu[[p, j]];
128                lu[[p, j]] = temp;
129            }
130
131            piv.swap(k, p);
132
133            // Update permutation sign
134            sign = -sign;
135        }
136
137        // Compute multipliers and eliminate k-th column
138        for i in (k + 1)..n {
139            lu[[i, k]] = lu[[i, k]] / lu[[k, k]];
140
141            for j in k + 1..m {
142                lu[[i, j]] = lu[[i, j]] - lu[[i, k]] * lu[[k, j]];
143            }
144        }
145    }
146
147    Ok(LUDecomposition { lu, piv, sign })
148}
149
150/// Performs QR decomposition using Householder reflections.
151///
152/// # Arguments
153///
154/// * `a` - Input matrix
155///
156/// # Returns
157///
158/// * QR decomposition result
159///
160/// # Examples
161///
162/// ```
163/// use scirs2_core::ndarray::{array, ScalarOperand};
164/// use scirs2_linalg::lapack::qr_factor;
165///
166/// let a = array![[2.0, 1.0], [4.0, 3.0], [8.0, 7.0]];
167/// let qr_result = qr_factor(&a.view()).unwrap();
168///
169/// // Check that A = Q*R
170/// // (implementation dependent, so not shown here)
171/// ```
172#[allow(dead_code)]
173pub fn qr_factor<F>(a: &ArrayView2<F>) -> LinalgResult<QRDecomposition<F>>
174where
175    F: Float + NumAssign + Sum + Send + Sync + ScalarOperand + 'static,
176{
177    let n = a.nrows();
178    let m = a.ncols();
179
180    if n == 0 || m == 0 {
181        return Err(LinalgError::ComputationError(
182            "Empty matrix provided".to_string(),
183        ));
184    }
185
186    // Make sure we have at least as many rows as columns
187    if n < m {
188        return Err(LinalgError::ComputationError(
189            "QR decomposition requires rows >= columns".to_string(),
190        ));
191    }
192
193    // Make a copy of the input matrix
194    let mut r = a.to_owned();
195
196    // Initialize Q as identity matrix
197    let mut q = Array2::zeros((n, n));
198    for i in 0..n {
199        q[[i, i]] = F::one();
200    }
201
202    // Householder reflections
203    for k in 0..m.min(n) {
204        // Extract the k-th column from k-th row to bottom
205        let x = r.slice(scirs2_core::ndarray::s![k.., k]).to_owned();
206
207        // Compute the Householder vector
208        let mut v = x.clone();
209        let x_norm = x.iter().map(|&xi| xi * xi).sum::<F>().sqrt();
210
211        if x_norm > F::epsilon() {
212            let alpha = if x[0] >= F::zero() { -x_norm } else { x_norm };
213            v[0] -= alpha;
214
215            // Normalize v
216            let v_norm = v.iter().map(|&vi| vi * vi).sum::<F>().sqrt();
217            if v_norm > F::epsilon() {
218                for i in 0..v.len() {
219                    v[i] /= v_norm;
220                }
221
222                // Apply Householder reflection to R
223                for j in k..m {
224                    let column = r.slice(scirs2_core::ndarray::s![k.., j]).to_owned();
225                    let dot_product = v
226                        .iter()
227                        .zip(column.iter())
228                        .map(|(&vi, &ci)| vi * ci)
229                        .fold(F::zero(), |acc, val| acc + val);
230
231                    for i in k..n {
232                        r[[i, j]] -= F::from(2.0).unwrap() * v[i - k] * dot_product;
233                    }
234                }
235
236                // Apply Householder reflection to Q
237                for j in 0..n {
238                    let column = q.slice(scirs2_core::ndarray::s![.., j]).to_owned();
239                    let dot_product = (k..n)
240                        .map(|i| v[i - k] * column[i])
241                        .fold(F::zero(), |acc, val| acc + val);
242
243                    for i in k..n {
244                        q[[i, j]] -= F::from(2.0).unwrap() * v[i - k] * dot_product;
245                    }
246                }
247            }
248        }
249    }
250
251    // Transpose Q to get Q instead of Q^T
252    let q = q.t().to_owned();
253
254    Ok(QRDecomposition { q, r })
255}
256
257/// Performs singular value decomposition.
258///
259/// # Arguments
260///
261/// * `a` - Input matrix
262/// * `full_matrices` - Whether to return full U and V^T matrices
263///
264/// # Returns
265///
266/// * SVD decomposition result
267///
268/// # Examples
269///
270/// ```
271/// use scirs2_core::ndarray::{array, ScalarOperand};
272/// use scirs2_linalg::lapack::svd;
273///
274/// let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
275/// let svd_result = svd(&a.view(), false).unwrap();
276///
277/// // Check that A = U*diag(S)*V^T
278/// // (implementation dependent, so not shown here)
279/// ```
280#[allow(dead_code)]
281pub fn svd<F>(a: &ArrayView2<F>, fullmatrices: bool) -> LinalgResult<SVDDecomposition<F>>
282where
283    F: Float
284        + NumAssign
285        + scirs2_core::ndarray::ScalarOperand
286        + std::iter::Sum
287        + Send
288        + Sync
289        + 'static,
290{
291    let n = a.nrows();
292    let m = a.ncols();
293
294    if n == 0 || m == 0 {
295        return Err(LinalgError::ComputationError(
296            "Empty matrix provided".to_string(),
297        ));
298    }
299
300    // Special case for 1x1 matrix
301    if n == 1 && m == 1 {
302        let u = Array2::from_elem((1, 1), F::one());
303        let s = array![a[[0, 0]].abs()];
304        let vt = if a[[0, 0]] >= F::zero() {
305            Array2::from_elem((1, 1), F::one())
306        } else {
307            Array2::from_elem((1, 1), -F::one())
308        };
309        return Ok(SVDDecomposition { u, s, vt });
310    }
311
312    // Use a more stable approach: compute SVD via eigendecomposition
313    // but with better numerical stability
314
315    // For numerical stability, choose the smaller dimension
316    let use_ata = n >= m; // Use A^T*A if tall matrix, A*A^T if wide matrix
317
318    let (eigenvalues, eigenvectors) = if use_ata {
319        // Compute A^T * A for right singular vectors (when n >= m)
320        let a_t = a.t();
321        let ata = a_t.dot(a);
322
323        use crate::eigen::eigh;
324        match eigh(&ata.view(), None) {
325            Ok(result) => result,
326            Err(_) => {
327                return Err(LinalgError::ComputationError(
328                    "Failed to compute eigendecomposition for SVD".to_string(),
329                ));
330            }
331        }
332    } else {
333        // Compute A * A^T for left singular vectors (when m > n)
334        let aat = a.dot(&a.t());
335
336        use crate::eigen::eigh;
337        match eigh(&aat.view(), None) {
338            Ok(result) => result,
339            Err(_) => {
340                return Err(LinalgError::ComputationError(
341                    "Failed to compute eigendecomposition for SVD".to_string(),
342                ));
343            }
344        }
345    };
346
347    // Sort eigenvalues in descending order and filter out negative ones
348    let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
349    indices.sort_by(|&i, &j| {
350        eigenvalues[j]
351            .partial_cmp(&eigenvalues[i])
352            .unwrap_or(std::cmp::Ordering::Equal)
353    });
354
355    // Create singular values (square roots of eigenvalues, taking absolute value for stability)
356    let rank = n.min(m);
357    let mut s = Array1::<F>::zeros(rank);
358    for (new_idx, &old_idx) in indices.iter().enumerate().take(rank) {
359        s[new_idx] = eigenvalues[old_idx].abs().sqrt();
360    }
361
362    // Build U and V _matrices with improved orthogonality
363    let (u, vt) = if use_ata {
364        // We computed V from A^T*A, now compute U = A*V*S^(-1)
365        let mut v_sorted = Array2::<F>::zeros((m, rank));
366        for (new_idx, &old_idx) in indices.iter().enumerate().take(rank) {
367            v_sorted
368                .column_mut(new_idx)
369                .assign(&eigenvectors.column(old_idx));
370        }
371
372        // Compute U with better numerical stability
373        let mut u = Array2::<F>::zeros((n, rank));
374        for i in 0..rank {
375            if s[i] > F::from(1e-14).unwrap() {
376                // More conservative threshold
377                let av_col = a.dot(&v_sorted.column(i));
378                let norm = av_col.dot(&av_col).sqrt();
379                if norm > F::from(1e-14).unwrap() {
380                    u.column_mut(i).assign(&(&av_col / norm));
381                    // Recompute singular value more accurately
382                    s[i] = norm;
383                }
384            }
385        }
386
387        // Apply modified Gram-Schmidt for better orthogonality
388        modified_gram_schmidt(&mut u);
389
390        let vt = v_sorted.t().to_owned();
391        (u, vt)
392    } else {
393        // We computed U from A*A^T, now compute V = A^T*U*S^(-1)
394        let mut u_sorted = Array2::<F>::zeros((n, rank));
395        for (new_idx, &old_idx) in indices.iter().enumerate().take(rank) {
396            u_sorted
397                .column_mut(new_idx)
398                .assign(&eigenvectors.column(old_idx));
399        }
400
401        // Compute V with better numerical stability
402        let mut v = Array2::<F>::zeros((m, rank));
403        for i in 0..rank {
404            if s[i] > F::from(1e-14).unwrap() {
405                let atv_col = a.t().dot(&u_sorted.column(i));
406                let norm = atv_col.dot(&atv_col).sqrt();
407                if norm > F::from(1e-14).unwrap() {
408                    v.column_mut(i).assign(&(&atv_col / norm));
409                    // Recompute singular value more accurately
410                    s[i] = norm;
411                }
412            }
413        }
414
415        // Apply modified Gram-Schmidt for better orthogonality
416        modified_gram_schmidt(&mut u_sorted);
417        modified_gram_schmidt(&mut v);
418
419        let vt = v.t().to_owned();
420        (u_sorted, vt)
421    };
422
423    // Handle full _matrices case with better orthogonalization
424    let final_u = if fullmatrices && u.ncols() < n {
425        extend_to_orthogonal_basis(u, n)
426    } else {
427        u
428    };
429
430    let final_vt = if fullmatrices && vt.nrows() < m {
431        let v_extended = extend_to_orthogonal_basis(vt.t().to_owned(), m);
432        v_extended.t().to_owned()
433    } else {
434        vt
435    };
436
437    // Ensure singular values are sorted in descending order (required by SciPy compatibility)
438    let mut sort_indices: Vec<usize> = (0..s.len()).collect();
439    sort_indices.sort_by(|&i, &j| s[j].partial_cmp(&s[i]).unwrap());
440
441    // Check if sorting is needed
442    let needs_sorting = sort_indices.iter().enumerate().any(|(i, &j)| i != j);
443
444    let (final_u_sorted, final_s, final_vt_sorted) = if needs_sorting {
445        // Create sorted singular values
446        let mut s_sorted = Array1::<F>::zeros(s.len());
447        for (new_idx, &old_idx) in sort_indices.iter().enumerate() {
448            s_sorted[new_idx] = s[old_idx];
449        }
450
451        // Reorder U columns
452        let mut u_sorted = Array2::<F>::zeros(final_u.raw_dim());
453        for (new_idx, &old_idx) in sort_indices.iter().enumerate() {
454            if old_idx < final_u.ncols() && new_idx < u_sorted.ncols() {
455                u_sorted
456                    .column_mut(new_idx)
457                    .assign(&final_u.column(old_idx));
458            }
459        }
460
461        // Reorder Vt rows
462        let mut vt_sorted = Array2::<F>::zeros(final_vt.raw_dim());
463        for (new_idx, &old_idx) in sort_indices.iter().enumerate() {
464            if old_idx < final_vt.nrows() && new_idx < vt_sorted.nrows() {
465                vt_sorted.row_mut(new_idx).assign(&final_vt.row(old_idx));
466            }
467        }
468
469        (u_sorted, s_sorted, vt_sorted)
470    } else {
471        (final_u, s, final_vt)
472    };
473
474    Ok(SVDDecomposition {
475        u: final_u_sorted,
476        s: final_s,
477        vt: final_vt_sorted,
478    })
479}
480
481/// Modified Gram-Schmidt orthogonalization for better numerical stability
482#[allow(dead_code)]
483fn modified_gram_schmidt<F>(matrix: &mut Array2<F>)
484where
485    F: Float
486        + NumAssign
487        + scirs2_core::ndarray::ScalarOperand
488        + std::iter::Sum
489        + Send
490        + Sync
491        + 'static,
492{
493    let n_cols = matrix.ncols();
494
495    for i in 0..n_cols {
496        // Normalize column i
497        let mut col_i = matrix.column(i).to_owned();
498        let norm = col_i.dot(&col_i).sqrt();
499
500        if norm > F::from(1e-14).unwrap() {
501            col_i /= norm;
502            matrix.column_mut(i).assign(&col_i);
503
504            // Orthogonalize subsequent columns against column i
505            for j in (i + 1)..n_cols {
506                let mut col_j = matrix.column(j).to_owned();
507                let proj = col_i.dot(&col_j);
508                col_j = col_j - &col_i * proj;
509                matrix.column_mut(j).assign(&col_j);
510            }
511        }
512    }
513}
514
515/// Extend a matrix to form a complete orthogonal basis
516#[allow(dead_code)]
517fn extend_to_orthogonal_basis<F>(matrix: Array2<F>, targetsize: usize) -> Array2<F>
518where
519    F: Float
520        + NumAssign
521        + scirs2_core::ndarray::ScalarOperand
522        + std::iter::Sum
523        + Send
524        + Sync
525        + 'static,
526{
527    let current_cols = matrix.ncols();
528    if current_cols >= targetsize {
529        return matrix;
530    }
531
532    let n_rows = matrix.nrows();
533    let mut extended = Array2::<F>::zeros((n_rows, targetsize));
534    extended
535        .slice_mut(scirs2_core::ndarray::s![.., 0..current_cols])
536        .assign(&matrix);
537
538    // Add orthogonal vectors using QR decomposition approach
539    for k in current_cols..targetsize {
540        // Start with a random vector
541        let mut new_vec = Array1::<F>::zeros(n_rows);
542        if k < n_rows {
543            new_vec[k] = F::one();
544        } else {
545            // Use a different approach for overcomplete case
546            new_vec[k % n_rows] = F::one();
547        }
548
549        // Orthogonalize against existing columns using modified Gram-Schmidt
550        for j in 0..k {
551            let existing_col = extended.column(j);
552            let proj = existing_col.dot(&new_vec);
553            new_vec = new_vec - &existing_col * proj;
554        }
555
556        // Normalize
557        let norm = new_vec.dot(&new_vec).sqrt();
558        if norm > F::from(1e-14).unwrap() {
559            new_vec /= norm;
560        }
561
562        extended.column_mut(k).assign(&new_vec);
563    }
564
565    // Apply final Gram-Schmidt pass for better orthogonality
566    modified_gram_schmidt(&mut extended);
567
568    extended
569}
570
571/// Computes the eigenvalues and eigenvectors of a square matrix.
572///
573/// # Arguments
574///
575/// * `a` - Input square matrix
576///
577/// # Returns
578///
579/// * Eigenvalue decomposition result
580///
581/// # Examples
582///
583/// ```
584/// use scirs2_core::ndarray::{array, ScalarOperand};
585/// use scirs2_linalg::lapack::eig;
586///
587/// let a = array![[1.0, 2.0], [3.0, 4.0]];
588/// let eig_result = eig(&a.view()).unwrap();
589///
590/// // Check that A*V = V*diag(eigenvalues)
591/// // (implementation dependent, so not shown here)
592/// ```
593#[allow(dead_code)]
594pub fn eig<F>(a: &ArrayView2<F>) -> LinalgResult<EigDecomposition<F>>
595where
596    F: Float + NumAssign,
597{
598    // This is a placeholder implementation. A proper eigenvalue decomposition would use
599    // more efficient numerical methods like the QR algorithm.
600
601    let n = a.nrows();
602    let m = a.ncols();
603
604    if n == 0 || m == 0 {
605        return Err(LinalgError::ComputationError(
606            "Empty matrix provided".to_string(),
607        ));
608    }
609
610    if n != m {
611        return Err(LinalgError::DimensionError(
612            "Matrix must be square for eigenvalue decomposition".to_string(),
613        ));
614    }
615
616    // Create placeholder eigenvalues and eigenvectors
617    let mut eigenvalues = Array1::zeros(n);
618    let eigenvectors = Array2::eye(n);
619
620    // For demonstration, set diagonal elements as the eigenvalues
621    // This is only correct for diagonal matrices!
622    for i in 0..n {
623        eigenvalues[i] = a[[i, i]];
624    }
625
626    // Return the placeholder result
627    // In a real implementation, we would compute the actual eigenvalues and eigenvectors
628    Ok(EigDecomposition {
629        eigenvalues,
630        eigenvectors,
631    })
632}
633
634/// Computes the Cholesky decomposition of a symmetric positive-definite matrix.
635///
636/// # Arguments
637///
638/// * `a` - Input symmetric positive-definite matrix
639///
640/// # Returns
641///
642/// * The lower triangular matrix L such that A = L*L^T
643///
644/// # Examples
645///
646/// ```
647/// use scirs2_core::ndarray::{array, ScalarOperand};
648/// use scirs2_linalg::lapack::cholesky;
649///
650/// let a = array![[4.0, 2.0], [2.0, 5.0]];
651/// let l = cholesky(&a.view()).unwrap();
652///
653/// // Check that A = L*L^T
654/// // (implementation dependent, so not shown here)
655/// ```
656#[allow(dead_code)]
657pub fn cholesky<F>(a: &ArrayView2<F>) -> LinalgResult<Array2<F>>
658where
659    F: Float + NumAssign,
660{
661    let n = a.nrows();
662
663    if n == 0 || a.ncols() == 0 {
664        return Err(LinalgError::ComputationError(
665            "Empty matrix provided".to_string(),
666        ));
667    }
668
669    if n != a.ncols() {
670        return Err(LinalgError::DimensionError(
671            "Matrix must be square for Cholesky decomposition".to_string(),
672        ));
673    }
674
675    // Initialize the result as a copy of the input
676    let mut l = Array2::zeros((n, n));
677
678    // Cholesky-Banachiewicz algorithm
679    for i in 0..n {
680        for j in 0..=i {
681            let mut sum = F::zero();
682            for k in 0..j {
683                sum += l[[i, k]] * l[[j, k]];
684            }
685
686            if i == j {
687                let val = a[[i, i]] - sum;
688                if val <= F::zero() {
689                    // Use enhanced error with regularization suggestions
690                    return Err(LinalgError::non_positive_definite_with_suggestions(
691                        "Cholesky decomposition",
692                        a.dim(),
693                        None, // Could analyze eigenvalues to count negative ones
694                    ));
695                }
696                l[[i, j]] = val.sqrt();
697            } else {
698                l[[i, j]] = (a[[i, j]] - sum) / l[[j, j]];
699            }
700        }
701    }
702
703    Ok(l)
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use approx::assert_relative_eq;
710    use scirs2_core::ndarray::array;
711
712    #[test]
713    fn test_lu_factor() {
714        let a = array![[2.0, 1.0], [4.0, 3.0]];
715        let result = lu_factor(&a.view()).unwrap();
716
717        // Verify the specific values in our LU decomposition implementation
718        // First row should be [4.0, 3.0] due to pivoting
719        assert_relative_eq!(result.lu[[0, 0]], 4.0);
720        assert_relative_eq!(result.lu[[0, 1]], 3.0);
721
722        // L multiplier should be 0.5 because 2.0/4.0 = 0.5
723        assert_relative_eq!(result.lu[[1, 0]], 0.5);
724
725        // The actual value in our implementation is -0.5 due to the specific algorithm
726        assert_relative_eq!(result.lu[[1, 1]], -0.5);
727
728        // Verify that we can reconstruct the original matrix
729        // Permutation should be [1, 0] meaning row 1 (the second row) was moved to position 0
730        assert_eq!(result.piv[0], 1);
731        assert_eq!(result.piv[1], 0);
732
733        // Verify that we can reconstruct A from the LU decomposition
734        // Create L matrix with unit diagonal
735        let mut l = Array2::<f64>::zeros((2, 2));
736        l[[0, 0]] = 1.0;
737        l[[1, 0]] = result.lu[[1, 0]];
738        l[[1, 1]] = 1.0;
739
740        // Create U matrix
741        let mut u = Array2::<f64>::zeros((2, 2));
742        u[[0, 0]] = result.lu[[0, 0]];
743        u[[0, 1]] = result.lu[[0, 1]];
744        u[[1, 1]] = result.lu[[1, 1]];
745
746        // Create permutation matrix
747        let mut p = Array2::<f64>::zeros((2, 2));
748        p[[0, result.piv[0]]] = 1.0;
749        p[[1, result.piv[1]]] = 1.0;
750
751        // Get permuted original matrix
752        let pa = p.dot(&a);
753
754        // The final matrix element may differ in sign but this should still reconstruct the original matrix
755        assert_relative_eq!(pa[[0, 0]], 4.0);
756        assert_relative_eq!(pa[[0, 1]], 3.0);
757        assert_relative_eq!(pa[[1, 0]], 2.0);
758        assert_relative_eq!(pa[[1, 1]], 1.0);
759    }
760
761    #[test]
762    fn test_cholesky() {
763        let a = array![[4.0, 2.0], [2.0, 5.0]];
764        let l = cholesky(&a.view()).unwrap();
765
766        // Check some elements
767        assert_relative_eq!(l[[0, 0]], 2.0);
768        assert_relative_eq!(l[[1, 0]], 1.0);
769        assert_relative_eq!(l[[1, 1]], 2.0);
770
771        // Verify L*L^T = A
772        let lt = l.t();
773        let product = l.dot(&lt);
774
775        assert_relative_eq!(product[[0, 0]], a[[0, 0]]);
776        assert_relative_eq!(product[[0, 1]], a[[0, 1]]);
777        assert_relative_eq!(product[[1, 0]], a[[1, 0]]);
778        assert_relative_eq!(product[[1, 1]], a[[1, 1]]);
779    }
780
781    #[test]
782    fn test_qr_factor() {
783        let a = array![[2.0, 1.0], [4.0, 3.0]];
784        let result = qr_factor(&a.view()).unwrap();
785
786        // Basic check of dimensions
787        assert_eq!(result.q.shape(), &[2, 2]);
788        assert_eq!(result.r.shape(), &[2, 2]);
789
790        // Verify that Q is orthogonal (Q^T * Q = I)
791        let qt = result.q.t();
792        let q_orthogonal = qt.dot(&result.q);
793
794        assert_relative_eq!(q_orthogonal[[0, 0]], 1.0, epsilon = 1e-5);
795        assert_relative_eq!(q_orthogonal[[0, 1]], 0.0, epsilon = 1e-5);
796        assert_relative_eq!(q_orthogonal[[1, 0]], 0.0, epsilon = 1e-5);
797        assert_relative_eq!(q_orthogonal[[1, 1]], 1.0, epsilon = 1e-5);
798    }
799}