diff --git a/src/linalg/col_piv_qr.rs b/src/linalg/col_piv_qr.rs new file mode 100644 index 00000000..4fa97482 --- /dev/null +++ b/src/linalg/col_piv_qr.rs @@ -0,0 +1,333 @@ +use num::Zero; +#[cfg(feature = "serde-serialize")] +use serde::{Deserialize, Serialize}; + +use crate::allocator::{Allocator, Reallocator}; +use crate::base::{DefaultAllocator, Matrix, MatrixMN, MatrixN, Unit, VectorN}; +use crate::constraint::{SameNumberOfRows, ShapeConstraint}; +use crate::dimension::{Dim, DimMin, DimMinimum, U1}; +use crate::storage::{Storage, StorageMut}; +use crate::ComplexField; + +use crate::geometry::Reflection; +use crate::linalg::{householder, PermutationSequence}; + +/// The QR decomposition (with column pivoting) of a general matrix. +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-serialize", + serde(bound(serialize = "DefaultAllocator: Allocator + + Allocator>, + MatrixMN: Serialize, + PermutationSequence>: Serialize, + VectorN>: Serialize")) +)] +#[cfg_attr( + feature = "serde-serialize", + serde(bound(deserialize = "DefaultAllocator: Allocator + + Allocator>, + MatrixMN: Deserialize<'de>, + PermutationSequence>: Deserialize<'de>, + VectorN>: Deserialize<'de>")) +)] +#[derive(Clone, Debug)] +pub struct ColPivQR, C: Dim> +where + DefaultAllocator: Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, +{ + col_piv_qr: MatrixMN, + p: PermutationSequence>, + diag: VectorN>, +} + +impl, C: Dim> Copy for ColPivQR +where + DefaultAllocator: Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, + MatrixMN: Copy, + PermutationSequence>: Copy, + VectorN>: Copy, +{ +} + +impl, C: Dim> ColPivQR +where + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, +{ + /// Computes the ColPivQR decomposition using householder reflections. + pub fn new(mut matrix: MatrixMN) -> Self { + let (nrows, ncols) = matrix.data.shape(); + let min_nrows_ncols = nrows.min(ncols); + let mut p = PermutationSequence::identity_generic(min_nrows_ncols); + + let mut diag = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; + + if min_nrows_ncols.value() == 0 { + return ColPivQR { + col_piv_qr: matrix, + p, + diag, + }; + } + + for i in 0..min_nrows_ncols.value() { + let piv = matrix.slice_range(i.., i..).icamax_full(); + let col_piv = piv.1 + i; + matrix.swap_columns(i, col_piv); + p.append_permutation(i, col_piv); + + householder::clear_column_unchecked(&mut matrix, &mut diag[i], i, 0, None); + } + + ColPivQR { + col_piv_qr: matrix, + p, + diag, + } + } + + /// Retrieves the upper trapezoidal submatrix `R` of this decomposition. + #[inline] + pub fn r(&self) -> MatrixMN, C> + where + DefaultAllocator: Allocator, C>, + { + let (nrows, ncols) = self.col_piv_qr.data.shape(); + let mut res = self + .col_piv_qr + .rows_generic(0, nrows.min(ncols)) + .upper_triangle(); + res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); + res + } + + /// Retrieves the upper trapezoidal submatrix `R` of this decomposition. + /// + /// This is usually faster than `r` but consumes `self`. + #[inline] + pub fn unpack_r(self) -> MatrixMN, C> + where + DefaultAllocator: Reallocator, C>, + { + let (nrows, ncols) = self.col_piv_qr.data.shape(); + let mut res = self + .col_piv_qr + .resize_generic(nrows.min(ncols), ncols, N::zero()); + res.fill_lower_triangle(N::zero(), 1); + res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); + res + } + + /// Computes the orthogonal matrix `Q` of this decomposition. + pub fn q(&self) -> MatrixMN> + where + DefaultAllocator: Allocator>, + { + let (nrows, ncols) = self.col_piv_qr.data.shape(); + + // NOTE: we could build the identity matrix and call q_mul on it. + // Instead we don't so that we take in account the matrix sparseness. + let mut res = Matrix::identity_generic(nrows, nrows.min(ncols)); + let dim = self.diag.len(); + + for i in (0..dim).rev() { + let axis = self.col_piv_qr.slice_range(i.., i); + // TODO: sometimes, the axis might have a zero magnitude. + let refl = Reflection::new(Unit::new_unchecked(axis), N::zero()); + + let mut res_rows = res.slice_range_mut(i.., i..); + refl.reflect_with_sign(&mut res_rows, self.diag[i].signum()); + } + + res + } + /// Retrieves the column permutation of this decomposition. + #[inline] + pub fn p(&self) -> &PermutationSequence> { + &self.p + } + + /// Unpacks this decomposition into its two matrix factors. + pub fn unpack( + self, + ) -> ( + MatrixMN>, + MatrixMN, C>, + PermutationSequence>, + ) + where + DimMinimum: DimMin>, + DefaultAllocator: Allocator> + + Reallocator, C> + + Allocator<(usize, usize), DimMinimum>, + { + (self.q(), self.r(), self.p) + } + + #[doc(hidden)] + pub fn col_piv_qr_internal(&self) -> &MatrixMN { + &self.col_piv_qr + } + + /// Multiplies the provided matrix by the transpose of the `Q` matrix of this decomposition. + pub fn q_tr_mul(&self, rhs: &mut Matrix) + where + S2: StorageMut, + { + let dim = self.diag.len(); + + for i in 0..dim { + let axis = self.col_piv_qr.slice_range(i.., i); + let refl = Reflection::new(Unit::new_unchecked(axis), N::zero()); + + let mut rhs_rows = rhs.rows_range_mut(i..); + refl.reflect_with_sign(&mut rhs_rows, self.diag[i].signum().conjugate()); + } + } +} + +impl> ColPivQR +where + DefaultAllocator: + Allocator + Allocator + Allocator<(usize, usize), DimMinimum>, +{ + /// Solves the linear system `self * x = b`, where `x` is the unknown to be determined. + /// + /// Returns `None` if `self` is not invertible. + pub fn solve( + &self, + b: &Matrix, + ) -> Option> + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + DefaultAllocator: Allocator, + { + let mut res = b.clone_owned(); + + if self.solve_mut(&mut res) { + Some(res) + } else { + None + } + } + + /// Solves the linear system `self * x = b`, where `x` is the unknown to be determined. + /// + /// If the decomposed matrix is not invertible, this returns `false` and its input `b` is + /// overwritten with garbage. + pub fn solve_mut(&self, b: &mut Matrix) -> bool + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + { + assert_eq!( + self.col_piv_qr.nrows(), + b.nrows(), + "ColPivQR solve matrix dimension mismatch." + ); + assert!( + self.col_piv_qr.is_square(), + "ColPivQR solve: unable to solve a non-square system." + ); + + self.q_tr_mul(b); + let solved = self.solve_upper_triangular_mut(b); + self.p.inv_permute_rows(b); + + solved + } + + // TODO: duplicate code from the `solve` module. + fn solve_upper_triangular_mut( + &self, + b: &mut Matrix, + ) -> bool + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + { + let dim = self.col_piv_qr.nrows(); + + for k in 0..b.ncols() { + let mut b = b.column_mut(k); + for i in (0..dim).rev() { + let coeff; + + unsafe { + let diag = self.diag.vget_unchecked(i).modulus(); + + if diag.is_zero() { + return false; + } + + coeff = b.vget_unchecked(i).unscale(diag); + *b.vget_unchecked_mut(i) = coeff; + } + + b.rows_range_mut(..i) + .axpy(-coeff, &self.col_piv_qr.slice_range(..i, i), N::one()); + } + } + + true + } + + /// Computes the inverse of the decomposed matrix. + /// + /// Returns `None` if the decomposed matrix is not invertible. + pub fn try_inverse(&self) -> Option> { + assert!( + self.col_piv_qr.is_square(), + "ColPivQR inverse: unable to compute the inverse of a non-square matrix." + ); + + // TODO: is there a less naive method ? + let (nrows, ncols) = self.col_piv_qr.data.shape(); + let mut res = MatrixN::identity_generic(nrows, ncols); + + if self.solve_mut(&mut res) { + Some(res) + } else { + None + } + } + + /// Indicates if the decomposed matrix is invertible. + pub fn is_invertible(&self) -> bool { + assert!( + self.col_piv_qr.is_square(), + "ColPivQR: unable to test the invertibility of a non-square matrix." + ); + + for i in 0..self.diag.len() { + if self.diag[i].is_zero() { + return false; + } + } + + true + } + + /// Computes the determinant of the decomposed matrix. + pub fn determinant(&self) -> N { + let dim = self.col_piv_qr.nrows(); + assert!( + self.col_piv_qr.is_square(), + "ColPivQR determinant: unable to compute the determinant of a non-square matrix." + ); + + let mut res = N::one(); + for i in 0..dim { + res *= unsafe { *self.diag.vget_unchecked(i) }; + } + + res * self.p.determinant() + } +} diff --git a/src/linalg/decomposition.rs b/src/linalg/decomposition.rs index 67cc4c6a..7890748c 100644 --- a/src/linalg/decomposition.rs +++ b/src/linalg/decomposition.rs @@ -1,8 +1,8 @@ use crate::storage::Storage; use crate::{ - Allocator, Bidiagonal, Cholesky, ComplexField, DefaultAllocator, Dim, DimDiff, DimMin, - DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, Schur, SymmetricEigen, SymmetricTridiagonal, - LU, QR, SVD, U1, + Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff, + DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, Schur, SymmetricEigen, + SymmetricTridiagonal, LU, QR, SVD, U1, }; /// # Rectangular matrix decomposition @@ -13,8 +13,9 @@ use crate::{ /// | Decomposition | Factors | Details | /// | -------------------------|---------------------|--------------| /// | QR | `Q * R` | `Q` is an unitary matrix, and `R` is upper-triangular. | +/// | QR with column pivoting | `Q * R * P⁻¹` | `Q` is an unitary matrix, and `R` is upper-triangular. `P` is a permutation matrix. | /// | LU with partial pivoting | `P⁻¹ * L * U` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` is a permutation matrix. | -/// | LU with full pivoting | `P⁻¹ * L * U ~ Q⁻¹` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` and `Q` are permutation matrices. | +/// | LU with full pivoting | `P⁻¹ * L * U * Q⁻¹` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` and `Q` are permutation matrices. | /// | SVD | `U * Σ * Vᵀ` | `U` and `V` are two orthogonal matrices and `Σ` is a diagonal matrix containing the singular values. | impl> Matrix { /// Computes the bidiagonalization using householder reflections. @@ -60,6 +61,18 @@ impl> Matrix { QR::new(self.into_owned()) } + /// Computes the QR decomposition (with column pivoting) of this matrix. + pub fn col_piv_qr(self) -> ColPivQR + where + R: DimMin, + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, + { + ColPivQR::new(self.into_owned()) + } + /// Computes the Singular Value Decomposition using implicit shift. pub fn svd(self, compute_u: bool, compute_v: bool) -> SVD where diff --git a/src/linalg/mod.rs b/src/linalg/mod.rs index c602bfa7..df9ae7de 100644 --- a/src/linalg/mod.rs +++ b/src/linalg/mod.rs @@ -8,6 +8,7 @@ mod determinant; // TODO: this should not be needed. However, the exp uses // explicit float operations on `f32` and `f64`. We need to // get rid of these to allow exp to be used on a no-std context. +mod col_piv_qr; mod decomposition; #[cfg(feature = "std")] mod exp; @@ -31,6 +32,7 @@ mod symmetric_tridiagonal; pub use self::bidiagonal::*; pub use self::cholesky::*; +pub use self::col_piv_qr::*; pub use self::convolution::*; #[cfg(feature = "std")] pub use self::exp::*; diff --git a/src/linalg/qr.rs b/src/linalg/qr.rs index f404aa5a..fdf6b70a 100644 --- a/src/linalg/qr.rs +++ b/src/linalg/qr.rs @@ -60,8 +60,8 @@ where return QR { qr: matrix, diag }; } - for ite in 0..min_nrows_ncols.value() { - householder::clear_column_unchecked(&mut matrix, &mut diag[ite], ite, 0, None); + for i in 0..min_nrows_ncols.value() { + householder::clear_column_unchecked(&mut matrix, &mut diag[i], i, 0, None); } QR { qr: matrix, diag } diff --git a/tests/linalg/col_piv_qr.rs b/tests/linalg/col_piv_qr.rs new file mode 100644 index 00000000..e9ffba86 --- /dev/null +++ b/tests/linalg/col_piv_qr.rs @@ -0,0 +1,161 @@ +#[cfg_attr(rustfmt, rustfmt_skip)] + +use na::Matrix4; + +#[test] +fn col_piv_qr() { + let m = Matrix4::new( + 1.0, -1.0, 2.0, 1.0, -1.0, 3.0, -1.0, -1.0, 3.0, -5.0, 5.0, 3.0, 1.0, 2.0, 1.0, -2.0, + ); + let col_piv_qr = m.col_piv_qr(); + assert!(relative_eq!( + col_piv_qr.determinant(), + 0.0, + epsilon = 1.0e-7 + )); + + let (q, r, p) = col_piv_qr.unpack(); + + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); +} + +#[cfg(feature = "arbitrary")] +mod quickcheck_tests { + macro_rules! gen_tests( + ($module: ident, $scalar: ty) => { + mod $module { + use na::{DMatrix, DVector, Matrix3x5, Matrix4, Matrix4x3, Matrix5x3, Vector4}; + use std::cmp; + #[allow(unused_imports)] + use crate::core::helper::{RandScalar, RandComplex}; + + quickcheck! { + fn col_piv_qr(m: DMatrix<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.clone().col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = &q * &r; + p.inv_permute_columns(&mut qr); + + println!("m: {}", m); + println!("col_piv_qr: {}", &q * &r); + + relative_eq!(m, &qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_static_5_3(m: Matrix5x3<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_static_3_5(m: Matrix3x5<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_static_square(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + println!("{}{}{}{}", q, r, qr, m); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_solve(n: usize, nb: usize) -> bool { + if n != 0 && nb != 0 { + let n = cmp::min(n, 50); // To avoid slowing down the test too much. + let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. + let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + + let col_piv_qr = m.clone().col_piv_qr(); + let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + + if col_piv_qr.is_invertible() { + let sol1 = col_piv_qr.solve(&b1).unwrap(); + let sol2 = col_piv_qr.solve(&b2).unwrap(); + + return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && + relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) + } + } + + return true; + } + + fn col_piv_qr_solve_static(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + + if col_piv_qr.is_invertible() { + let sol1 = col_piv_qr.solve(&b1).unwrap(); + let sol2 = col_piv_qr.solve(&b2).unwrap(); + + relative_eq!(m * sol1, b1, epsilon = 1.0e-6) && + relative_eq!(m * sol2, b2, epsilon = 1.0e-6) + } + else { + false + } + } + + fn col_piv_qr_inverse(n: usize) -> bool { + let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. + let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + + if let Some(m1) = m.clone().col_piv_qr().try_inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; + + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) + } + else { + true + } + } + + fn col_piv_qr_inverse_static(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + + if let Some(m1) = col_piv_qr.try_inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; + + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) + } + else { + true + } + } + } + } + } + ); + + gen_tests!(complex, RandComplex); + gen_tests!(f64, RandScalar); +} diff --git a/tests/linalg/mod.rs b/tests/linalg/mod.rs index 7fc01396..66a0c06a 100644 --- a/tests/linalg/mod.rs +++ b/tests/linalg/mod.rs @@ -1,6 +1,7 @@ mod balancing; mod bidiagonal; mod cholesky; +mod col_piv_qr; mod convolution; mod eigen; mod exp;