1use crate::matrix::rand_util::{mix_seed, name_seed};
5use crate::matrix::traits::*;
6use nalgebra::{DMatrix, DVector};
7use num_traits::Float;
8use rayon::prelude::*;
9
10const TOP_EIGEN_SEED: u64 = 0x544F_5045_4947_454E; const LANCZOS_STEPS: usize = 40;
16
17const LOWER_MARGIN: f64 = 0.05;
20
21const MIN_ROWS_PER_TASK: usize = 64;
23
24trait EigScalar: nalgebra::RealField + Float + Copy + Send + Sync {}
26impl<T: nalgebra::RealField + Float + Copy + Send + Sync> EigScalar for T {}
27
28fn to_f64<T: EigScalar>(x: T) -> f64 {
29 num_traits::ToPrimitive::to_f64(&x).unwrap_or(f64::NAN)
30}
31
32fn from_f64<T: EigScalar>(x: f64) -> T {
33 <T as num_traits::NumCast>::from(x).expect("f64 fits the scalar")
34}
35
36trait SymOp<T: EigScalar> {
38 fn dim(&self) -> usize;
39 fn apply(&self, x: &DMatrix<T>) -> anyhow::Result<DMatrix<T>>;
41 fn dense(&self) -> anyhow::Result<DMatrix<T>>;
43}
44
45fn sym_times<T: EigScalar>(a: &DMatrix<T>, x: &DMatrix<T>) -> DMatrix<T> {
48 let n = a.ncols();
49 let block = n
50 .div_ceil(rayon::current_num_threads())
51 .max(MIN_ROWS_PER_TASK);
52 let starts: Vec<usize> = (0..n).step_by(block).collect();
53 let parts: Vec<DMatrix<T>> = starts
54 .par_iter()
55 .map(|&j| a.rows(j, block.min(n - j)) * x)
56 .collect();
57 let mut out = DMatrix::zeros(n, x.ncols());
58 for (&j, part) in starts.iter().zip(&parts) {
59 out.rows_mut(j, part.nrows()).copy_from(part);
60 }
61 out
62}
63
64impl<T: EigScalar> SymOp<T> for DMatrix<T> {
65 fn dim(&self) -> usize {
66 self.nrows()
67 }
68 fn apply(&self, x: &DMatrix<T>) -> anyhow::Result<DMatrix<T>> {
69 Ok(sym_times(self, x))
70 }
71 fn dense(&self) -> anyhow::Result<DMatrix<T>> {
72 Ok(self.clone())
73 }
74}
75
76fn sorted_eigen<T: EigScalar>(h: DMatrix<T>) -> (DVector<T>, DMatrix<T>) {
78 let eig = h.symmetric_eigen();
79 let mut order: Vec<usize> = (0..eig.eigenvalues.len()).collect();
80 order.sort_by(|&i, &j| {
81 eig.eigenvalues[j]
82 .partial_cmp(&eig.eigenvalues[i])
83 .unwrap_or(std::cmp::Ordering::Equal)
84 });
85 let values = DVector::from_iterator(order.len(), order.iter().map(|&i| eig.eigenvalues[i]));
86 let vectors = DMatrix::from_columns(
87 &order
88 .iter()
89 .map(|&i| eig.eigenvectors.column(i))
90 .collect::<Vec<_>>(),
91 );
92 (values, vectors)
93}
94
95fn project_out<T: EigScalar>(w: &mut DMatrix<T>, basis: nalgebra::DMatrixView<T>) {
98 for _ in 0..2 {
99 let coef = basis.tr_mul(w);
100 *w -= basis * coef;
101 }
102}
103
104fn lanczos_bounds<T: EigScalar, D: SymOp<T>>(op: &D) -> anyhow::Result<(f64, f64)> {
111 let n = op.dim();
112 let steps = LANCZOS_STEPS.min(n);
113 let mut basis = DMatrix::<T>::zeros(n, steps);
114 let mut alpha = vec![0.0; steps];
115 let mut beta = vec![0.0; steps];
116 let mut v = DMatrix::<T>::rnorm_seeded(n, 1, name_seed(TOP_EIGEN_SEED, "lanczos"));
117 let mut len = 0;
118 let mut scale = 0.0f64;
120 while len < steps {
121 let nrm = v.norm();
122 if to_f64(nrm) <= f64::MIN_POSITIVE {
123 break;
124 }
125 v /= nrm;
126 basis.set_column(len, &v.column(0));
127 let mut w = op.apply(&v)?;
128 alpha[len] = to_f64(v.column(0).dot(&w.column(0)));
129 scale = scale.max(to_f64(w.norm()));
130 anyhow::ensure!(
131 alpha[len].is_finite() && scale.is_finite(),
132 "top_eigen: the matrix has non-finite entries"
133 );
134 project_out(&mut w, basis.columns(0, len + 1));
136 len += 1;
137 let b = to_f64(w.norm());
138 let breakdown = b <= 1e3 * to_f64(T::epsilon()) * scale.max(f64::MIN_POSITIVE);
139 beta[len - 1] = if breakdown { 0.0 } else { b };
140 if breakdown && len < steps {
141 let seed = mix_seed(name_seed(TOP_EIGEN_SEED, "lanczos"), len as u64);
143 w = DMatrix::<T>::rnorm_seeded(n, 1, seed);
144 project_out(&mut w, basis.columns(0, len));
145 }
146 v = w;
147 }
148 anyhow::ensure!(len > 0, "Lanczos found no direction");
149 let mut t = DMatrix::<f64>::zeros(len, len);
150 for i in 0..len {
151 t[(i, i)] = alpha[i];
152 if i + 1 < len {
153 t[(i, i + 1)] = beta[i];
154 t[(i + 1, i)] = beta[i];
155 }
156 }
157 let (theta, s) = sorted_eigen(t);
158 let resid = |i: usize| (beta[len - 1] * s[(len - 1, i)]).abs();
159 let hi = theta[0] + resid(0);
160 let lo = theta[len - 1] - resid(len - 1);
161 Ok((lo, hi))
162}
163
164struct Ritz<T: nalgebra::Scalar> {
167 v: DMatrix<T>,
168 av: DMatrix<T>,
169 theta: DVector<T>,
170}
171
172fn rayleigh_ritz<T: EigScalar, D: SymOp<T>>(op: &D, x: DMatrix<T>) -> anyhow::Result<Ritz<T>> {
174 let q = x.qr().q();
175 let aq = op.apply(&q)?;
176 let h = q.transpose() * &aq;
177 let h = (&h + h.transpose()) * from_f64::<T>(0.5);
178 let (theta, s) = sorted_eigen(h);
179 Ok(Ritz {
180 v: &q * &s,
181 av: &aq * &s,
182 theta,
183 })
184}
185
186fn chebyshev_filter<T: EigScalar, D: SymOp<T>>(
191 op: &D,
192 ritz: &Ritz<T>,
193 m: usize,
194 c: f64,
195 e: f64,
196 tau: f64,
197) -> anyhow::Result<DMatrix<T>> {
198 let mut sigma = 1.0 / tau;
199 let mut prev = ritz.v.clone();
200 let mut cur = (&ritz.av - &ritz.v * from_f64::<T>(c)) * from_f64::<T>(sigma / e);
201 for _ in 1..m {
202 let next_sigma = 1.0 / (2.0 * tau - sigma);
203 let mut next = op.apply(&cur)?;
204 next -= &cur * from_f64::<T>(c);
205 next *= from_f64::<T>(2.0 * next_sigma / e);
206 next -= prev * from_f64::<T>(sigma * next_sigma);
207 prev = cur;
208 cur = next;
209 sigma = next_sigma;
210 }
211 Ok(cur)
212}
213
214fn fix_signs<T: EigScalar>(v: &mut DMatrix<T>) {
217 for mut col in v.column_iter_mut() {
218 if col.nrows() > 0 && col[col.iamax()] < T::zero() {
219 col.neg_mut();
220 }
221 }
222}
223
224fn leading<T: EigScalar>(
226 values: &DVector<T>,
227 vectors: &DMatrix<T>,
228 rank: usize,
229 iters: usize,
230 converged: bool,
231) -> TopEigen<DMatrix<T>, DVector<T>> {
232 let mut vectors = vectors.columns(0, rank).into_owned();
233 fix_signs(&mut vectors);
234 TopEigen {
235 values: values.rows(0, rank).into_owned(),
236 vectors,
237 iters,
238 converged,
239 }
240}
241
242fn top_eigen_op<T: EigScalar, D: SymOp<T>>(
244 op: &D,
245 rank: usize,
246 args: &TopEigenArgs,
247 warm_start: Option<&DMatrix<T>>,
248) -> anyhow::Result<TopEigen<DMatrix<T>, DVector<T>>> {
249 let n = op.dim();
250 let rank = rank.min(n);
251 if rank == 0 {
252 return Ok(leading(
253 &DVector::zeros(0),
254 &DMatrix::zeros(n, 0),
255 0,
256 0,
257 true,
258 ));
259 }
260 let k = rank.saturating_add(args.oversample).min(n);
261
262 if 2 * k >= n {
265 let (values, vectors) = sorted_eigen(op.dense()?);
266 return Ok(leading(&values, &vectors, rank, 0, true));
267 }
268
269 let (mut lo, hi) = lanczos_bounds(op)?;
270 let norm = lo.abs().max(hi.abs()).max(f64::MIN_POSITIVE);
271 let growth_cap = (2.0 / to_f64(T::epsilon()).sqrt()).ln();
274
275 let mut x = DMatrix::<T>::rnorm_seeded(n, k, TOP_EIGEN_SEED);
276 if let Some(w) = warm_start {
277 anyhow::ensure!(
278 w.nrows() == n,
279 "warm start has {} rows, the matrix {n}",
280 w.nrows()
281 );
282 let wc = w.ncols().min(k);
283 x.columns_mut(0, wc).copy_from(&w.columns(0, wc));
284 }
285 let mut ritz = rayleigh_ritz(op, x)?;
286 let mut iters = 0;
287 let converged = loop {
288 let worst = (0..rank)
289 .map(|i| to_f64((ritz.av.column(i) - ritz.v.column(i) * ritz.theta[i]).norm()))
290 .fold(0.0f64, f64::max);
291 if worst <= args.tol * norm {
292 break true;
293 }
294 if iters >= args.max_iters {
295 break false;
296 }
297
298 let (t_hi, t_lo) = (to_f64(ritz.theta[0]), to_f64(ritz.theta[k - 1]));
304 lo = lo.min(t_lo);
305 let a = lo - LOWER_MARGIN * (hi - lo).max(norm * f64::EPSILON);
306 let b = t_lo.max(a + 1e-3 * (hi.max(t_hi) - a));
307 let top = hi.max(t_hi).max(b + (b - a));
308 let (c, e) = ((b + a) / 2.0, (b - a) / 2.0);
309 let tau = (top - c) / e;
310 let lead = ((t_hi - c) / e).max(1.0 + 1e-12);
311 let degree =
312 ((growth_cap / lead.acosh()).floor() as usize).clamp(1, args.max_degree.max(1));
313
314 let x = chebyshev_filter(op, &ritz, degree, c, e, tau)?;
315 ritz = rayleigh_ritz(op, x)?;
316 iters += 1;
317 };
318
319 if !converged {
320 log::warn!(
321 "top_eigen: {rank} pairs not converged to tol {} in {iters} iterations",
322 args.tol
323 );
324 }
325 Ok(leading(&ritz.theta, &ritz.v, rank, iters, converged))
326}
327
328impl<T> SymmetricTopEigen for DMatrix<T>
331where
332 T: nalgebra::RealField + Float + Copy + Send + Sync,
333{
334 type Mat = DMatrix<T>;
335 type DVec = DVector<T>;
336
337 fn top_eigen_with(
338 &self,
339 rank: usize,
340 args: &TopEigenArgs,
341 warm_start: Option<&Self::Mat>,
342 ) -> anyhow::Result<TopEigen<Self::Mat, Self::DVec>> {
343 anyhow::ensure!(
344 self.is_square(),
345 "top_eigen needs a square matrix, got {} x {}",
346 self.nrows(),
347 self.ncols()
348 );
349 top_eigen_op(self, rank, args, warm_start)
350 }
351}
352
353#[cfg(feature = "tensor")]
354mod tensor {
355 use super::*;
356 use candle_core::{DType, Tensor};
357
358 struct TensorOp<'a>(&'a Tensor);
361
362 impl<T> SymOp<T> for TensorOp<'_>
363 where
364 T: EigScalar + candle_core::WithDType,
365 {
366 fn dim(&self) -> usize {
367 self.0.dims()[0]
368 }
369 fn apply(&self, x: &DMatrix<T>) -> anyhow::Result<DMatrix<T>> {
373 let (n, k) = x.shape();
374 let xt = Tensor::from_slice(x.as_slice(), (k, n), self.0.device())?;
375 let axt = xt.matmul(self.0)?;
376 Ok(DMatrix::from_vec(n, k, axt.flatten_all()?.to_vec1()?))
377 }
378 fn dense(&self) -> anyhow::Result<DMatrix<T>> {
379 DMatrix::<T>::from_tensor(self.0)
380 }
381 }
382
383 fn run<T>(
384 a: &Tensor,
385 rank: usize,
386 args: &TopEigenArgs,
387 warm_start: Option<&Tensor>,
388 ) -> anyhow::Result<TopEigen<Tensor, Tensor>>
389 where
390 T: EigScalar + candle_core::WithDType,
391 {
392 let warm = warm_start
393 .map(|w| DMatrix::<T>::from_tensor(&w.to_dtype(a.dtype())?))
394 .transpose()?;
395 let out = top_eigen_op::<T, _>(&TensorOp(a), rank, args, warm.as_ref())?;
396 Ok(TopEigen {
397 values: Tensor::from_slice(out.values.as_slice(), out.values.len(), a.device())?,
398 vectors: out.vectors.to_tensor(a.device())?.contiguous()?,
399 iters: out.iters,
400 converged: out.converged,
401 })
402 }
403
404 impl SymmetricTopEigen for Tensor {
405 type Mat = Tensor;
406 type DVec = Tensor;
407
408 fn top_eigen_with(
411 &self,
412 rank: usize,
413 args: &TopEigenArgs,
414 warm_start: Option<&Self::Mat>,
415 ) -> anyhow::Result<TopEigen<Self::Mat, Self::DVec>> {
416 let dims = self.dims();
417 anyhow::ensure!(
418 dims.len() == 2 && dims[0] == dims[1],
419 "top_eigen needs a square 2-D tensor, got {dims:?}"
420 );
421 match self.dtype() {
422 DType::F32 => run::<f32>(self, rank, args, warm_start),
423 DType::F64 => run::<f64>(self, rank, args, warm_start),
424 dt => anyhow::bail!("top_eigen supports f32 and f64 tensors, got {dt:?}"),
425 }
426 }
427 }
428}
429
430#[cfg(feature = "ndarray")]
431mod ndarray_impl {
432 use super::*;
433 use ndarray::{Array1, Array2, ShapeBuilder};
434
435 fn to_dmatrix<T: EigScalar>(a: &Array2<T>) -> DMatrix<T> {
436 let (n, m) = a.dim();
437 DMatrix::from_fn(n, m, |i, j| a[(i, j)])
438 }
439
440 fn to_array<T: EigScalar>(m: &DMatrix<T>) -> Array2<T> {
441 Array2::from_shape_vec(m.shape().f(), m.as_slice().to_vec())
443 .expect("shape matches the buffer")
444 }
445
446 impl<T> SymmetricTopEigen for Array2<T>
452 where
453 T: nalgebra::RealField + Float + Copy + Send + Sync,
454 {
455 type Mat = Array2<T>;
456 type DVec = Array1<T>;
457
458 fn top_eigen_with(
459 &self,
460 rank: usize,
461 args: &TopEigenArgs,
462 warm_start: Option<&Self::Mat>,
463 ) -> anyhow::Result<TopEigen<Self::Mat, Self::DVec>> {
464 let warm = warm_start.map(to_dmatrix);
465 let out = to_dmatrix(self).top_eigen_with(rank, args, warm.as_ref())?;
466 Ok(TopEigen {
467 values: Array1::from_vec(out.values.as_slice().to_vec()),
468 vectors: to_array(&out.vectors),
469 iters: out.iters,
470 converged: out.converged,
471 })
472 }
473 }
474}
475
476#[cfg(test)]
477#[path = "dmatrix_top_eigen_tests.rs"]
478mod tests;