Skip to main content

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}