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}