diff --git a/ndarray-linalg/src/generate.rs b/ndarray-linalg/src/generate.rs index 67def14a..0ee2a07a 100644 --- a/ndarray-linalg/src/generate.rs +++ b/ndarray-linalg/src/generate.rs @@ -1,12 +1,15 @@ //! Generator functions for matrices +use ndarray::linalg::general_mat_mul; use ndarray::*; use rand::prelude::*; use super::convert::*; use super::error::*; use super::qr::*; +use super::rank::Rank; use super::types::*; +use super::Scalar; /// Hermite conjugate matrix pub fn conjugate<A, Si, So>(a: &ArrayBase<Si, Ix2>) -> ArrayBase<So, Ix2> @@ -34,6 +37,69 @@ where ArrayBase::from_shape_fn(sh, |_| A::rand(&mut rng)) } +/// Generate random array with a given rank +/// +/// The rank must be less then or equal to the smallest dimension of array +pub fn random_with_rank<A, Sh>(shape: Sh, rank: usize) -> Array2<A> +where + A: Scalar + Lapack, + Sh: ShapeBuilder<Dim = Ix2> + Clone, +{ + // handle zero-rank case + if rank == 0 { + return Array2::zeros(shape); + } + + let (n, m) = shape.clone().into_shape().raw_dim().into_pattern(); + let min_dim = usize::min(n, m); + assert!(rank <= min_dim); + + let mut rng = thread_rng(); + + // handle full-rank case + if rank == min_dim { + let mut out = random(shape); + for _ in 0..10 { + // check rank + if let Ok(out_rank) = out.rank() { + if out_rank == rank { + return out; + } + } + + out.mapv_inplace(|_| A::rand(&mut rng)); + } + + // handle partial-rank case + // + // multiplying two full-rank arrays with dimensions `m × r` and `r × n` will + // produce `an m × n` array with rank `r` + // https://en.wikipedia.org/wiki/Rank_(linear_algebra)#Properties + } else { + let mut out = Array2::zeros(shape); + let mut left: Array2<A> = random([out.nrows(), rank]); + let mut right: Array2<A> = random([rank, out.ncols()]); + + for _ in 0..10 { + general_mat_mul(A::one(), &left, &right, A::zero(), &mut out); + + // check rank + if let Ok(out_rank) = out.rank() { + if out_rank == rank { + return out; + } + } + + left.mapv_inplace(|_| A::rand(&mut rng)); + right.mapv_inplace(|_| A::rand(&mut rng)); + } + } + + unreachable!( + "Failed to generate random matrix of desired rank within 10 tries. This is very unlikely." + ); +} + /// Generate random unitary matrix using QR decomposition /// /// Be sure that this it **NOT** a uniform distribution. Use it only for test purpose. diff --git a/ndarray-linalg/src/lib.rs b/ndarray-linalg/src/lib.rs index ba9d10c5..aef7f3cb 100644 --- a/ndarray-linalg/src/lib.rs +++ b/ndarray-linalg/src/lib.rs @@ -64,7 +64,9 @@ pub mod lobpcg; pub mod norm; pub mod operator; pub mod opnorm; +pub mod pinv; pub mod qr; +pub mod rank; pub mod solve; pub mod solveh; pub mod svd; @@ -88,7 +90,9 @@ pub use lobpcg::{TruncatedEig, TruncatedOrder, TruncatedSvd}; pub use norm::*; pub use operator::*; pub use opnorm::*; +pub use pinv::*; pub use qr::*; +pub use rank::*; pub use solve::*; pub use solveh::*; pub use svd::*; diff --git a/ndarray-linalg/src/pinv.rs b/ndarray-linalg/src/pinv.rs new file mode 100644 index 00000000..9f5a36a2 --- /dev/null +++ b/ndarray-linalg/src/pinv.rs @@ -0,0 +1,116 @@ +//! Moore-Penrose pseudo-inverse of a Matrices +//! +//! [](https://hadrienj.github.io/posts/Deep-Learning-Book-Series-2.9-The-Moore-Penrose-Pseudoinverse/) + +use crate::{error::*, svd::SVDInplace, types::*}; +use ndarray::*; +use num_traits::Float; + +/// pseudo-inverse of a matrix reference +pub trait Pinv { + type E; + type C; + fn pinv(&self, threshold: Option<Self::E>) -> Result<Self::C>; +} + +/// pseudo-inverse +pub trait PInvInto { + type E; + type C; + fn pinv_into(self, rcond: Option<Self::E>) -> Result<Self::C>; +} + +/// pseudo-inverse for a mutable reference of a matrix +pub trait PInvInplace { + type E; + type C; + fn pinv_inplace(&mut self, rcond: Option<Self::E>) -> Result<Self::C>; +} + +impl<A, S> PInvInto for ArrayBase<S, Ix2> +where + A: Scalar + Lapack, + S: DataMut<Elem = A>, +{ + type E = A::Real; + type C = Array2<A>; + + fn pinv_into(mut self, rcond: Option<Self::E>) -> Result<Self::C> { + self.pinv_inplace(rcond) + } +} + +impl<A, S> Pinv for ArrayBase<S, Ix2> +where + A: Scalar + Lapack, + S: Data<Elem = A>, +{ + type E = A::Real; + type C = Array2<A>; + + fn pinv(&self, rcond: Option<Self::E>) -> Result<Self::C> { + let a = self.to_owned(); + a.pinv_into(rcond) + } +} + +impl<A, S> PInvInplace for ArrayBase<S, Ix2> +where + A: Scalar + Lapack, + S: DataMut<Elem = A>, +{ + type E = A::Real; + type C = Array2<A>; + + fn pinv_inplace(&mut self, rcond: Option<Self::E>) -> Result<Self::C> { + if let (Some(u), s, Some(v_h)) = self.svd_inplace(true, true)? { + // threshold = ε⋅max(m, n)⋅max(Σ) + // NumPy defaults rcond to 1e-15 which is about 10 * f64 machine epsilon + let rcond = rcond.unwrap_or_else(|| { + let (n, m) = self.dim(); + Self::E::epsilon() * Self::E::real(n.max(m)) + }); + let threshold = rcond * s[0]; + + // Determine how many singular values to keep and compute the + // values of `V Σ⁺` (up to `num_keep` columns). + let (num_keep, v_s_inv) = { + let mut v_h_t = v_h.reversed_axes(); + let mut num_keep = 0; + for (&sing_val, mut v_h_t_col) in s.iter().zip(v_h_t.columns_mut()) { + if sing_val > threshold { + let sing_val_recip = sing_val.recip(); + v_h_t_col.map_inplace(|v_h_t| { + *v_h_t = A::from_real(sing_val_recip) * v_h_t.conj() + }); + num_keep += 1; + } else { + /* + if sing_val != Self::E::real(0.0) { + panic!( + "for {:#?} singular value {:?} smaller then threshold {:?}", + &self, &sing_val, &threshold + ); + } + */ + break; + } + } + v_h_t.slice_axis_inplace(Axis(1), Slice::from(..num_keep)); + (num_keep, v_h_t) + }; + + // Compute `U^H` (up to `num_keep` rows). + let u_h = { + let mut u_t = u.reversed_axes(); + u_t.slice_axis_inplace(Axis(0), Slice::from(..num_keep)); + u_t.map_inplace(|x| *x = x.conj()); + u_t + }; + + Ok(v_s_inv.dot(&u_h)) + } else { + unreachable!() + } + } +} diff --git a/ndarray-linalg/src/rank.rs b/ndarray-linalg/src/rank.rs new file mode 100644 index 00000000..759960de --- /dev/null +++ b/ndarray-linalg/src/rank.rs @@ -0,0 +1,27 @@ +///! Computes the rank of a matrix using single value decomposition +use ndarray::*; + +use super::error::*; +use super::svd::SVD; +use super::types::*; +use num_traits::Float; + +pub trait Rank { + fn rank(&self) -> Result<Ix>; +} + +impl<A, S> Rank for ArrayBase<S, Ix2> +where + A: Scalar + Lapack, + S: Data<Elem = A>, +{ + fn rank(&self) -> Result<Ix> { + let (_, sv, _) = self.svd(false, false)?; + + let (n, m) = self.dim(); + let tol = A::Real::epsilon() * A::Real::real(n.max(m)) * sv[0]; + + let output = sv.iter().take_while(|v| v > &&tol).count(); + Ok(output) + } +} diff --git a/ndarray-linalg/tests/pinv.rs b/ndarray-linalg/tests/pinv.rs new file mode 100644 index 00000000..4829b137 --- /dev/null +++ b/ndarray-linalg/tests/pinv.rs @@ -0,0 +1,117 @@ +use ndarray::arr2; +use ndarray::*; +use ndarray_linalg::*; +use rand::{thread_rng, Rng}; + +/// create a zero rank array +pub fn zero_rank<A, Sh>(sh: Sh) -> Array2<A> +where + A: Scalar + Lapack, + Sh: ShapeBuilder<Dim = Ix2> + Clone, +{ + random_with_rank(sh, 0) +} + +/// create a random matrix with a random partial rank. +pub fn partial_rank<A, Sh>(sh: Sh) -> Array2<A> +where + A: Scalar + Lapack, + Sh: ShapeBuilder<Dim = Ix2> + Clone, +{ + let mut rng = thread_rng(); + let (m, n) = sh.clone().into_shape().raw_dim().into_pattern(); + let min_dim = n.min(m); + let rank = rng.gen_range(1..min_dim); + println!("desired rank = {}", rank); + random_with_rank(sh, rank) +} + +/// create a random matrix and ensures it is full rank. +pub fn full_rank<A, Sh>(sh: Sh) -> Array2<A> +where + A: Scalar + Lapack, + Sh: ShapeBuilder<Dim = Ix2> + Clone, +{ + let (m, n) = sh.clone().into_shape().raw_dim().into_pattern(); + let min_dim = n.min(m); + random_with_rank(sh, min_dim) +} + +fn test<T: Scalar + Lapack>(a: &Array2<T>, tolerance: T::Real) { + println!("a = \n{:?}", &a); + let a_plus: Array2<_> = a.pinv(None).unwrap(); + println!("a_plus = \n{:?}", &a_plus); + let ident = a.dot(&a_plus); + assert_close_l2!(&ident.dot(a), &a, tolerance); + assert_close_l2!(&a_plus.dot(&ident), &a_plus, tolerance); +} + +macro_rules! test_both_impl { + ($type:ty, $test:tt, $n:expr, $m:expr, $t:expr) => { + paste::item! { + #[test] + fn [<pinv_test_ $type _ $test _ $n x $m _r>]() { + let a: Array2<$type> = $test(($n, $m)); + test::<$type>(&a, $t); + } + + #[test] + fn [<pinv_test_ $type _ $test _ $n x $m _c>]() { + let a = $test(($n, $m).f()); + test::<$type>(&a, $t); + } + } + }; +} + +macro_rules! test_pinv_impl { + ($type:ty, $n:expr, $m:expr, $a:expr) => { + test_both_impl!($type, zero_rank, $n, $m, $a); + test_both_impl!($type, partial_rank, $n, $m, $a); + test_both_impl!($type, full_rank, $n, $m, $a); + }; +} + +test_pinv_impl!(f32, 3, 3, 1e-4); +test_pinv_impl!(f32, 4, 3, 1e-4); +test_pinv_impl!(f32, 3, 4, 1e-4); + +test_pinv_impl!(c32, 3, 3, 1e-4); +test_pinv_impl!(c32, 4, 3, 1e-4); +test_pinv_impl!(c32, 3, 4, 1e-4); + +test_pinv_impl!(f64, 3, 3, 1e-12); +test_pinv_impl!(f64, 4, 3, 1e-12); +test_pinv_impl!(f64, 3, 4, 1e-12); + +test_pinv_impl!(c64, 3, 3, 1e-12); +test_pinv_impl!(c64, 4, 3, 1e-12); +test_pinv_impl!(c64, 3, 4, 1e-12); + +// +// This matrix was taken from 7.1.1 Test1 in +// "On Moore-Penrose Pseudoinverse Computation for Stiffness Matrices Resulting +// from Higher Order Approximation" by Marek Klimczak +// https://doi.org/10.1155/2019/5060397 +// +#[test] +fn pinv_test_single_value_less_then_threshold_3x3() { + #[rustfmt::skip] + let a: Array2<f64> = arr2(&[ + [ 1., -1., 0.], + [-1., 2., -1.], + [ 0., -1., 1.] + ], + ); + #[rustfmt::skip] + let a_plus_actual: Array2<f64> = arr2(&[ + [ 5. / 9., -1. / 9., -4. / 9.], + [-1. / 9., 2. / 9., -1. / 9.], + [-4. / 9., -1. / 9., 5. / 9.], + ], + ); + let a_plus: Array2<_> = a.pinv(None).unwrap(); + println!("a_plus -> {:?}", &a_plus); + println!("a_plus_actual -> {:?}", &a_plus); + assert_close_l2!(&a_plus, &a_plus_actual, 1e-15); +} diff --git a/ndarray-linalg/tests/rank.rs b/ndarray-linalg/tests/rank.rs new file mode 100644 index 00000000..3cb93d85 --- /dev/null +++ b/ndarray-linalg/tests/rank.rs @@ -0,0 +1,38 @@ +use ndarray::*; +use ndarray_linalg::*; + +#[test] +fn rank_test_zero_3x3() { + #[rustfmt::skip] + let a: Array2<f64> = arr2(&[ + [0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.], + ], + ); + assert_eq!(0, a.rank().unwrap()); +} + +#[test] +fn rank_test_partial_3x3() { + #[rustfmt::skip] + let a: Array2<f64> = arr2(&[ + [1., 2., 3.], + [4., 5., 6.], + [7., 8., 9.], + ], + ); + assert_eq!(2, a.rank().unwrap()); +} + +#[test] +fn rank_test_full_3x3() { + #[rustfmt::skip] + let a: Array2<f64> = arr2(&[ + [1., 0., 2.], + [2., 1., 0.], + [3., 2., 1.], + ], + ); + assert_eq!(3, a.rank().unwrap()); +}