rusty_compression/
random_sampling.rs

1//! Random sampling of matrices
2//!
3//! This module defines traits for the randomized sampling of the range of a linear operator
4//! and the associated computation of randomized QR and Singular Value Decompositions.
5//!
6//! In many applications we want to complete an approximate low-rank approximation of a linear
7//! operator but do not have access to the whole matrix, or computing the full QR or SVD of a
8//! matrix is too expensive. In these cases techniques from randomized linear algebra can be used
9//! to compute approximate low-rank decompositions.
10//! 
11//! Assume we have an operator $A$. We have two conditions on $A$.
12//! 
13//! 1. We need to be able to compute $y=Ax$ for a given vector $x$.
14//! 2. We need to be able to compute $y=A^Hx$ for a given vector $x$.
15//! 
16//! These abilities are implemented through the traits [MatVec](crate::types::MatVec) and [ConjMatVec](crate::types::ConjMatVec).
17//! Implementing these traits also automatically implements the corresponding traits
18//! [MatMat](crate::types::MatMat) and [ConjMatMat](crate::types::ConjMatMat), which
19//! are the corresponding versions for multiplications with a matrix $X$ instead of a vector $x$.
20//! The routines in this module use the latter traits. For performance reasons it may sometimes be
21//! preferable to directly implement [MatMat](crate::types::MatMat) and [ConjMatMat](crate::types::ConjMatMat)
22//! instead of relying on the corresponding vector versions.
23//! 
24//! In the following we describe the traits in more detail.
25//! 
26//! - [`SampleRange`]: This trait can be used to randomly sample the range of an operator by specifying a target
27//! rank. It only requires the [MatMat](crate::types::MatMat) trait.
28//! - [`SampleRangePowerIteration`]: This trait is an improved version of [`SampleRange']. It uses a power
29//!   iteration to give a more precise result but requires the [ConjMatMat](crate::types::ConjMatMat) trait
30//!   to be implemented.
31//! - [`AdaptiveSampling`]: This trait samples the range of an operator adaptively through specifying
32//!   a target tolerance. The error tolerance is checked probabilistically.
33//! 
34//! Once we have sampled the range of an operator we can use the method 
35//! [compute_from_range_estimate](crate::qr::QRTraits::compute_from_range_estimate) of the
36//! [QRTraits](crate::qr::QRTraits) to obtain an approximate QR Decomposition of the operator, from
37//! which we can for example compute an interpolative decomposition. Alternatively, we can use
38//! the [corresponding method](crate::svd::SVDTraits::compute_from_range_estimate) to compute an
39//! approximate Singular Value Decomposition of the operator.
40
41
42use crate::qr::{QRTraits, QR};
43use crate::random_matrix::RandomMatrix;
44use crate::CompressionType;
45use ndarray::{concatenate, Array2, ArrayBase, Axis, Data, Ix2};
46use ndarray_linalg::Norm;
47use num::ToPrimitive;
48use rand::Rng;
49use crate::types::{c32, c64, Result, Scalar, MatMat, ConjMatMat};
50
51
52/// Randomly sample the range of an operator
53///
54/// Let $A\in\mathbb{C}{m\times n}$ be a matrix. To sample the range of rank $k$ one can multiply
55/// $A$ by a Gaussian random matrix $\Omega$ of dimension $n\times k + p$, where $p$ is a small oversampling
56/// parameter. The result of this product is post-processed by a pivoted QR decomposition and the first $k$
57/// columns of the $Q$ matrix in the pivoted QR decomposition returned.
58pub trait SampleRange<A: Scalar> {
59    /// Randomly sample the range of an operator.
60    /// Return an approximate orthogonal basis of the dominant range.
61    /// # Arguments
62    /// * `k`: The target rank of the basis for the range.
63    /// * `p`: Oversampling parameter. `p` should be chosen small. A typical size
64    ///      is p=5.
65    /// * `rng`: The random number generator.
66    fn sample_range_by_rank<R: Rng>(
67        &self,
68        k: usize,
69        p: usize,
70        rng: &mut R,
71    ) -> Result<Array2<A>>;
72}
73
74/// Randomly sample the range of an operator through a power iteration
75///
76/// Let $A\in\mathbb{C}{m\times n}$ be a matrix. To sample the range of rank $k$ one can multiply
77/// $A$ by a Gaussian random matrix $\Omega$ of dimension $n\times k + p$, where $p$ is a small oversampling
78/// parameter. To improve the accuracy of the range computation this is then `it_count` times multiplied with the
79/// operator $AA^H$. Each intermediate result is QR orthogonalized to stabilise this power iteration.
80/// The result of the power iteration is post-processed by a pivoted QR decomposition and the first $k$
81/// columns of the $Q$ matrix in the pivoted QR decomposition returned.
82pub trait SampleRangePowerIteration<A: Scalar> {
83    /// Randomly sample the range of an operator refined through a power iteration
84    /// Return an approximate orthogonal basis of the dominant range.
85    /// # Arguments
86    /// * `k`: The target rank of the basis for the range.
87    /// * `p`: Oversampling parameter. `p` should be chosen small. A typical size
88    ///      is p=5.
89    /// * `it_count`: The number of steps in the power iteration. For `it_count = 0` the
90    ///               routine is identical to `sample_range_by_rank`.
91    fn sample_range_power_iteration<R: Rng>(
92        &self,
93        k: usize,
94        p: usize,
95        it_count: usize,
96        rng: &mut R,
97    ) -> Result<Array2<A>>;
98}
99
100macro_rules! sample_range_impl {
101    ($scalar:ty) => {
102        impl<Op: MatMat<A = $scalar>> SampleRange<$scalar> for Op {
103            fn sample_range_by_rank<R: Rng>(
104                &self,
105                k: usize,
106                p: usize,
107                rng: &mut R,
108            ) -> Result<Array2<$scalar>> {
109                let m = self.ncols();
110
111                let omega = <$scalar>::random_gaussian((m, k + p), rng);
112                let basis = self.matmat(omega.view());
113
114                let qr = QR::<$scalar>::compute_from(basis.view())?
115                    .compress(CompressionType::RANK(k))?;
116
117                Ok(qr.get_q().to_owned())
118            }
119        }
120    };
121}
122
123sample_range_impl!(f32);
124sample_range_impl!(f64);
125sample_range_impl!(c32);
126sample_range_impl!(c64);
127
128macro_rules! sample_range_power_impl {
129    ($scalar:ty) => {
130        impl<Op: ConjMatMat<A = $scalar>> SampleRangePowerIteration<$scalar> for Op {
131            fn sample_range_power_iteration<R: Rng>(
132                &self,
133                k: usize,
134                p: usize,
135                it_count: usize,
136                rng: &mut R,
137            ) -> Result<Array2<$scalar>> {
138                let m = self.ncols();
139
140                let omega = <$scalar>::random_gaussian((m, k + p), rng);
141                let op_omega = self.matmat(omega.view());
142                let mut res = op_omega.clone();
143
144                for index in 0..it_count {
145                    let qr = QR::<$scalar>::compute_from(op_omega.view())?;
146                    let q = qr.get_q();
147
148                    let qr = QR::<$scalar>::compute_from(self.conj_matmat(q).view())?;
149                    let w = qr.get_q();
150                    let op_omega = self.matmat(w);
151                    if index == it_count - 1 {
152                        res.assign(&op_omega);
153                    }
154                }
155
156                let compressed =
157                    QR::<$scalar>::compute_from(res.view())?.compress(CompressionType::RANK(k))?;
158
159                Ok(compressed.get_q().to_owned())
160            }
161        }
162    };
163}
164
165sample_range_power_impl!(f32);
166sample_range_power_impl!(f64);
167sample_range_power_impl!(c32);
168sample_range_power_impl!(c64);
169
170/// Trait defining the maximum column norm of an operator
171///
172/// If $A\in\mathbb{C}^{m\times n}$ the maximum column-norm is
173/// computed by first taking the Euclidian norm of each column and then
174/// returning the maximum of the column norms.
175pub trait MaxColNorm<A: Scalar> {
176    // For a given matrix return the maximum column norm.
177    fn max_col_norm(&self) -> A::Real;
178}
179
180macro_rules! impl_max_col_norm {
181    ($scalar:ty) => {
182        impl<S: Data<Elem = $scalar>> MaxColNorm<$scalar> for ArrayBase<S, Ix2> {
183            // For a given matrix return the maximum column norm.
184            fn max_col_norm(&self) -> <$scalar as Scalar>::Real {
185                let mut max_val = num::zero::<<$scalar as Scalar>::Real>();
186
187                for col in self.axis_iter(Axis(1)) {
188                    max_val = num::Float::max(max_val, col.norm_l2());
189                }
190                max_val
191            }
192        }
193    };
194}
195
196impl_max_col_norm!(f32);
197impl_max_col_norm!(f64);
198impl_max_col_norm!(c32);
199impl_max_col_norm!(c64);
200
201/// This trait defines the adaptive sampling of the range of an operator.
202pub trait AdaptiveSampling<A: Scalar> {
203    /// Adaptively randomly sample the range of an operator up to a given tolerance.
204    /// # Arguments
205    /// * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
206    /// * `sample_size`: Number of samples drawn together in each iteration.
207    /// * `rng`: The random number generator.
208    ///
209    /// Returns a tuple (q, residuals), where `q` is an ndarray containing the orthogonalized columns of
210    /// the range, and `residuals` is a vector of tuples of the form `(rank, rel_res)`, where `rel_res`
211    /// is the estimated relative residual for the first `rank` columns of `q`.
212    fn sample_range_adaptive<R: Rng>(
213        &self,
214        rel_tol: f64,
215        sample_size: usize,
216        rng: &mut R,
217    ) -> Result<(Array2<A>, Vec<(usize, f64)>)>;
218}
219
220macro_rules! adaptive_sampling_impl {
221    ($scalar:ty) => {
222        impl<Op: ConjMatMat<A = $scalar>> AdaptiveSampling<$scalar> for Op {
223            fn sample_range_adaptive<R: Rng>(
224                &self,
225                rel_tol: f64,
226                sample_size: usize,
227                rng: &mut R,
228            ) -> Result<(Array2<$scalar>, Vec<(usize, f64)>)> {
229                // This is a sampling factor. See Section 4.3 in Halko, Martinsson, Tropp,
230                // Finding Structure with Randomness, SIAM Review.
231                let tol_factor = num::cast::<f64, <$scalar as Scalar>::Real>(
232                    10.0 * <f64 as num::traits::FloatConst>::FRAC_2_PI().sqrt(),
233                )
234                .unwrap();
235
236                let m = self.ncols();
237                let rel_tol = num::cast::<f64, <$scalar as Scalar>::Real>(rel_tol).unwrap();
238                let omega = <$scalar>::random_gaussian((m, sample_size), rng);
239                let mut op_omega = self.matmat(omega.view());
240                // Randomized estimate of the original operator norm.
241                let operator_norm = op_omega.max_col_norm() * tol_factor;
242                let mut max_norm = operator_norm;
243                let mut q = Array2::<$scalar>::zeros((self.nrows(), 0));
244                let mut b = Array2::<$scalar>::zeros((0, self.ncols()));
245
246                let mut residuals = Vec::<(usize, f64)>::new();
247
248                while max_norm / operator_norm >= rel_tol {
249                    // Orthogonalize against existing basis
250                    if q.ncols() > 0 {
251                        op_omega -= &q.dot(&q.t().map(|item| item.conj()).dot(&op_omega));
252                    }
253                    // Now do the QR of the vectors
254                    let qr = QR::<$scalar>::compute_from(op_omega.view())?;
255                    // Extend the b matrix
256                    b = concatenate![
257                        Axis(0),
258                        b,
259                        self.conj_matmat(qr.get_q()).t().map(|item| item.conj())
260                    ];
261                    // Extend the Q matrix
262                    q = concatenate![Axis(1), q, qr.get_q()];
263
264                    // Now compute new vectors
265                    let omega = <$scalar>::random_gaussian((m, sample_size), rng);
266                    op_omega = self.matmat(omega.view()) - q.dot(&b.dot(&omega));
267
268                    // Update the error tolerance
269                    max_norm = op_omega.max_col_norm() * tol_factor;
270                    residuals.push((q.ncols(), (max_norm / operator_norm).to_f64().unwrap()));
271                }
272
273                Ok((q, residuals))
274            }
275        }
276    };
277}
278
279adaptive_sampling_impl!(f32);
280adaptive_sampling_impl!(c64);
281adaptive_sampling_impl!(f64);
282adaptive_sampling_impl!(c32);
283
284// fn qr_from_range_estimate<R: Rng>(
285//     &self,
286//     rel_tol: f64,
287//     sample_size: usize,
288//     rng: &mut R,
289// ) -> Result<QR<$scalar>> {
290//     let (q, _) = self.sample_range_adaptive(rel_tol, sample_size, rng)?;
291
292//     let b = self.conj_matmat(q.view()).t().map(|item| item.conj());
293//     let qr = QR::<$scalar>::compute_from(b.view())?;
294
295//     Ok(QR {
296//         q: b.dot(&qr.get_q()),
297//         r: qr.get_r().into_owned(),
298//         ind: qr.get_ind().into_owned(),
299//     })
300// }
301
302//     // Compute a QR decomposition via adaptive random range sampling.
303//     // # Arguments
304//     // * `op`: The operator for which to compute the QR decomposition.
305//     // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
306//     // * `sample_size` Number of samples drawn together in each iteration.
307//     // * `rng`: The random number generator.
308//     fn randomized_adaptive_qr<R: Rng>(
309//         &self,
310//         rel_tol: f64,
311//         sample_size: usize,
312//         rng: &mut R,
313//     ) -> Result<QR<A>>;
314
315//     // Compute a SVD decomposition via adaptive random range sampling.
316//     // # Arguments
317//     // * `op`: The operator for which to compute the QR decomposition.
318//     // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
319//     // * `sample_size` Number of samples drawn together in each iteration.
320//     // * `rng`: The random number generator.
321//     fn randomized_adaptive_svd<R: Rng>(
322//         &self,
323//         rel_tol: f64,
324//         sample_size: usize,
325//         rng: &mut R,
326//     ) -> Result<SVD<A>>;
327// }
328
329//             // Compute a SVD decomposition via adaptive random range sampling.
330//             // # Arguments
331//             // * `op`: The operator for which to compute the QR decomposition.
332//             // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
333//             // * `sample_size` Number of samples drawn together in each iteration.
334//             // * `rng`: The random number generator.
335//             fn randomized_adaptive_svd<R: Rng>(
336//                 &self,
337//                 rel_tol: f64,
338//                 sample_size: usize,
339//                 rng: &mut R,
340//             ) -> Result<SVD<$scalar>> {
341//                 let (q, _) = self.sample_range_adaptive(rel_tol, sample_size, rng)?;
342
343//                 let b = self.conj_matmat(q.view()).t().map(|item| item.conj());
344//                 let svd = SVD::<$scalar>::compute_from(b.view())?;
345
346//                 Ok(SVD {
347//                     u: b.dot(&svd.u),
348//                     s: svd.get_s().into_owned(),
349//                     vt: svd.get_vt().into_owned(),
350//                 })
351//             }
352//         }
353
354// // Adaptively randomly sample the range of an operator up to a given tolerance.
355// // # Arguments
356// // * `op`: The operator for which to sample the range.
357// // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
358// // * `sample_size`: Number of samples drawn together in each iteration.
359// // * `rng`: The random number generator.
360// //
361// // Returns a tuple (q, residuals), where `q` is an ndarray containing the orthogonalized columns of
362// // the range, and `residuals` is a vector of tuples of the form `(rank, rel_res)`, where `rel_res`
363// // is the estimated relative residual for the first `rank` columns of `q`.
364// pub fn sample_range_adaptive<Op: ConjRowMatMat, R: Rng>(
365//     op: &Op,
366//     rel_tol: f64,
367//     sample_size: usize,
368//     rng: &mut R,
369// ) -> Result<(Array2<Op::A>, Vec<(usize, f64)>)> {
370//     // This is a sampling factor. See Section 4.3 in Halko, Martinsson, Tropp,
371//     // Finding Structure with Randomness, SIAM Review.
372//     let tol_factor = num::cast::<f64, <Op::A as Scalar>::Real>(
373//         10.0 * <f64 as num::traits::FloatConst>::FRAC_2_PI().sqrt(),
374//     )
375//     .unwrap();
376
377//     let m = op.ncols();
378//     let rel_tol = num::cast::<f64, <Op::A as Scalar>::Real>(rel_tol).unwrap();
379//     let omega = Op::A::random_gaussian((m, sample_size), rng);
380//     let mut op_omega = op.matmat(omega.view());
381//     // Randomized estimate of the original operator norm.
382//     let operator_norm = max_col_norm(&op_omega) * tol_factor;
383//     let mut max_norm = operator_norm;
384//     let mut q = Array2::<Op::A>::zeros((op.nrows(), 0));
385//     let mut b = Array2::<Op::A>::zeros((0, op.ncols()));
386
387//     let mut residuals = Vec::<(usize, f64)>::new();
388
389//     while max_norm / operator_norm >= rel_tol {
390//         // Orthogonalize against existing basis
391//         if q.ncols() > 0 {
392//             op_omega -= &q.dot(&q.t().map(|item| item.conj()).dot(&op_omega));
393//         }
394//         // Now do the QR of the vectors
395//         let (qtemp, _) = op_omega.qr().unwrap();
396//         // Extend the b matrix
397//         b = concatenate![Axis(0), b, op.conj_row_matmat(qtemp.view())];
398//         // Extend the Q matrix
399//         q = concatenate![Axis(1), q, qtemp];
400
401//         // Now compute new vectors
402//         let omega = Op::A::random_gaussian((m, sample_size), rng);
403//         op_omega = op.matmat(omega.view()) - q.dot(&b.dot(&omega));
404
405//         // Update the error tolerance
406//         max_norm = max_col_norm(&op_omega) * tol_factor;
407//         residuals.push((q.ncols(), (max_norm / operator_norm).to_f64().unwrap()));
408//     }
409
410//     Ok((q, residuals))
411// }
412
413// // Compute a QR decomposition via adaptive random range sampling.
414// // # Arguments
415// // * `op`: The operator for which to compute the QR decomposition.
416// // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
417// // * `sample_size` Number of samples drawn together in each iteration.
418// // * `rng`: The random number generator.
419// pub fn randomized_adaptive_qr<Op: ConjRowMatMat, R: Rng>(
420//     op: &Op,
421//     rel_tol: f64,
422//     sample_size: usize,
423//     rng: &mut R,
424// ) -> Result<QRContainer<Op::A>> {
425//     let (q, _) = sample_range_adaptive(op, rel_tol, sample_size, rng)?;
426
427//     let b = op.conj_row_matmat(q.view());
428//     let mut qr = b.pivoted_qr()?;
429
430//     qr.q = b.dot(&qr.q);
431
432//     Ok(qr)
433// }
434
435// // Compute a SVD decomposition via adaptive random range sampling.
436// // # Arguments
437// // * `op`: The operator for which to compute the QR decomposition.
438// // * `rel_tol`: The relative error tolerance. The error is checked probabilistically.
439// // * `sample_size` Number of samples drawn together in each iteration.
440// // * `rng`: The random number generator.
441// pub fn randomized_adaptive_svd<Op: ConjRowMatMat, R: Rng>(
442//     op: &Op,
443//     rel_tol: f64,
444//     sample_size: usize,
445//     rng: &mut R,
446// ) -> Result<SVDContainer<Op::A>> {
447//     let (q, _) = sample_range_adaptive(op, rel_tol, sample_size, rng)?;
448
449//     let b = op.conj_row_matmat(q.view());
450//     let mut svd = b.compute_svd()?;
451
452//     svd.u = b.dot(&svd.u);
453
454//     Ok(svd)
455// }