scirs2_stats/distributions/multivariate/wishart.rs
1//! Wishart distribution functions
2//!
3//! This module provides functionality for the Wishart distribution.
4
5use crate::error::{StatsError, StatsResult};
6use crate::sampling::SampleableDistribution;
7use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Data, Ix2};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::{ChiSquared, Distribution, Normal as RandNormal};
10use std::fmt::Debug;
11
12// Import helper functions from the multivariate module
13use super::normal::{compute_cholesky, compute_inverse_from_cholesky};
14
15/// Implementation of the natural logarithm of the gamma function
16/// This is a workaround for the unstable gamma function in Rust
17#[allow(dead_code)]
18fn lgamma(x: f64) -> f64 {
19 if x <= 0.0 {
20 panic!("lgamma requires positive input");
21 }
22
23 // For integers, we can use a simpler calculation
24 if x.fract() == 0.0 && x <= 20.0 {
25 let n = x as usize;
26 if n == 1 || n == 2 {
27 return 0.0; // ln(1) = 0
28 }
29
30 let mut result = 0.0;
31 for i in 2..n {
32 result += (i as f64).ln();
33 }
34 return result;
35 }
36
37 // For x = 0.5, we have Γ(0.5) = sqrt(π)
38 if (x - 0.5).abs() < 1e-10 {
39 return (std::f64::consts::PI.sqrt()).ln();
40 }
41
42 // For x > 1, use the recurrence relation: Γ(x+1) = x * Γ(x)
43 if x > 1.0 {
44 return (x - 1.0).ln() + lgamma(x - 1.0);
45 }
46
47 // For 0 < x < 1, use the reflection formula: Γ(x) * Γ(1-x) = π/sin(πx)
48 if x < 1.0 {
49 return (std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - lgamma(1.0 - x);
50 }
51
52 // Lanczos approximation for other values around 1
53 let p = [
54 676.5203681218851,
55 -1259.1392167224028,
56 771.323_428_777_653_1,
57 -176.615_029_162_140_6,
58 12.507343278686905,
59 -0.13857109526572012,
60 9.984_369_578_019_572e-6,
61 1.5056327351493116e-7,
62 ];
63
64 let x_adj = x - 1.0;
65 let t = x_adj + 7.5;
66
67 let mut sum = 0.0;
68 for (i, &coef) in p.iter().enumerate() {
69 sum += coef / (x_adj + (i + 1) as f64);
70 }
71
72 let pi = std::f64::consts::PI;
73 let sqrt_2pi = (2.0 * pi).sqrt();
74
75 sqrt_2pi.ln() + sum.ln() + (x_adj + 0.5) * t.ln() - t
76}
77
78/// Calculates the multivariate gamma function (natural log)
79///
80/// This is defined as:
81/// ln Γₚ(n/2) = ln (π^(p(p-1)/4) ∏ᵢ₌₁ᵖ Γ((n+1-i)/2))
82///
83/// where p is the dimension and n is the degrees of freedom
84#[allow(dead_code)]
85pub fn lmultigamma(n: f64, p: usize) -> f64 {
86 // Calculate ln(π^(p(p-1)/4))
87 let pi = std::f64::consts::PI;
88 let term1 = (p * (p - 1)) as f64 / 4.0 * pi.ln();
89
90 // Calculate ln(∏ᵢ₌₁ᵖ Γ((n+1-i)/2))
91 let mut term2 = 0.0;
92 for i in 1..=p {
93 let arg = (n + 1.0 - i as f64) / 2.0;
94 term2 += lgamma(arg);
95 }
96
97 term1 + term2
98}
99
100/// Wishart distribution structure
101#[derive(Debug, Clone)]
102pub struct Wishart {
103 /// Scale matrix
104 pub scale: Array2<f64>,
105 /// Degrees of freedom
106 pub df: f64,
107 /// Dimension of the distribution (p x p)
108 pub dim: usize,
109 /// Cholesky decomposition of the scale matrix
110 scale_chol: Array2<f64>,
111 /// Determinant of the scale matrix
112 scale_det: f64,
113}
114
115impl Wishart {
116 /// Create a new Wishart distribution with given parameters
117 ///
118 /// # Arguments
119 ///
120 /// * `scale` - Scale matrix (p x p, symmetric positive-definite)
121 /// * `df` - Degrees of freedom (must be greater than or equal to the dimension)
122 ///
123 /// # Returns
124 ///
125 /// * A new Wishart distribution instance
126 ///
127 /// # Examples
128 ///
129 /// ```
130 /// use scirs2_core::ndarray::array;
131 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
132 ///
133 /// // Create a 2D Wishart distribution with 5 degrees of freedom
134 /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
135 /// let df = 5.0;
136 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
137 /// ```
138 pub fn new<D>(scale: ArrayBase<D, Ix2>, df: f64) -> StatsResult<Self>
139 where
140 D: Data<Elem = f64>,
141 {
142 let scale_owned = scale.to_owned();
143 let dim = scale_owned.shape()[0];
144
145 // Check if the matrix is square
146 if scale_owned.shape()[1] != dim {
147 return Err(StatsError::DimensionMismatch(
148 "Scale matrix must be square".to_string(),
149 ));
150 }
151
152 // Check the degrees of freedom constraint: df >= dim
153 if df < dim as f64 {
154 return Err(StatsError::DomainError(format!(
155 "Degrees of freedom ({}) must be greater than or equal to dimension ({})",
156 df, dim
157 )));
158 }
159
160 // Compute Cholesky decomposition to check if the matrix is positive definite
161 let scale_chol = compute_cholesky(&scale_owned).map_err(|_| {
162 StatsError::DomainError("Scale matrix must be positive definite".to_string())
163 })?;
164
165 // Compute determinant of the _scale matrix
166 let scale_det = {
167 let mut det = 1.0;
168 for i in 0..dim {
169 det *= scale_chol[[i, i]];
170 }
171 det * det // Square it since det(Σ) = det(L)^2
172 };
173
174 Ok(Wishart {
175 scale: scale_owned,
176 df,
177 dim,
178 scale_chol,
179 scale_det,
180 })
181 }
182
183 /// Calculate the probability density function (PDF) at a given matrix point
184 ///
185 /// # Arguments
186 ///
187 /// * `x` - The matrix at which to evaluate the PDF (p x p)
188 ///
189 /// # Returns
190 ///
191 /// * The value of the PDF at the given point
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// use scirs2_core::ndarray::array;
197 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
198 ///
199 /// let scale = array![[1.0, 0.0], [0.0, 1.0]];
200 /// let df = 5.0;
201 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
202 ///
203 /// let x = array![[5.0, 1.0], [1.0, 5.0]];
204 /// let pdf_value = wishart.pdf(&x);
205 /// ```
206 pub fn pdf<D>(&self, x: &ArrayBase<D, Ix2>) -> f64
207 where
208 D: Data<Elem = f64>,
209 {
210 // Check if x has the right dimensions
211 if x.shape()[0] != self.dim || x.shape()[1] != self.dim {
212 return 0.0;
213 }
214
215 // Check if x is symmetric and positive definite
216 let x_owned = x.to_owned();
217 let x_chol = match compute_cholesky(&x_owned) {
218 Ok(chol) => chol,
219 Err(_) => return 0.0, // Not positive definite, PDF is 0
220 };
221
222 // Compute the log PDF and exponentiate
223 self.logpdf_with_cholesky(x, &x_chol).exp()
224 }
225
226 /// Calculate the log PDF with precomputed Cholesky decomposition of x
227 fn logpdf_with_cholesky<D>(&self, x: &ArrayBase<D, Ix2>, xchol: &Array2<f64>) -> f64
228 where
229 D: Data<Elem = f64>,
230 {
231 // Calculate determinant of x
232 let mut x_det = 1.0;
233 for i in 0..self.dim {
234 x_det *= xchol[[i, i]];
235 }
236 x_det = x_det * x_det; // Square it since det(X) = det(L)^2
237
238 // Calculate trace(Σ^-1 X)
239 let scale_inv = compute_inverse_from_cholesky(&self.scale_chol)
240 .expect("Failed to compute matrix inverse");
241
242 let mut trace = 0.0;
243 for i in 0..self.dim {
244 for j in 0..self.dim {
245 trace += scale_inv[[i, j]] * x[[j, i]];
246 }
247 }
248
249 // Calculate log PDF
250 // ln(PDF) = -0.5 * [tr(Σ^-1 X) + n ln|Σ| + p ln 2 + 2 ln Γₚ(n/2)] + 0.5 * (n - p - 1) ln|X|
251 let p = self.dim as f64;
252 let n = self.df;
253
254 let term1 = -0.5 * trace;
255 let term2 = -0.5 * n * self.scale_det.ln();
256 let term3 = -0.5 * p * (2.0f64).ln();
257 let term4 = -lmultigamma(n, self.dim);
258 let term5 = 0.5 * (n - p - 1.0) * x_det.ln();
259
260 term1 + term2 + term3 + term4 + term5
261 }
262
263 /// Calculate the log probability density function (log PDF) at a given matrix point
264 ///
265 /// # Arguments
266 ///
267 /// * `x` - The matrix at which to evaluate the log PDF (p x p)
268 ///
269 /// # Returns
270 ///
271 /// * The value of the log PDF at the given point
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use scirs2_core::ndarray::array;
277 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
278 ///
279 /// let scale = array![[1.0, 0.0], [0.0, 1.0]];
280 /// let df = 5.0;
281 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
282 ///
283 /// let x = array![[5.0, 1.0], [1.0, 5.0]];
284 /// let logpdf_value = wishart.logpdf(&x);
285 /// ```
286 pub fn logpdf<D>(&self, x: &ArrayBase<D, Ix2>) -> f64
287 where
288 D: Data<Elem = f64>,
289 {
290 // Check if x has the right dimensions
291 if x.shape()[0] != self.dim || x.shape()[1] != self.dim {
292 return f64::NEG_INFINITY;
293 }
294
295 // Check if x is symmetric and positive definite
296 let x_owned = x.to_owned();
297 let x_chol = match compute_cholesky(&x_owned) {
298 Ok(chol) => chol,
299 Err(_) => return f64::NEG_INFINITY, // Not positive definite, log PDF is -∞
300 };
301
302 // Compute the log PDF
303 self.logpdf_with_cholesky(x, &x_chol)
304 }
305
306 /// Generate random samples from the distribution
307 ///
308 /// # Arguments
309 ///
310 /// * `size` - Number of samples to generate
311 ///
312 /// # Returns
313 ///
314 /// * Vector of random matrix samples
315 ///
316 /// # Examples
317 ///
318 /// ```ignore
319 /// use scirs2_core::ndarray::array;
320 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
321 ///
322 /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
323 /// let df = 5.0;
324 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
325 ///
326 /// let samples = wishart.rvs(10).expect("Operation failed");
327 /// assert_eq!(samples.len(), 10);
328 /// assert_eq!(samples[0].shape(), &[2, 2]);
329 /// ```
330 pub fn rvs(&self, size: usize) -> StatsResult<Vec<Array2<f64>>> {
331 let mut rng = thread_rng();
332 let normal_dist = RandNormal::new(0.0, 1.0).expect("Operation failed");
333 let mut samples = Vec::with_capacity(size);
334
335 for _ in 0..size {
336 // For integer degrees of freedom, we use the sum of outer products method
337 if self.df.fract() == 0.0 {
338 let n = self.df as usize;
339 let mut x = Array2::<f64>::zeros((self.dim, self.dim));
340
341 // Generate n independent vectors
342 for _ in 0..n {
343 // Generate standard normal vector
344 let mut z = Array1::<f64>::zeros(self.dim);
345 for j in 0..self.dim {
346 z[j] = normal_dist.sample(&mut rng);
347 }
348
349 // Transform using Cholesky decomposition
350 let az = self.scale_chol.dot(&z);
351
352 // Add outer product to X
353 for i in 0..self.dim {
354 for j in 0..self.dim {
355 x[[i, j]] += az[i] * az[j];
356 }
357 }
358 }
359
360 samples.push(x);
361 } else {
362 // For non-integer df, we use Bartlett's decomposition
363 // Generate lower triangular matrix A
364 let mut a = Array2::<f64>::zeros((self.dim, self.dim));
365
366 // Diagonal elements from chi-square distributions
367 for i in 0..self.dim {
368 let df_i = self.df - (i as f64);
369 let chi2_dist = ChiSquared::new(df_i).map_err(|_| {
370 StatsError::ComputationError(
371 "Failed to create chi-square distribution".to_string(),
372 )
373 })?;
374
375 // sqrt because we need to sample from sqrt(χ²) here
376 a[[i, i]] = chi2_dist.sample(&mut rng).sqrt();
377 }
378
379 // Off-diagonal elements from standard normal distribution
380 for i in 0..self.dim {
381 for j in 0..i {
382 a[[i, j]] = normal_dist.sample(&mut rng);
383 }
384 }
385
386 // Compute B = L·A where L is the Cholesky decomposition of scale matrix
387 let b = self.scale_chol.dot(&a);
388
389 // Compute X = B·B'
390 let mut x = Array2::<f64>::zeros((self.dim, self.dim));
391 for i in 0..self.dim {
392 for j in 0..=i {
393 // Only compute lower triangular part
394 let mut sum = 0.0;
395 for k in 0..self.dim {
396 sum += b[[i, k]] * b[[j, k]];
397 }
398 x[[i, j]] = sum;
399 if i != j {
400 x[[j, i]] = sum; // Symmetric
401 }
402 }
403 }
404
405 samples.push(x);
406 }
407 }
408
409 Ok(samples)
410 }
411
412 /// Generate a single random sample from the distribution
413 ///
414 /// # Returns
415 ///
416 /// * A random matrix sample
417 ///
418 /// # Examples
419 ///
420 /// ```ignore
421 /// use scirs2_core::ndarray::array;
422 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
423 ///
424 /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
425 /// let df = 5.0;
426 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
427 ///
428 /// let sample = wishart.rvs_single().expect("Operation failed");
429 /// assert_eq!(sample.shape(), &[2, 2]);
430 /// ```
431 pub fn rvs_single(&self) -> StatsResult<Array2<f64>> {
432 let samples = self.rvs(1)?;
433 Ok(samples[0].clone())
434 }
435
436 /// Mean of the Wishart distribution
437 ///
438 /// # Returns
439 ///
440 /// * Mean matrix (ν × Σ)
441 ///
442 /// # Examples
443 ///
444 /// ```
445 /// use scirs2_core::ndarray::array;
446 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
447 ///
448 /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
449 /// let df = 5.0;
450 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
451 ///
452 /// let mean = wishart.mean();
453 /// ```
454 pub fn mean(&self) -> Array2<f64> {
455 // E[X] = ν × Σ
456 let mut mean = self.scale.clone();
457 mean *= self.df;
458 mean
459 }
460
461 /// Mode of the Wishart distribution (only defined for ν ≥ p + 1)
462 ///
463 /// # Returns
464 ///
465 /// * Mode matrix ((ν - p - 1) × Σ) if ν ≥ p + 1, otherwise None
466 ///
467 /// # Examples
468 ///
469 /// ```
470 /// use scirs2_core::ndarray::array;
471 /// use scirs2_stats::distributions::multivariate::wishart::Wishart;
472 ///
473 /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
474 /// let df = 5.0; // For 2x2 matrix, mode exists when df ≥ 3
475 /// let wishart = Wishart::new(scale, df).expect("Operation failed");
476 ///
477 /// if let Some(mode) = wishart.mode() {
478 /// println!("Mode exists: {:?}", mode);
479 /// }
480 /// ```
481 pub fn mode(&self) -> Option<Array2<f64>> {
482 let p = self.dim as f64;
483 if self.df < p + 1.0 {
484 None // Mode doesn't exist
485 } else {
486 let mut mode = self.scale.clone();
487 mode *= self.df - p - 1.0;
488 Some(mode)
489 }
490 }
491}
492
493/// Create a Wishart distribution with the given parameters.
494///
495/// This is a convenience function to create a Wishart distribution with
496/// the given scale matrix and degrees of freedom.
497///
498/// # Arguments
499///
500/// * `scale` - Scale matrix (p x p, symmetric positive-definite)
501/// * `df` - Degrees of freedom (must be greater than or equal to the dimension)
502///
503/// # Returns
504///
505/// * A Wishart distribution object
506///
507/// # Examples
508///
509/// ```
510/// use scirs2_core::ndarray::array;
511/// use scirs2_stats::distributions::multivariate;
512///
513/// let scale = array![[1.0, 0.5], [0.5, 2.0]];
514/// let df = 5.0;
515/// let wishart = multivariate::wishart(scale, df).expect("Operation failed");
516/// ```
517#[allow(dead_code)]
518pub fn wishart<D>(scale: ArrayBase<D, Ix2>, df: f64) -> StatsResult<Wishart>
519where
520 D: Data<Elem = f64>,
521{
522 Wishart::new(scale, df)
523}
524
525/// Implementation of SampleableDistribution for Wishart
526impl SampleableDistribution<Array2<f64>> for Wishart {
527 fn rvs(&self, size: usize) -> StatsResult<Vec<Array2<f64>>> {
528 self.rvs(size)
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use approx::assert_relative_eq;
536 use scirs2_core::ndarray::array;
537
538 #[test]
539 fn test_wishart_creation() {
540 // 2x2 Wishart with identity scale
541 let scale = array![[1.0, 0.0], [0.0, 1.0]];
542 let df = 5.0;
543 let wishart = Wishart::new(scale.clone(), df).expect("Operation failed");
544
545 assert_eq!(wishart.dim, 2);
546 assert_eq!(wishart.df, df);
547 assert_eq!(wishart.scale, scale);
548
549 // 3x3 Wishart with custom scale
550 let scale3 = array![[2.0, 0.5, 0.3], [0.5, 1.0, 0.1], [0.3, 0.1, 1.5]];
551 let df3 = 10.0;
552 let wishart3 = Wishart::new(scale3.clone(), df3).expect("Operation failed");
553
554 assert_eq!(wishart3.dim, 3);
555 assert_eq!(wishart3.df, df3);
556 assert_eq!(wishart3.scale, scale3);
557 }
558
559 #[test]
560 fn test_wishart_creation_errors() {
561 // Non-square scale matrix
562 let non_square_scale = array![[1.0, 0.5, 0.3], [0.5, 1.0, 0.1]];
563 assert!(Wishart::new(non_square_scale, 5.0).is_err());
564
565 // Degrees of freedom too small
566 let scale = array![[1.0, 0.0], [0.0, 1.0]];
567 assert!(Wishart::new(scale.clone(), 1.0).is_err()); // df < dim
568
569 // Non-positive definite scale matrix
570 let non_pd_scale = array![[1.0, 2.0], [2.0, 1.0]]; // Not positive definite
571 assert!(Wishart::new(non_pd_scale, 5.0).is_err());
572 }
573
574 #[test]
575 fn test_wishart_mean() {
576 let scale = array![[1.0, 0.5], [0.5, 2.0]];
577 let df = 5.0;
578 let wishart = Wishart::new(scale.clone(), df).expect("Operation failed");
579
580 let mean = wishart.mean();
581 let expected_mean = scale * df;
582
583 for i in 0..2 {
584 for j in 0..2 {
585 assert_relative_eq!(mean[[i, j]], expected_mean[[i, j]], epsilon = 1e-10);
586 }
587 }
588 }
589
590 #[test]
591 fn test_wishart_mode() {
592 let scale = array![[1.0, 0.5], [0.5, 2.0]];
593 let df = 5.0; // df = 5 > p + 1 = 3, so mode exists
594 let wishart = Wishart::new(scale.clone(), df).expect("Operation failed");
595
596 let mode = wishart.mode().expect("Operation failed"); // Mode should exist
597 let expected_mode = scale.clone() * (df - 3.0); // (ν - p - 1) × Σ where p = 2
598
599 for i in 0..2 {
600 for j in 0..2 {
601 assert_relative_eq!(mode[[i, j]], expected_mode[[i, j]], epsilon = 1e-10);
602 }
603 }
604
605 // Test case when mode doesn't exist
606 let wishart2 = Wishart::new(scale, 2.5).expect("Operation failed"); // df = 2.5 < p + 1 = 3
607 assert!(wishart2.mode().is_none()); // Mode should not exist
608 }
609
610 #[test]
611 fn test_wishart_pdf() {
612 // Simple identity scale case
613 let scale = array![[1.0, 0.0], [0.0, 1.0]];
614 let df = 5.0;
615 let wishart = Wishart::new(scale, df).expect("Operation failed");
616
617 // PDF at identity matrix
618 let x = array![[1.0, 0.0], [0.0, 1.0]];
619 let pdf_at_id = wishart.pdf(&x);
620 assert!(pdf_at_id > 0.0);
621
622 // PDF at another matrix
623 let x2 = array![[2.0, 0.5], [0.5, 3.0]];
624 let pdf_at_x2 = wishart.pdf(&x2);
625 assert!(pdf_at_x2 > 0.0);
626
627 // Check pdf of non-positive definite matrix is zero
628 let non_pd = array![[1.0, 2.0], [2.0, 1.0]];
629 assert_eq!(wishart.pdf(&non_pd), 0.0);
630 }
631
632 #[test]
633 fn test_wishart_logpdf() {
634 let scale = array![[1.0, 0.0], [0.0, 1.0]];
635 let df = 5.0;
636 let wishart = Wishart::new(scale, df).expect("Operation failed");
637
638 // Check that exp(logPDF) = PDF
639 let x = array![[2.0, 0.5], [0.5, 3.0]];
640 let pdf = wishart.pdf(&x);
641 let logpdf = wishart.logpdf(&x);
642 assert_relative_eq!(logpdf.exp(), pdf, epsilon = 1e-10);
643
644 // Check logpdf of non-positive definite matrix is -∞
645 let non_pd = array![[1.0, 2.0], [2.0, 1.0]];
646 assert_eq!(wishart.logpdf(&non_pd), f64::NEG_INFINITY);
647 }
648
649 #[test]
650 fn test_wishart_rvs() {
651 let scale = array![[1.0, 0.5], [0.5, 2.0]];
652 let df = 5.0;
653 let wishart = Wishart::new(scale.clone(), df).expect("Operation failed");
654
655 // Generate samples (increased from 100 to 1000 for robustness)
656 let n_samples_ = 1000;
657 let samples = wishart.rvs(n_samples_).expect("Operation failed");
658
659 // Check number of samples
660 assert_eq!(samples.len(), n_samples_);
661
662 // Check dimensions of each sample
663 for sample in &samples {
664 assert_eq!(sample.shape(), &[2, 2]);
665 }
666
667 // Compute sample mean
668 let mut sample_mean = Array2::<f64>::zeros((2, 2));
669 for sample in &samples {
670 sample_mean += sample;
671 }
672 sample_mean /= n_samples_ as f64;
673
674 // Expected mean is ν × Σ
675 let expected_mean = scale * df;
676
677 // Check that sample mean is close to expected mean (allowing for sampling variation)
678 for i in 0..2 {
679 for j in 0..2 {
680 assert_relative_eq!(
681 sample_mean[[i, j]],
682 expected_mean[[i, j]],
683 epsilon = 0.8, // Larger epsilon for random sampling
684 max_relative = 0.3
685 );
686 }
687 }
688 }
689
690 #[test]
691 fn test_wishart_rvs_single() {
692 let scale = array![[1.0, 0.5], [0.5, 2.0]];
693 let df = 5.0;
694 let wishart = Wishart::new(scale, df).expect("Operation failed");
695
696 let sample = wishart.rvs_single().expect("Operation failed");
697
698 // Check dimensions
699 assert_eq!(sample.shape(), &[2, 2]);
700
701 // Check that sample is symmetric
702 assert_relative_eq!(sample[[0, 1]], sample[[1, 0]], epsilon = 1e-10);
703
704 // Check that sample is positive definite (can compute Cholesky)
705 assert!(compute_cholesky(&sample).is_ok());
706 }
707}