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// }