1use super::brownian::Rng;
5use super::monte_carlo::Histogram;
6use glam::Vec2;
7
8pub struct RandomMatrix {
14 pub rows: usize,
15 pub cols: usize,
16 pub entries: Vec<f64>,
17}
18
19impl RandomMatrix {
20 pub fn zeros(rows: usize, cols: usize) -> Self {
22 Self {
23 rows,
24 cols,
25 entries: vec![0.0; rows * cols],
26 }
27 }
28
29 pub fn get(&self, i: usize, j: usize) -> f64 {
31 self.entries[i * self.cols + j]
32 }
33
34 pub fn set(&mut self, i: usize, j: usize, val: f64) {
36 self.entries[i * self.cols + j] = val;
37 }
38
39 pub fn identity(n: usize) -> Self {
41 let mut m = Self::zeros(n, n);
42 for i in 0..n {
43 m.set(i, i, 1.0);
44 }
45 m
46 }
47
48 pub fn mul(&self, other: &RandomMatrix) -> RandomMatrix {
50 assert_eq!(self.cols, other.rows);
51 let mut result = RandomMatrix::zeros(self.rows, other.cols);
52 for i in 0..self.rows {
53 for j in 0..other.cols {
54 let mut sum = 0.0;
55 for k in 0..self.cols {
56 sum += self.get(i, k) * other.get(k, j);
57 }
58 result.set(i, j, sum);
59 }
60 }
61 result
62 }
63
64 pub fn transpose(&self) -> RandomMatrix {
66 let mut result = RandomMatrix::zeros(self.cols, self.rows);
67 for i in 0..self.rows {
68 for j in 0..self.cols {
69 result.set(j, i, self.get(i, j));
70 }
71 }
72 result
73 }
74
75 pub fn frobenius_norm(&self) -> f64 {
77 self.entries.iter().map(|x| x * x).sum::<f64>().sqrt()
78 }
79
80 pub fn trace(&self) -> f64 {
82 let n = self.rows.min(self.cols);
83 (0..n).map(|i| self.get(i, i)).sum()
84 }
85}
86
87pub fn goe(n: usize, rng: &mut Rng) -> RandomMatrix {
94 let mut m = RandomMatrix::zeros(n, n);
95 let scale = 1.0 / (2.0 * n as f64).sqrt();
96 for i in 0..n {
97 m.set(i, i, rng.normal() * (2.0_f64).sqrt() * scale);
98 for j in (i + 1)..n {
99 let val = rng.normal() * scale;
100 m.set(i, j, val);
101 m.set(j, i, val);
102 }
103 }
104 m
105}
106
107pub fn gue(n: usize, rng: &mut Rng) -> RandomMatrix {
111 let mut m = RandomMatrix::zeros(n, n);
112 let scale = 1.0 / (n as f64).sqrt();
113 for i in 0..n {
114 m.set(i, i, rng.normal() * scale);
115 for j in (i + 1)..n {
116 let val = rng.normal() * scale / (2.0_f64).sqrt();
117 m.set(i, j, val);
118 m.set(j, i, val);
119 }
120 }
121 m
122}
123
124pub fn wishart(n: usize, p: usize, rng: &mut Rng) -> RandomMatrix {
126 let mut x = RandomMatrix::zeros(p, n);
127 for i in 0..p {
128 for j in 0..n {
129 x.set(i, j, rng.normal());
130 }
131 }
132 let xt = x.transpose();
133 let mut w = xt.mul(&x);
134 for v in w.entries.iter_mut() {
136 *v /= n as f64;
137 }
138 w
139}
140
141pub fn random_correlation(n: usize, rng: &mut Rng) -> RandomMatrix {
143 let samples = 5 * n;
145 let mut data = RandomMatrix::zeros(n, samples);
146 for i in 0..n {
147 for j in 0..samples {
148 data.set(i, j, rng.normal());
149 }
150 }
151 let mut corr = RandomMatrix::zeros(n, n);
153 for i in 0..n {
154 let mean_i: f64 = (0..samples).map(|j| data.get(i, j)).sum::<f64>() / samples as f64;
155 let std_i: f64 = ((0..samples).map(|j| (data.get(i, j) - mean_i).powi(2)).sum::<f64>() / samples as f64).sqrt();
156 for k in i..n {
157 let mean_k: f64 = (0..samples).map(|j| data.get(k, j)).sum::<f64>() / samples as f64;
158 let std_k: f64 = ((0..samples).map(|j| (data.get(k, j) - mean_k).powi(2)).sum::<f64>() / samples as f64).sqrt();
159 let cov: f64 = (0..samples)
160 .map(|j| (data.get(i, j) - mean_i) * (data.get(k, j) - mean_k))
161 .sum::<f64>() / samples as f64;
162 let r = if std_i > 1e-10 && std_k > 1e-10 { cov / (std_i * std_k) } else { 0.0 };
163 corr.set(i, k, r);
164 corr.set(k, i, r);
165 }
166 }
167 corr
168}
169
170pub fn eigenvalues(matrix: &RandomMatrix) -> Vec<f64> {
177 assert_eq!(matrix.rows, matrix.cols, "matrix must be square");
178 let n = matrix.rows;
179 if n == 0 {
180 return Vec::new();
181 }
182 if n == 1 {
183 return vec![matrix.get(0, 0)];
184 }
185
186 let (diag, off_diag) = tridiagonalize(matrix);
188
189 qr_tridiagonal(diag, off_diag)
191}
192
193fn tridiagonalize(m: &RandomMatrix) -> (Vec<f64>, Vec<f64>) {
196 let n = m.rows;
197 let mut a = m.entries.clone();
198 let cols = n;
199
200 let get = |a: &[f64], i: usize, j: usize| a[i * cols + j];
201 let set = |a: &mut [f64], i: usize, j: usize, v: f64| a[i * cols + j] = v;
202
203 for k in 0..n.saturating_sub(2) {
204 let mut x: Vec<f64> = (k + 1..n).map(|i| get(&a, i, k)).collect();
206 let x_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
207 if x_norm < 1e-15 {
208 continue;
209 }
210 let sign = if x[0] >= 0.0 { 1.0 } else { -1.0 };
211 x[0] += sign * x_norm;
212 let x_new_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
213 if x_new_norm < 1e-15 {
214 continue;
215 }
216 for v in x.iter_mut() {
217 *v /= x_new_norm;
218 }
219
220 let m_size = n - k - 1;
222
223 let mut p = vec![0.0; m_size];
225 for i in 0..m_size {
226 for j in 0..m_size {
227 p[i] += get(&a, i + k + 1, j + k + 1) * x[j];
228 }
229 }
230
231 let vtp: f64 = x.iter().zip(p.iter()).map(|(a, b)| a * b).sum();
233 let mut q = vec![0.0; m_size];
234 for i in 0..m_size {
235 q[i] = 2.0 * p[i] - 2.0 * vtp * x[i];
236 }
237
238 for i in 0..m_size {
240 for j in 0..m_size {
241 let val = get(&a, i + k + 1, j + k + 1) - 2.0 * (x[i] * q[j] + q[i] * x[j]) / 2.0;
242 let new_val = get(&a, i + k + 1, j + k + 1)
245 - 2.0 * x[i] * p[j]
246 - 2.0 * p[i] * x[j]
247 + 4.0 * vtp * x[i] * x[j];
248 set(&mut a, i + k + 1, j + k + 1, new_val);
249 }
250 }
251
252 let mut border: Vec<f64> = (0..m_size).map(|i| get(&a, k, i + k + 1)).collect();
255 let bv: f64 = border.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
256 for i in 0..m_size {
257 border[i] -= 2.0 * bv * x[i];
258 }
259 for i in 0..m_size {
260 set(&mut a, k, i + k + 1, border[i]);
261 set(&mut a, i + k + 1, k, border[i]);
262 }
263 }
264
265 let diag: Vec<f64> = (0..n).map(|i| get(&a, i, i)).collect();
266 let off_diag: Vec<f64> = (0..n.saturating_sub(1)).map(|i| get(&a, i, i + 1)).collect();
267 (diag, off_diag)
268}
269
270fn qr_tridiagonal(mut diag: Vec<f64>, mut off: Vec<f64>) -> Vec<f64> {
272 let n = diag.len();
273 if n <= 1 {
274 return diag;
275 }
276
277 let max_iter = 100 * n;
278 let mut m = n;
279
280 for _ in 0..max_iter {
281 if m <= 1 {
282 break;
283 }
284
285 while m > 1 && off[m - 2].abs() < 1e-12 * (diag[m - 2].abs() + diag[m - 1].abs()).max(1e-15) {
287 m -= 1;
288 }
289 if m <= 1 {
290 break;
291 }
292
293 let d = (diag[m - 2] - diag[m - 1]) / 2.0;
295 let sign_d = if d >= 0.0 { 1.0 } else { -1.0 };
296 let shift = diag[m - 1] - off[m - 2] * off[m - 2] / (d + sign_d * (d * d + off[m - 2] * off[m - 2]).sqrt());
297
298 let mut x = diag[0] - shift;
300 let mut z = off[0];
301
302 for k in 0..m - 1 {
303 let r = (x * x + z * z).sqrt();
305 let c = x / r;
306 let s = z / r;
307
308 if k > 0 {
309 off[k - 1] = r;
310 }
311
312 let d1 = diag[k];
313 let d2 = diag[k + 1];
314 let e = off[k];
315
316 diag[k] = c * c * d1 + 2.0 * c * s * e + s * s * d2;
317 diag[k + 1] = s * s * d1 - 2.0 * c * s * e + c * c * d2;
318 off[k] = c * s * (d2 - d1) + (c * c - s * s) * e;
319
320 if k + 1 < m - 1 {
321 x = off[k + 1] * c + 0.0;
322 let old_off_next = off[k + 1];
324 x = off[k];
325 z = -s * old_off_next;
326 off[k + 1] = c * old_off_next;
327 x = off[k]; }
329 }
330 }
331
332 diag.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
333 diag
334}
335
336pub fn eigenvalues_jacobi(matrix: &RandomMatrix) -> Vec<f64> {
339 assert_eq!(matrix.rows, matrix.cols);
340 let n = matrix.rows;
341 if n == 0 {
342 return Vec::new();
343 }
344
345 let mut a = matrix.entries.clone();
346 let max_iter = 100 * n * n;
347
348 for _ in 0..max_iter {
349 let mut max_val = 0.0;
351 let mut p = 0;
352 let mut q = 1;
353 for i in 0..n {
354 for j in (i + 1)..n {
355 let v = a[i * n + j].abs();
356 if v > max_val {
357 max_val = v;
358 p = i;
359 q = j;
360 }
361 }
362 }
363 if max_val < 1e-12 {
364 break;
365 }
366
367 let app = a[p * n + p];
369 let aqq = a[q * n + q];
370 let apq = a[p * n + q];
371
372 let theta = if (app - aqq).abs() < 1e-15 {
373 std::f64::consts::FRAC_PI_4
374 } else {
375 0.5 * (2.0 * apq / (app - aqq)).atan()
376 };
377
378 let c = theta.cos();
379 let s = theta.sin();
380
381 let mut new_a = a.clone();
383 for i in 0..n {
384 let aip = a[i * n + p];
386 let aiq = a[i * n + q];
387 new_a[i * n + p] = c * aip + s * aiq;
388 new_a[i * n + q] = -s * aip + c * aiq;
389 new_a[p * n + i] = new_a[i * n + p];
390 new_a[q * n + i] = new_a[i * n + q];
391 }
392 new_a[p * n + p] = c * c * app + 2.0 * s * c * apq + s * s * aqq;
393 new_a[q * n + q] = s * s * app - 2.0 * s * c * apq + c * c * aqq;
394 new_a[p * n + q] = 0.0;
395 new_a[q * n + p] = 0.0;
396
397 a = new_a;
398 }
399
400 let mut eigs: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
401 eigs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
402 eigs
403}
404
405pub fn wigner_semicircle(eigenvalues: &[f64]) -> Histogram {
412 Histogram::from_samples_bounded(eigenvalues, 50, -2.5, 2.5)
413}
414
415pub fn semicircle_density(x: f64, r: f64) -> f64 {
417 if x.abs() > r {
418 return 0.0;
419 }
420 2.0 / (std::f64::consts::PI * r * r) * (r * r - x * x).sqrt()
421}
422
423pub fn marchenko_pastur(eigenvalues: &[f64], ratio: f64) -> Histogram {
426 let lambda_min = (1.0 - ratio.sqrt()).powi(2);
427 let lambda_max = (1.0 + ratio.sqrt()).powi(2);
428 Histogram::from_samples_bounded(eigenvalues, 50, lambda_min * 0.5, lambda_max * 1.5)
429}
430
431pub fn marchenko_pastur_density(x: f64, ratio: f64) -> f64 {
433 let lambda_min = (1.0 - ratio.sqrt()).powi(2);
434 let lambda_max = (1.0 + ratio.sqrt()).powi(2);
435 if x < lambda_min || x > lambda_max || x <= 0.0 {
436 return 0.0;
437 }
438 let num = ((lambda_max - x) * (x - lambda_min)).sqrt();
439 num / (2.0 * std::f64::consts::PI * ratio * x)
440}
441
442pub fn level_spacing_ratios(eigenvalues: &[f64]) -> Vec<f64> {
445 if eigenvalues.len() < 3 {
446 return Vec::new();
447 }
448 let spacings: Vec<f64> = eigenvalues.windows(2).map(|w| w[1] - w[0]).collect();
449 spacings
450 .windows(2)
451 .map(|w| w[0].min(w[1]) / w[0].max(w[1]).max(1e-15))
452 .collect()
453}
454
455pub struct EigenvalueRenderer {
461 pub bar_character: char,
462 pub bar_color: [f32; 4],
463 pub theory_character: char,
464 pub theory_color: [f32; 4],
465 pub x_scale: f32,
466 pub y_scale: f32,
467}
468
469impl EigenvalueRenderer {
470 pub fn new() -> Self {
471 Self {
472 bar_character: '█',
473 bar_color: [0.4, 0.6, 1.0, 0.8],
474 theory_character: '·',
475 theory_color: [1.0, 0.3, 0.3, 1.0],
476 x_scale: 0.3,
477 y_scale: 2.0,
478 }
479 }
480
481 pub fn render_histogram(&self, hist: &Histogram) -> Vec<(Vec2, char, [f32; 4])> {
483 let mut glyphs = Vec::new();
484 let max_count = hist.max_count().max(1) as f32;
485 for (i, &count) in hist.bins.iter().enumerate() {
486 let x = (i as f32 - hist.bin_count as f32 / 2.0) * self.x_scale;
487 let height = (count as f32 / max_count * 15.0) as usize;
488 for h in 0..height {
489 glyphs.push((
490 Vec2::new(x, h as f32 * self.y_scale * 0.1),
491 self.bar_character,
492 self.bar_color,
493 ));
494 }
495 }
496 glyphs
497 }
498
499 pub fn render_with_semicircle(&self, hist: &Histogram, radius: f64) -> Vec<(Vec2, char, [f32; 4])> {
501 let mut glyphs = self.render_histogram(hist);
502
503 for i in 0..100 {
505 let x = -radius + 2.0 * radius * i as f64 / 100.0;
506 let density = semicircle_density(x, radius);
507 let pos = Vec2::new(x as f32 * self.x_scale * (hist.bin_count as f32 / (2.0 * radius as f32)),
508 density as f32 * self.y_scale);
509 glyphs.push((pos, self.theory_character, self.theory_color));
510 }
511 glyphs
512 }
513}
514
515impl Default for EigenvalueRenderer {
516 fn default() -> Self {
517 Self::new()
518 }
519}
520
521#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn test_goe_symmetric() {
531 let mut rng = Rng::new(42);
532 let m = goe(10, &mut rng);
533 for i in 0..10 {
534 for j in 0..10 {
535 assert!(
536 (m.get(i, j) - m.get(j, i)).abs() < 1e-14,
537 "GOE should be symmetric"
538 );
539 }
540 }
541 }
542
543 #[test]
544 fn test_gue_symmetric() {
545 let mut rng = Rng::new(42);
546 let m = gue(10, &mut rng);
547 for i in 0..10 {
548 for j in 0..10 {
549 assert!(
550 (m.get(i, j) - m.get(j, i)).abs() < 1e-14,
551 "GUE (real approx) should be symmetric"
552 );
553 }
554 }
555 }
556
557 #[test]
558 fn test_wishart_positive_semidefinite() {
559 let mut rng = Rng::new(42);
561 let w = wishart(20, 10, &mut rng);
562 let eigs = eigenvalues_jacobi(&w);
563 for &e in &eigs {
564 assert!(
565 e > -0.1,
566 "Wishart eigenvalue should be >= 0, got {}",
567 e
568 );
569 }
570 }
571
572 #[test]
573 fn test_eigenvalues_identity() {
574 let id = RandomMatrix::identity(5);
575 let eigs = eigenvalues_jacobi(&id);
576 for &e in &eigs {
577 assert!((e - 1.0).abs() < 1e-6, "identity eigenvalue should be 1, got {}", e);
578 }
579 }
580
581 #[test]
582 fn test_eigenvalues_diagonal() {
583 let mut m = RandomMatrix::zeros(3, 3);
584 m.set(0, 0, 1.0);
585 m.set(1, 1, 3.0);
586 m.set(2, 2, 5.0);
587 let eigs = eigenvalues_jacobi(&m);
588 assert!((eigs[0] - 1.0).abs() < 1e-6);
589 assert!((eigs[1] - 3.0).abs() < 1e-6);
590 assert!((eigs[2] - 5.0).abs() < 1e-6);
591 }
592
593 #[test]
594 fn test_eigenvalues_2x2() {
595 let mut m = RandomMatrix::zeros(2, 2);
597 m.set(0, 0, 2.0);
598 m.set(0, 1, 1.0);
599 m.set(1, 0, 1.0);
600 m.set(1, 1, 2.0);
601 let eigs = eigenvalues_jacobi(&m);
602 assert!((eigs[0] - 1.0).abs() < 1e-6, "got {}", eigs[0]);
603 assert!((eigs[1] - 3.0).abs() < 1e-6, "got {}", eigs[1]);
604 }
605
606 #[test]
607 fn test_wigner_semicircle_approximate() {
608 let mut rng = Rng::new(42);
610 let mut all_eigs = Vec::new();
611 let n = 50;
612 let samples = 100;
613 for _ in 0..samples {
614 let m = goe(n, &mut rng);
615 let eigs = eigenvalues_jacobi(&m);
616 all_eigs.extend(eigs);
617 }
618
619 let in_range = all_eigs.iter().filter(|&&e| e.abs() < 2.5).count();
621 let fraction = in_range as f64 / all_eigs.len() as f64;
622 assert!(
623 fraction > 0.9,
624 "most GOE eigenvalues should be in [-2.5, 2.5], fraction = {}",
625 fraction
626 );
627
628 let hist = wigner_semicircle(&all_eigs);
629 assert_eq!(hist.bins.len(), 50);
630 }
631
632 #[test]
633 fn test_semicircle_density() {
634 let r = 2.0;
636 let d0 = semicircle_density(0.0, r);
637 let expected = 2.0 / (std::f64::consts::PI * r);
638 assert!((d0 - expected).abs() < 1e-10);
639
640 assert_eq!(semicircle_density(3.0, 2.0), 0.0);
642 }
643
644 #[test]
645 fn test_matrix_operations() {
646 let mut a = RandomMatrix::zeros(2, 2);
647 a.set(0, 0, 1.0);
648 a.set(0, 1, 2.0);
649 a.set(1, 0, 3.0);
650 a.set(1, 1, 4.0);
651
652 assert!((a.trace() - 5.0).abs() < 1e-10);
653
654 let at = a.transpose();
655 assert!((at.get(0, 1) - 3.0).abs() < 1e-10);
656 assert!((at.get(1, 0) - 2.0).abs() < 1e-10);
657
658 let prod = a.mul(&RandomMatrix::identity(2));
659 assert!((prod.get(0, 0) - 1.0).abs() < 1e-10);
660 }
661
662 #[test]
663 fn test_marchenko_pastur_density() {
664 let ratio = 0.5;
665 let lambda_min = (1.0 - ratio.sqrt()).powi(2);
666 let lambda_max = (1.0 + ratio.sqrt()).powi(2);
667 assert_eq!(marchenko_pastur_density(0.0, ratio), 0.0);
669 assert_eq!(marchenko_pastur_density(lambda_max + 1.0, ratio), 0.0);
670 let mid = (lambda_min + lambda_max) / 2.0;
672 assert!(marchenko_pastur_density(mid, ratio) > 0.0);
673 }
674
675 #[test]
676 fn test_renderer() {
677 let mut rng = Rng::new(42);
678 let m = goe(10, &mut rng);
679 let eigs = eigenvalues_jacobi(&m);
680 let hist = wigner_semicircle(&eigs);
681 let renderer = EigenvalueRenderer::new();
682 let glyphs = renderer.render_histogram(&hist);
683 assert!(!glyphs.is_empty());
684 }
685}