Skip to main content

scirs2_stats/distributions/multivariate/
inverse_wishart.rs

1//! Inverse Wishart distribution functions
2//!
3//! This module provides functionality for the Inverse Wishart distribution.
4
5use crate::error::{StatsError, StatsResult};
6use crate::sampling::SampleableDistribution;
7use scirs2_core::ndarray::{Array2, ArrayBase, Data, Ix2};
8use std::fmt::Debug;
9
10// Import helper functions from the multivariate module
11use super::normal::{compute_cholesky, compute_inverse_from_cholesky};
12use super::wishart::Wishart;
13
14/// Inverse Wishart distribution structure
15#[derive(Debug, Clone)]
16pub struct InverseWishart {
17    /// Scale matrix
18    pub scale: Array2<f64>,
19    /// Degrees of freedom
20    pub df: f64,
21    /// Dimension of the distribution (p x p)
22    pub dim: usize,
23    /// Cholesky decomposition of the scale matrix
24    #[allow(dead_code)]
25    scale_chol: Array2<f64>,
26    /// Determinant of the scale matrix
27    scale_det: f64,
28}
29
30impl InverseWishart {
31    /// Create a new Inverse Wishart distribution with given parameters
32    ///
33    /// # Arguments
34    ///
35    /// * `scale` - Scale matrix (p x p, symmetric positive-definite)
36    /// * `df` - Degrees of freedom (must be greater than p + 1)
37    ///
38    /// # Returns
39    ///
40    /// * A new Inverse Wishart distribution instance
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use scirs2_core::ndarray::array;
46    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
47    ///
48    /// // Create a 2D Inverse Wishart distribution with 5 degrees of freedom
49    /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
50    /// let df = 5.0;
51    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
52    /// ```
53    pub fn new<D>(scale: ArrayBase<D, Ix2>, df: f64) -> StatsResult<Self>
54    where
55        D: Data<Elem = f64>,
56    {
57        let scale_owned = scale.to_owned();
58        let dim = scale_owned.shape()[0];
59
60        // Check if the matrix is square
61        if scale_owned.shape()[1] != dim {
62            return Err(StatsError::DimensionMismatch(
63                "Scale matrix must be square".to_string(),
64            ));
65        }
66
67        // Check the degrees of freedom constraint: df > p + 1
68        if df <= dim as f64 + 1.0 {
69            return Err(StatsError::DomainError(format!(
70                "Degrees of freedom ({}) must be greater than dimension + 1 ({})",
71                df,
72                dim + 1
73            )));
74        }
75
76        // Compute Cholesky decomposition to check if the matrix is positive definite
77        let scale_chol = compute_cholesky(&scale_owned).map_err(|_| {
78            StatsError::DomainError("Scale matrix must be positive definite".to_string())
79        })?;
80
81        // Compute determinant of the _scale matrix
82        let scale_det = {
83            let mut det = 1.0;
84            for i in 0..dim {
85                det *= scale_chol[[i, i]];
86            }
87            det * det // Square it since det(Σ) = det(L)^2
88        };
89
90        Ok(InverseWishart {
91            scale: scale_owned,
92            df,
93            dim,
94            scale_chol,
95            scale_det,
96        })
97    }
98
99    /// Calculate the probability density function (PDF) at a given matrix point
100    ///
101    /// # Arguments
102    ///
103    /// * `x` - The matrix at which to evaluate the PDF (p x p)
104    ///
105    /// # Returns
106    ///
107    /// * The value of the PDF at the given point
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use scirs2_core::ndarray::array;
113    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
114    ///
115    /// let scale = array![[1.0, 0.0], [0.0, 1.0]];
116    /// let df = 5.0;
117    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
118    ///
119    /// let x = array![[0.2, 0.05], [0.05, 0.3]];
120    /// let pdf_value = inv_wishart.pdf(&x);
121    /// ```
122    pub fn pdf<D>(&self, x: &ArrayBase<D, Ix2>) -> f64
123    where
124        D: Data<Elem = f64>,
125    {
126        // Check if x has the right dimensions
127        if x.shape()[0] != self.dim || x.shape()[1] != self.dim {
128            return 0.0;
129        }
130
131        // Check if x is symmetric and positive definite
132        let x_owned = x.to_owned();
133        let x_chol = match compute_cholesky(&x_owned) {
134            Ok(chol) => chol,
135            Err(_) => return 0.0, // Not positive definite, PDF is 0
136        };
137
138        // Compute the log PDF and exponentiate
139        self.logpdf_with_cholesky(x, &x_chol).exp()
140    }
141
142    /// Calculate the log PDF with precomputed Cholesky decomposition of x
143    fn logpdf_with_cholesky<D>(&self, x: &ArrayBase<D, Ix2>, xchol: &Array2<f64>) -> f64
144    where
145        D: Data<Elem = f64>,
146    {
147        // Calculate determinant of _x
148        let mut x_det = 1.0;
149        for i in 0..self.dim {
150            x_det *= xchol[[i, i]];
151        }
152        x_det = x_det * x_det; // Square it since det(X) = det(L)^2
153
154        // Compute x_inv using its Cholesky decomposition
155        let x_inv = compute_inverse_from_cholesky(xchol).expect("Failed to compute matrix inverse");
156
157        // Calculate trace(Ψ·X^-1)
158        let mut trace = 0.0;
159        for i in 0..self.dim {
160            for j in 0..self.dim {
161                trace += self.scale[[i, j]] * x_inv[[j, i]];
162            }
163        }
164
165        // Calculate log PDF
166        // ln(PDF) = -0.5 * [tr(Ψ·X^-1) + ln|Ψ| + p ln 2 + 2 ln Γₚ((n+p+1)/2)]
167        //           - 0.5 * (n + p + 1) ln|X|
168        // Note: For Inverse Wishart, we use n+p+1 as the effective degree of freedom
169        let p = self.dim as f64;
170        let n = self.df;
171
172        let term1 = -0.5 * trace;
173        let term2 = -0.5 * self.scale_det.ln();
174        let term3 = -0.5 * p * (2.0f64).ln();
175        let term4 = -super::wishart::lmultigamma(n, self.dim);
176        let term5 = -0.5 * (n + p + 1.0) * x_det.ln();
177
178        term1 + term2 + term3 + term4 + term5
179    }
180
181    /// Calculate the log probability density function (log PDF) at a given matrix point
182    ///
183    /// # Arguments
184    ///
185    /// * `x` - The matrix at which to evaluate the log PDF (p x p)
186    ///
187    /// # Returns
188    ///
189    /// * The value of the log PDF at the given point
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use scirs2_core::ndarray::array;
195    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
196    ///
197    /// let scale = array![[1.0, 0.0], [0.0, 1.0]];
198    /// let df = 5.0;
199    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
200    ///
201    /// let x = array![[0.2, 0.05], [0.05, 0.3]];
202    /// let logpdf_value = inv_wishart.logpdf(&x);
203    /// ```
204    pub fn logpdf<D>(&self, x: &ArrayBase<D, Ix2>) -> f64
205    where
206        D: Data<Elem = f64>,
207    {
208        // Check if x has the right dimensions
209        if x.shape()[0] != self.dim || x.shape()[1] != self.dim {
210            return f64::NEG_INFINITY;
211        }
212
213        // Check if x is symmetric and positive definite
214        let x_owned = x.to_owned();
215        let x_chol = match compute_cholesky(&x_owned) {
216            Ok(chol) => chol,
217            Err(_) => return f64::NEG_INFINITY, // Not positive definite, log PDF is -∞
218        };
219
220        // Compute the log PDF
221        self.logpdf_with_cholesky(x, &x_chol)
222    }
223
224    /// Generate random samples from the distribution
225    ///
226    /// # Arguments
227    ///
228    /// * `size` - Number of samples to generate
229    ///
230    /// # Returns
231    ///
232    /// * Vector of random matrix samples
233    ///
234    /// # Examples
235    ///
236    /// ```ignore
237    /// use scirs2_core::ndarray::array;
238    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
239    ///
240    /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
241    /// let df = 5.0;
242    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
243    ///
244    /// let samples = inv_wishart.rvs(10).expect("Operation failed");
245    /// assert_eq!(samples.len(), 10);
246    /// assert_eq!(samples[0].shape(), &[2, 2]);
247    /// ```
248    pub fn rvs(&self, size: usize) -> StatsResult<Vec<Array2<f64>>> {
249        // Create a corresponding Wishart distribution
250        let wishart = Wishart::new(self.scale.clone(), self.df)?;
251
252        // Generate samples from the Wishart distribution and invert them
253        let wishart_samples = wishart.rvs(size)?;
254        let mut inv_wishart_samples = Vec::with_capacity(size);
255
256        for sample in wishart_samples {
257            // Compute Cholesky decomposition of the Wishart sample
258            let sample_chol = compute_cholesky(&sample).map_err(|_| {
259                StatsError::ComputationError("Failed to compute Cholesky decomposition".to_string())
260            })?;
261
262            // Compute the inverse
263            let inv_sample = compute_inverse_from_cholesky(&sample_chol).map_err(|_| {
264                StatsError::ComputationError("Failed to compute matrix inverse".to_string())
265            })?;
266
267            inv_wishart_samples.push(inv_sample);
268        }
269
270        Ok(inv_wishart_samples)
271    }
272
273    /// Generate a single random sample from the distribution
274    ///
275    /// # Returns
276    ///
277    /// * A random matrix sample
278    ///
279    /// # Examples
280    ///
281    /// ```ignore
282    /// use scirs2_core::ndarray::array;
283    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
284    ///
285    /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
286    /// let df = 5.0;
287    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
288    ///
289    /// let sample = inv_wishart.rvs_single().expect("Operation failed");
290    /// assert_eq!(sample.shape(), &[2, 2]);
291    /// ```
292    pub fn rvs_single(&self) -> StatsResult<Array2<f64>> {
293        let samples = self.rvs(1)?;
294        Ok(samples[0].clone())
295    }
296
297    /// Mean of the Inverse Wishart distribution
298    ///
299    /// # Returns
300    ///
301    /// * Mean matrix (Ψ / (ν - p - 1))
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use scirs2_core::ndarray::array;
307    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
308    ///
309    /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
310    /// let df = 5.0;
311    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
312    ///
313    /// if let Ok(mean) = inv_wishart.mean() {
314    ///     println!("Mean: {:?}", mean);
315    /// }
316    /// ```
317    pub fn mean(&self) -> StatsResult<Array2<f64>> {
318        let p = self.dim as f64;
319        let nu = self.df;
320
321        // Mean exists only when ν > p + 1
322        if nu <= p + 1.0 {
323            return Err(StatsError::DomainError(
324                "Mean is undefined for degrees of freedom <= dimension + 1".to_string(),
325            ));
326        }
327
328        // E[X] = Ψ / (ν - p - 1)
329        let mut mean = self.scale.clone();
330        mean /= nu - p - 1.0;
331        Ok(mean)
332    }
333
334    /// Mode of the Inverse Wishart distribution
335    ///
336    /// # Returns
337    ///
338    /// * Mode matrix (Ψ / (ν + p + 1))
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use scirs2_core::ndarray::array;
344    /// use scirs2_stats::distributions::multivariate::inverse_wishart::InverseWishart;
345    ///
346    /// let scale = array![[1.0, 0.5], [0.5, 2.0]];
347    /// let df = 5.0;
348    /// let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
349    ///
350    /// let mode = inv_wishart.mode();
351    /// ```
352    pub fn mode(&self) -> Array2<f64> {
353        let p = self.dim as f64;
354        let nu = self.df;
355
356        // Mode = Ψ / (ν + p + 1)
357        let mut mode = self.scale.clone();
358        mode /= nu + p + 1.0;
359        mode
360    }
361}
362
363/// Create an Inverse Wishart distribution with the given parameters.
364///
365/// This is a convenience function to create an Inverse Wishart distribution with
366/// the given scale matrix and degrees of freedom.
367///
368/// # Arguments
369///
370/// * `scale` - Scale matrix (p x p, symmetric positive-definite)
371/// * `df` - Degrees of freedom (must be greater than dimension + 1)
372///
373/// # Returns
374///
375/// * An Inverse Wishart distribution object
376///
377/// # Examples
378///
379/// ```
380/// use scirs2_core::ndarray::array;
381/// use scirs2_stats::distributions::multivariate;
382///
383/// let scale = array![[1.0, 0.5], [0.5, 2.0]];
384/// let df = 5.0;
385/// let inv_wishart = multivariate::inverse_wishart(scale, df).expect("Operation failed");
386/// ```
387#[allow(dead_code)]
388pub fn inverse_wishart<D>(scale: ArrayBase<D, Ix2>, df: f64) -> StatsResult<InverseWishart>
389where
390    D: Data<Elem = f64>,
391{
392    InverseWishart::new(scale, df)
393}
394
395/// Implementation of SampleableDistribution for InverseWishart
396impl SampleableDistribution<Array2<f64>> for InverseWishart {
397    fn rvs(&self, size: usize) -> StatsResult<Vec<Array2<f64>>> {
398        self.rvs(size)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use approx::assert_relative_eq;
406    use scirs2_core::ndarray::array;
407
408    #[test]
409    fn test_inverse_wishart_creation() {
410        // 2x2 Inverse Wishart with identity scale
411        let scale = array![[1.0, 0.0], [0.0, 1.0]];
412        let df = 5.0;
413        let inv_wishart = InverseWishart::new(scale.clone(), df).expect("Operation failed");
414
415        assert_eq!(inv_wishart.dim, 2);
416        assert_eq!(inv_wishart.df, df);
417        assert_eq!(inv_wishart.scale, scale);
418
419        // 3x3 Inverse Wishart with custom scale
420        let scale3 = array![[2.0, 0.5, 0.3], [0.5, 1.0, 0.1], [0.3, 0.1, 1.5]];
421        let df3 = 10.0;
422        let inv_wishart3 = InverseWishart::new(scale3.clone(), df3).expect("Operation failed");
423
424        assert_eq!(inv_wishart3.dim, 3);
425        assert_eq!(inv_wishart3.df, df3);
426        assert_eq!(inv_wishart3.scale, scale3);
427    }
428
429    #[test]
430    fn test_inverse_wishart_creation_errors() {
431        // Non-square scale matrix
432        let non_square_scale = array![[1.0, 0.5, 0.3], [0.5, 1.0, 0.1]];
433        assert!(InverseWishart::new(non_square_scale, 5.0).is_err());
434
435        // Degrees of freedom too small
436        let scale = array![[1.0, 0.0], [0.0, 1.0]];
437        assert!(InverseWishart::new(scale.clone(), 3.0).is_err()); // df <= dim + 1
438
439        // Non-positive definite scale matrix
440        let non_pd_scale = array![[1.0, 2.0], [2.0, 1.0]]; // Not positive definite
441        assert!(InverseWishart::new(non_pd_scale, 5.0).is_err());
442    }
443
444    #[test]
445    fn test_inverse_wishart_mean() {
446        let scale = array![[1.0, 0.5], [0.5, 2.0]];
447        let df = 5.0;
448        let inv_wishart = InverseWishart::new(scale.clone(), df).expect("Operation failed");
449
450        let mean = inv_wishart.mean().expect("Operation failed");
451        let expected_mean = scale.clone() / (df - 3.0); // (df - p - 1) where p = 2
452
453        for i in 0..2 {
454            for j in 0..2 {
455                assert_relative_eq!(mean[[i, j]], expected_mean[[i, j]], epsilon = 1e-10);
456            }
457        }
458    }
459
460    #[test]
461    fn test_inverse_wishart_mode() {
462        let scale = array![[1.0, 0.5], [0.5, 2.0]];
463        let df = 5.0;
464        let inv_wishart = InverseWishart::new(scale.clone(), df).expect("Operation failed");
465
466        let mode = inv_wishart.mode();
467        let expected_mode = scale.clone() / (df + 3.0); // (df + p + 1) where p = 2
468
469        for i in 0..2 {
470            for j in 0..2 {
471                assert_relative_eq!(mode[[i, j]], expected_mode[[i, j]], epsilon = 1e-10);
472            }
473        }
474    }
475
476    #[test]
477    fn test_inverse_wishart_pdf() {
478        // Simple identity scale case
479        let scale = array![[1.0, 0.0], [0.0, 1.0]];
480        let df = 5.0;
481        let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
482
483        // PDF at identity matrix (should be positive)
484        let x = array![[1.0, 0.0], [0.0, 1.0]];
485        let pdf_at_id = inv_wishart.pdf(&x);
486        assert!(pdf_at_id > 0.0);
487
488        // PDF at another matrix
489        let x2 = array![[0.5, 0.1], [0.1, 0.8]];
490        let pdf_at_x2 = inv_wishart.pdf(&x2);
491        assert!(pdf_at_x2 > 0.0);
492
493        // Check pdf of non-positive definite matrix is zero
494        let non_pd = array![[1.0, 2.0], [2.0, 1.0]];
495        assert_eq!(inv_wishart.pdf(&non_pd), 0.0);
496    }
497
498    #[test]
499    fn test_inverse_wishart_logpdf() {
500        let scale = array![[1.0, 0.0], [0.0, 1.0]];
501        let df = 5.0;
502        let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
503
504        // Check that exp(logPDF) = PDF
505        let x = array![[0.5, 0.1], [0.1, 0.8]];
506        let pdf = inv_wishart.pdf(&x);
507        let logpdf = inv_wishart.logpdf(&x);
508        assert_relative_eq!(logpdf.exp(), pdf, epsilon = 1e-10);
509
510        // Check logpdf of non-positive definite matrix is -∞
511        let non_pd = array![[1.0, 2.0], [2.0, 1.0]];
512        assert_eq!(inv_wishart.logpdf(&non_pd), f64::NEG_INFINITY);
513    }
514
515    #[test]
516    fn test_inverse_wishart_rvs() {
517        let scale = array![[1.0, 0.5], [0.5, 2.0]];
518        let df = 5.0;
519        let inv_wishart = InverseWishart::new(scale.clone(), df).expect("Operation failed");
520
521        // Generate samples
522        let n_samples_ = 100;
523        let samples = inv_wishart.rvs(n_samples_).expect("Operation failed");
524
525        // Check number of samples
526        assert_eq!(samples.len(), n_samples_);
527
528        // Check dimensions of each sample
529        for sample in &samples {
530            assert_eq!(sample.shape(), &[2, 2]);
531        }
532
533        // Compute sample mean
534        let mut sample_mean = Array2::<f64>::zeros((2, 2));
535        for sample in &samples {
536            sample_mean += sample;
537        }
538        sample_mean /= n_samples_ as f64;
539
540        // Expected mean is Ψ/(ν-p-1) where p=2
541        let expected_mean = scale.clone() / (df - 3.0);
542
543        // Check that sample mean is close to expected mean (allowing for sampling variation)
544        for i in 0..2 {
545            for j in 0..2 {
546                assert_relative_eq!(
547                    sample_mean[[i, j]],
548                    expected_mean[[i, j]],
549                    epsilon = 1.0, // Larger epsilon for random sampling with higher variance
550                    max_relative = 0.8  // Higher tolerance for relative error
551                );
552            }
553        }
554    }
555
556    #[test]
557    fn test_inverse_wishart_rvs_single() {
558        let scale = array![[1.0, 0.5], [0.5, 2.0]];
559        let df = 5.0;
560        let inv_wishart = InverseWishart::new(scale, df).expect("Operation failed");
561
562        let sample = inv_wishart.rvs_single().expect("Operation failed");
563
564        // Check dimensions
565        assert_eq!(sample.shape(), &[2, 2]);
566
567        // Check that sample is symmetric
568        assert_relative_eq!(sample[[0, 1]], sample[[1, 0]], epsilon = 1e-10);
569
570        // Check that sample is positive definite (can compute Cholesky)
571        assert!(compute_cholesky(&sample).is_ok());
572    }
573}