Skip to content

Commit 088f0b1

Browse files
committed
Improve documentation and add MP law test
1 parent 28c7860 commit 088f0b1

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

ndarray-linalg/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ paste = "1.0"
5050
criterion = "0.3"
5151
# Keep the same version as ndarray's dependency!
5252
approx = { version = "0.4", features = ["num-complex"] }
53+
rand_xoshiro = "0.6"
5354

5455
[[bench]]
5556
name = "truncated_eig"

ndarray-linalg/src/lobpcg/eig.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
//! Truncated eigenvalue decomposition
2+
//!
3+
14
use super::lobpcg::{lobpcg, LobpcgResult, Order};
25
use crate::{generate, Scalar};
36
use lax::Lapack;
47

5-
///! Implements truncated eigenvalue decomposition
6-
///
78
use ndarray::prelude::*;
89
use ndarray::stack;
910
use ndarray::ScalarOperand;
@@ -15,6 +16,20 @@ use num_traits::{Float, NumCast};
1516
/// parameter like maximal iteration, precision and constraint matrix. Furthermore it allows
1617
/// conversion into a iterative solver where each iteration step yields a new eigenvalue/vector
1718
/// pair.
19+
///
20+
/// # Example
21+
///
22+
/// ```rust
23+
/// let diag = arr1(&[1., 2., 3., 4., 5.]);
24+
/// let a = Array2::from_diag(&diag);
25+
///
26+
/// let eig = TruncatedEig::new(a, Order::Largest)
27+
/// .precision(1e-5)
28+
/// .maxiter(500);
29+
///
30+
/// let res = eig.decompose();
31+
/// ```
32+
1833
pub struct TruncatedEig<A: Scalar> {
1934
order: Order,
2035
problem: Array2<A>,
@@ -25,6 +40,11 @@ pub struct TruncatedEig<A: Scalar> {
2540
}
2641

2742
impl<A: Float + Scalar + ScalarOperand + Lapack + PartialOrd + Default> TruncatedEig<A> {
43+
/// Create a new truncated eigenproblem solver
44+
///
45+
/// # Properties
46+
/// * `problem`: problem matrix
47+
/// * `order`: ordering of the eigenvalues with [TruncatedOrder](crate::TruncatedOrder)
2848
pub fn new(problem: Array2<A>, order: Order) -> TruncatedEig<A> {
2949
TruncatedEig {
3050
precision: 1e-5,
@@ -36,31 +56,68 @@ impl<A: Float + Scalar + ScalarOperand + Lapack + PartialOrd + Default> Truncate
3656
}
3757
}
3858

59+
/// Set desired precision
60+
///
61+
/// This argument specifies the desired precision, which is passed to the LOBPCG solver. It
62+
/// controls at which point the opimization of each eigenvalue is stopped. The precision is
63+
/// global and applied to all eigenvalues with respect to their L2 norm.
64+
///
65+
/// If the precision can't be reached and the maximum number of iteration is reached, then an
66+
/// error is returned in [LobpcgResult](crate::lobpcg::LobpcgResult).
3967
pub fn precision(mut self, precision: f32) -> Self {
4068
self.precision = precision;
4169

4270
self
4371
}
4472

73+
/// Set the maximal number of iterations
74+
///
75+
/// The LOBPCG is an iterative approach to eigenproblems and stops when this maximum
76+
/// number of iterations are reached.
4577
pub fn maxiter(mut self, maxiter: usize) -> Self {
4678
self.maxiter = maxiter;
4779

4880
self
4981
}
5082

83+
/// Construct a solution, which is orthogonal to this
84+
///
85+
/// If a number of eigenvectors are already known, then this function can be used to construct
86+
/// a orthogonal subspace. Also used with an iterative approach.
5187
pub fn orthogonal_to(mut self, constraints: Array2<A>) -> Self {
5288
self.constraints = Some(constraints);
5389

5490
self
5591
}
5692

93+
/// Apply a preconditioner
94+
///
95+
/// A preconditioning matrix can speed up the solving process by improving the spectral
96+
/// distribution of the eigenvalues. It requires prior knowledge of the problem.
5797
pub fn precondition_with(mut self, preconditioner: Array2<A>) -> Self {
5898
self.preconditioner = Some(preconditioner);
5999

60100
self
61101
}
62102

63-
// calculate the eigenvalues decompose
103+
/// Calculate the eigenvalue decomposition
104+
///
105+
/// # Parameters
106+
///
107+
/// * `num`: number of eigenvalues ordered by magnitude
108+
///
109+
/// # Example
110+
///
111+
/// ```rust
112+
/// let diag = arr1(&[1., 2., 3., 4., 5.]);
113+
/// let a = Array2::from_diag(&diag);
114+
///
115+
/// let eig = TruncatedEig::new(a, Order::Largest)
116+
/// .precision(1e-5)
117+
/// .maxiter(500);
118+
///
119+
/// let res = eig.decompose();
120+
/// ```
64121
pub fn decompose(&self, num: usize) -> LobpcgResult<A> {
65122
let x: Array2<f64> = generate::random((self.problem.len_of(Axis(0)), num));
66123
let x = x.mapv(|x| NumCast::from(x).unwrap());
@@ -104,10 +161,24 @@ impl<A: Float + Scalar + ScalarOperand + Lapack + PartialOrd + Default> IntoIter
104161
}
105162
}
106163

107-
/// Truncate eigenproblem iterator
164+
/// Truncated eigenproblem iterator
108165
///
109166
/// This wraps a truncated eigenproblem and provides an iterator where each step yields a new
110167
/// eigenvalue/vector pair. Useful for generating pairs until a certain condition is met.
168+
///
169+
/// # Example
170+
///
171+
/// ```rust
172+
/// let teig = TruncatedEig::new(a, Order::Largest)
173+
/// .precision(1e-5)
174+
/// .maxiter(500);
175+
///
176+
/// // solve eigenproblem until eigenvalues get smaller than 0.5
177+
/// let res = teig.into_iter()
178+
/// .take_while(|x| x.0[0] > 0.5)
179+
/// .flat_map(|x| x.0)
180+
/// .collect();
181+
/// ```
111182
pub struct TruncatedEigIterator<A: Scalar> {
112183
step_size: usize,
113184
remaining: usize,

ndarray-linalg/src/lobpcg/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ mod eig;
22
mod lobpcg;
33
mod svd;
44

5-
pub use eig::TruncatedEig;
5+
pub use eig::{TruncatedEig, TruncatedEigIterator};
66
pub use lobpcg::{lobpcg, LobpcgResult, Order as TruncatedOrder};
7-
pub use svd::TruncatedSvd;
7+
pub use svd::{TruncatedSvd, MagnitudeCorrection};

ndarray-linalg/src/lobpcg/svd.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ impl<A: Float + Scalar + ScalarOperand + Lapack + PartialOrd + Default> Truncate
173173
}
174174
}
175175

176+
/// Magnitude Correction
177+
///
178+
/// The magnitude correction changes the cut-off point at which an eigenvector belongs to the
179+
/// null-space and its eigenvalue is therefore zero. The correction is multiplied by the floating
180+
/// point epsilon and therefore dependent on the floating type.
176181
pub trait MagnitudeCorrection {
177182
fn correction() -> Self;
178183
}
@@ -196,6 +201,8 @@ mod tests {
196201
use crate::{close_l2, generate};
197202

198203
use ndarray::{arr1, arr2, Array2};
204+
use rand_xoshiro::Xoshiro256Plus;
205+
use approx::assert_abs_diff_eq;
199206

200207
#[test]
201208
fn test_truncated_svd() {
@@ -227,4 +234,56 @@ mod tests {
227234

228235
close_l2(&a, &reconstructed, 1e-5);
229236
}
237+
238+
/// Eigenvalue structure in high dimensions
239+
///
240+
/// This test checks that the eigenvalues are following the Marchensko-Pastur law. The data is
241+
/// standard uniformly distributed (i.e. E(x) = 0, E^2(x) = 1) and we have twice the amount of
242+
/// data when compared to features. The probability density of the eigenvalues should then follow
243+
/// a special densitiy function, described by the Marchenko-Pastur law.
244+
///
245+
/// See also https://en.wikipedia.org/wiki/Marchenko%E2%80%93Pastur_distribution
246+
#[test]
247+
fn test_marchenko_pastur() {
248+
// create random number generator
249+
let mut rng = SmallRng::seed_from_u64(3);
250+
251+
// generate normal distribution random data with N >> p
252+
let data = Array2::random_using((1000, 500), StandardNormal, &mut rng);
253+
let dataset = Dataset::from(data / 1000f64.sqrt());
254+
255+
let model = Pca::params(500).fit(&dataset);
256+
let sv = model.singular_values().mapv(|x| x * x);
257+
258+
// we have created a random spectrum and can apply the Marchenko-Pastur law
259+
// with variance 1 and p/n = 0.5
260+
let (a, b) = (
261+
1. * (1. - 0.5f64.sqrt()).powf(2.0),
262+
1. * (1. + 0.5f64.sqrt()).powf(2.0),
263+
);
264+
265+
// check that the spectrum has correct boundaries
266+
assert_abs_diff_eq!(b, sv[0], epsilon = 0.1);
267+
assert_abs_diff_eq!(a, sv[sv.len() - 1], epsilon = 0.1);
268+
269+
// estimate density empirical and compare with Marchenko-Pastur law
270+
let mut i = 0;
271+
'outer: for th in Array1::linspace(0.1, 2.8, 28).into_iter().rev() {
272+
let mut count = 0;
273+
while sv[i] >= *th {
274+
count += 1;
275+
i += 1;
276+
277+
if i == sv.len() {
278+
break 'outer;
279+
}
280+
}
281+
282+
let x = th + 0.05;
283+
let mp_law = ((b - x) * (x - a)).sqrt() / std::f64::consts::PI / x;
284+
let empirical = count as f64 / 500. / ((2.8 - 0.1) / 28.);
285+
286+
assert_abs_diff_eq!(mp_law, empirical, epsilon = 0.05);
287+
}
288+
}
230289
}

0 commit comments

Comments
 (0)