1use crate::error::{LinalgError, LinalgResult};
10use scirs2_core::ndarray::{array, Array1, Array2, ArrayView2, ScalarOperand};
11use scirs2_core::numeric::{Float, NumAssign};
12use std::iter::Sum;
13
14pub struct LUDecomposition<F: Float> {
16 pub lu: Array2<F>,
18 pub piv: Vec<usize>,
20 pub sign: F,
22}
23
24pub struct QRDecomposition<F: Float> {
26 pub q: Array2<F>,
28 pub r: Array2<F>,
30}
31
32pub struct SVDDecomposition<F: Float> {
34 pub u: Array2<F>,
36 pub s: Array1<F>,
38 pub vt: Array2<F>,
40}
41
42pub struct EigDecomposition<F: Float> {
44 pub eigenvalues: Array1<F>,
46 pub eigenvectors: Array2<F>,
48}
49
50#[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 let mut lu = a.to_owned();
88
89 let mut piv = (0..n).collect::<Vec<usize>>();
91 let mut sign = F::one(); for k in 0..n.min(m) {
95 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 if max_val < F::epsilon() {
109 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 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 sign = -sign;
135 }
136
137 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#[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 if n < m {
188 return Err(LinalgError::ComputationError(
189 "QR decomposition requires rows >= columns".to_string(),
190 ));
191 }
192
193 let mut r = a.to_owned();
195
196 let mut q = Array2::zeros((n, n));
198 for i in 0..n {
199 q[[i, i]] = F::one();
200 }
201
202 for k in 0..m.min(n) {
204 let x = r.slice(scirs2_core::ndarray::s![k.., k]).to_owned();
206
207 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 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 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 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 let q = q.t().to_owned();
253
254 Ok(QRDecomposition { q, r })
255}
256
257#[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 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 let use_ata = n >= m; let (eigenvalues, eigenvectors) = if use_ata {
319 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 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 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 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 let (u, vt) = if use_ata {
364 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 let mut u = Array2::<F>::zeros((n, rank));
374 for i in 0..rank {
375 if s[i] > F::from(1e-14).unwrap() {
376 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 s[i] = norm;
383 }
384 }
385 }
386
387 modified_gram_schmidt(&mut u);
389
390 let vt = v_sorted.t().to_owned();
391 (u, vt)
392 } else {
393 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 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 s[i] = norm;
411 }
412 }
413 }
414
415 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 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 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 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 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 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 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#[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 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 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#[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 for k in current_cols..targetsize {
540 let mut new_vec = Array1::<F>::zeros(n_rows);
542 if k < n_rows {
543 new_vec[k] = F::one();
544 } else {
545 new_vec[k % n_rows] = F::one();
547 }
548
549 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 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 modified_gram_schmidt(&mut extended);
567
568 extended
569}
570
571#[allow(dead_code)]
594pub fn eig<F>(a: &ArrayView2<F>) -> LinalgResult<EigDecomposition<F>>
595where
596 F: Float + NumAssign,
597{
598 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 let mut eigenvalues = Array1::zeros(n);
618 let eigenvectors = Array2::eye(n);
619
620 for i in 0..n {
623 eigenvalues[i] = a[[i, i]];
624 }
625
626 Ok(EigDecomposition {
629 eigenvalues,
630 eigenvectors,
631 })
632}
633
634#[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 let mut l = Array2::zeros((n, n));
677
678 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 return Err(LinalgError::non_positive_definite_with_suggestions(
691 "Cholesky decomposition",
692 a.dim(),
693 None, ));
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 assert_relative_eq!(result.lu[[0, 0]], 4.0);
720 assert_relative_eq!(result.lu[[0, 1]], 3.0);
721
722 assert_relative_eq!(result.lu[[1, 0]], 0.5);
724
725 assert_relative_eq!(result.lu[[1, 1]], -0.5);
727
728 assert_eq!(result.piv[0], 1);
731 assert_eq!(result.piv[1], 0);
732
733 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 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 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 let pa = p.dot(&a);
753
754 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 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 let lt = l.t();
773 let product = l.dot(<);
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 assert_eq!(result.q.shape(), &[2, 2]);
788 assert_eq!(result.r.shape(), &[2, 2]);
789
790 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}