Skip to main content

scirs2_stats/distributions/multivariate/
multivariate_lognormal.rs

1//! Multivariate Lognormal distribution functions
2//!
3//! This module provides functionality for the Multivariate Lognormal distribution.
4
5use crate::distributions::multivariate::normal::MultivariateNormal;
6use crate::error::StatsResult;
7use crate::sampling::SampleableDistribution;
8use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, ArrayView1, ArrayView2, Data, Ix1, Ix2};
9use std::fmt::Debug;
10
11/// Multivariate Lognormal distribution structure
12///
13/// The multivariate lognormal distribution arises when taking the exponential
14/// of a multivariate normal random variable.
15#[derive(Debug, Clone)]
16pub struct MultivariateLognormal {
17    /// Mean vector of the underlying multivariate normal distribution
18    pub mu: Array1<f64>,
19    /// Covariance matrix of the underlying multivariate normal distribution
20    pub sigma: Array2<f64>,
21    /// Dimensionality of the distribution
22    pub dim: usize,
23    /// Associated multivariate normal distribution
24    mvn: MultivariateNormal,
25}
26
27impl MultivariateLognormal {
28    /// Create a new multivariate lognormal distribution with given parameters.
29    ///
30    /// The parameters represent the mean vector and covariance matrix of the
31    /// underlying multivariate normal distribution, not of the lognormal distribution itself.
32    ///
33    /// # Arguments
34    ///
35    /// * `mu` - Mean vector of the underlying multivariate normal (k-dimensional)
36    /// * `sigma` - Covariance matrix of the underlying multivariate normal (k x k, symmetric positive-definite)
37    ///
38    /// # Returns
39    ///
40    /// * A new MultivariateLognormal distribution instance
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use scirs2_core::ndarray::{array, Array1, Array2};
46    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
47    ///
48    /// // Create a 2D multivariate lognormal distribution
49    /// let mu = array![0.0, 0.0];
50    /// let sigma = array![[0.5, 0.2], [0.2, 0.5]];
51    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
52    /// ```
53    pub fn new<D1, D2>(mu: ArrayBase<D1, Ix1>, sigma: ArrayBase<D2, Ix2>) -> StatsResult<Self>
54    where
55        D1: Data<Elem = f64>,
56        D2: Data<Elem = f64>,
57    {
58        // Create the underlying MultivariateNormal distribution
59        let mvn = MultivariateNormal::new(mu.to_owned(), sigma.to_owned())?;
60
61        // Get the dimension of the distribution
62        let dim = mvn.dim();
63
64        Ok(MultivariateLognormal {
65            mu: mu.to_owned(),
66            sigma: sigma.to_owned(),
67            dim,
68            mvn,
69        })
70    }
71
72    /// Calculate the probability density function (PDF) at a given point.
73    ///
74    /// # Arguments
75    ///
76    /// * `x` - The point at which to evaluate the PDF (must have all positive components)
77    ///
78    /// # Returns
79    ///
80    /// * The value of the PDF at the given point
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use scirs2_core::ndarray::array;
86    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
87    ///
88    /// let mu = array![0.0, 0.0];
89    /// let sigma = array![[0.5, 0.0], [0.0, 0.5]];
90    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
91    ///
92    /// let x = array![1.0, 1.0];
93    /// let pdf_value = mvln.pdf(&x);
94    /// ```
95    pub fn pdf<D>(&self, x: &ArrayBase<D, Ix1>) -> f64
96    where
97        D: Data<Elem = f64>,
98    {
99        if x.len() != self.dim {
100            return 0.0; // Invalid dimensions
101        }
102
103        // Check if all components are positive
104        for &xi in x.iter() {
105            if xi <= 0.0 {
106                return 0.0; // Log-normal is only defined for positive values
107            }
108        }
109
110        // Convert x to log space (element-wise log)
111        let log_x = x.mapv(|xi| xi.ln());
112
113        // Calculate normal PDF in log space
114        let normal_pdf = self.mvn.pdf(&log_x);
115
116        // Apply Jacobian correction: multiply by 1/(x1*x2*...*xn)
117        let jacobian_factor = x.iter().fold(1.0, |acc, &xi| acc * xi);
118
119        normal_pdf / jacobian_factor
120    }
121
122    /// Calculate the log probability density function (log PDF) at a given point.
123    ///
124    /// # Arguments
125    ///
126    /// * `x` - The point at which to evaluate the log PDF (must have all positive components)
127    ///
128    /// # Returns
129    ///
130    /// * The value of the log PDF at the given point
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use scirs2_core::ndarray::array;
136    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
137    ///
138    /// let mu = array![0.0, 0.0];
139    /// let sigma = array![[0.5, 0.0], [0.0, 0.5]];
140    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
141    ///
142    /// let x = array![1.0, 1.0];
143    /// let logpdf_value = mvln.logpdf(&x);
144    /// ```
145    pub fn logpdf<D>(&self, x: &ArrayBase<D, Ix1>) -> f64
146    where
147        D: Data<Elem = f64>,
148    {
149        if x.len() != self.dim {
150            return f64::NEG_INFINITY; // Invalid dimensions
151        }
152
153        // Check if all components are positive
154        for &xi in x.iter() {
155            if xi <= 0.0 {
156                return f64::NEG_INFINITY; // Log-normal is only defined for positive values
157            }
158        }
159
160        // Convert x to log space (element-wise log)
161        let log_x = x.mapv(|xi| xi.ln());
162
163        // Calculate normal log PDF in log space
164        let normal_logpdf = self.mvn.logpdf(&log_x);
165
166        // Apply Jacobian correction: subtract sum of log(xi)
167        let sum_log_x = x.iter().fold(0.0, |acc, &xi| acc + xi.ln());
168
169        normal_logpdf - sum_log_x
170    }
171
172    /// Generate random samples from the distribution.
173    ///
174    /// # Arguments
175    ///
176    /// * `size` - Number of samples to generate
177    ///
178    /// # Returns
179    ///
180    /// * Matrix where each row is a random sample
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use scirs2_core::ndarray::array;
186    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
187    ///
188    /// let mu = array![0.0, 0.0];
189    /// let sigma = array![[0.5, 0.2], [0.2, 0.5]];
190    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
191    ///
192    /// let samples = mvln.rvs(100).expect("Operation failed");
193    /// assert_eq!(samples.shape(), &[100, 2]);
194    /// ```
195    pub fn rvs(&self, size: usize) -> StatsResult<Array2<f64>> {
196        // Generate samples from the underlying normal distribution
197        let normal_samples = self.mvn.rvs(size)?;
198
199        // Transform the samples by taking element-wise exponential
200        let lognormal_samples = normal_samples.mapv(|x| x.exp());
201
202        Ok(lognormal_samples)
203    }
204
205    /// Generate a single random sample from the distribution.
206    ///
207    /// # Returns
208    ///
209    /// * Vector representing a single sample
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use scirs2_core::ndarray::array;
215    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
216    ///
217    /// let mu = array![0.0, 0.0];
218    /// let sigma = array![[0.5, 0.2], [0.2, 0.5]];
219    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
220    ///
221    /// let sample = mvln.rvs_single().expect("Operation failed");
222    /// assert_eq!(sample.len(), 2);
223    /// ```
224    pub fn rvs_single(&self) -> StatsResult<Array1<f64>> {
225        // Generate a sample from the underlying normal distribution
226        let normal_sample = self.mvn.rvs_single()?;
227
228        // Transform the sample by taking element-wise exponential
229        let lognormal_sample = normal_sample.mapv(|x| x.exp());
230
231        Ok(lognormal_sample)
232    }
233
234    /// Calculate the mean of the distribution.
235    ///
236    /// For a multivariate lognormal distribution with parameters μ and Σ,
237    /// the mean of component i is exp(μ_i + Σ_ii/2).
238    ///
239    /// # Returns
240    ///
241    /// * Mean vector
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use scirs2_core::ndarray::array;
247    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
248    ///
249    /// let mu = array![0.0, 0.0];
250    /// let sigma = array![[0.5, 0.0], [0.0, 0.5]];
251    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
252    ///
253    /// let mean = mvln.mean();
254    /// ```
255    pub fn mean(&self) -> Array1<f64> {
256        let mut mean = Array1::zeros(self.dim);
257
258        for i in 0..self.dim {
259            mean[i] = (self.mu[i] + self.sigma[[i, i]] / 2.0).exp();
260        }
261
262        mean
263    }
264
265    /// Calculate the median of the distribution.
266    ///
267    /// For a multivariate lognormal distribution with parameters μ and Σ,
268    /// the median of component i is exp(μ_i).
269    ///
270    /// # Returns
271    ///
272    /// * Median vector
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use scirs2_core::ndarray::array;
278    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
279    ///
280    /// let mu = array![0.0, 0.0];
281    /// let sigma = array![[0.5, 0.0], [0.0, 0.5]];
282    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
283    ///
284    /// let median = mvln.median();
285    /// ```
286    pub fn median(&self) -> Array1<f64> {
287        // Median of lognormal is exp(μ)
288        self.mu.mapv(|mu_i| mu_i.exp())
289    }
290
291    /// Calculate the mode of the distribution.
292    ///
293    /// For a multivariate lognormal distribution with parameters μ and Σ,
294    /// the mode of component i is exp(μ_i - Σ_ii).
295    ///
296    /// # Returns
297    ///
298    /// * Mode vector
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use scirs2_core::ndarray::array;
304    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
305    ///
306    /// let mu = array![0.0, 0.0];
307    /// let sigma = array![[0.5, 0.0], [0.0, 0.5]];
308    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
309    ///
310    /// let mode = mvln.mode();
311    /// ```
312    pub fn mode(&self) -> Array1<f64> {
313        let mut mode = Array1::zeros(self.dim);
314
315        for i in 0..self.dim {
316            mode[i] = (self.mu[i] - self.sigma[[i, i]]).exp();
317        }
318
319        mode
320    }
321
322    /// Calculate the covariance matrix of the distribution.
323    ///
324    /// For a multivariate lognormal distribution with parameters μ and Σ,
325    /// the covariance between components i and j is:
326    /// Cov(X_i, X_j) = exp(μ_i + μ_j + (Σ_ii + Σ_jj)/2) * (exp(Σ_ij) - 1)
327    ///
328    /// # Returns
329    ///
330    /// * Covariance matrix
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use scirs2_core::ndarray::array;
336    /// use scirs2_stats::distributions::multivariate::multivariate_lognormal::MultivariateLognormal;
337    ///
338    /// let mu = array![0.0, 0.0];
339    /// let sigma = array![[0.5, 0.2], [0.2, 0.5]];
340    /// let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
341    ///
342    /// let cov = mvln.cov();
343    /// ```
344    pub fn cov(&self) -> Array2<f64> {
345        let mut cov = Array2::zeros((self.dim, self.dim));
346
347        for i in 0..self.dim {
348            for j in 0..self.dim {
349                // For a lognormal, Var(X_i) = exp(2*μ_i + Σ_ii) * (exp(Σ_ii) - 1)
350                // Cov(X_i, X_j) = exp(μ_i + μ_j + (Σ_ii + Σ_jj)/2) * (exp(Σ_ij) - 1)
351
352                // Mean of X_i = exp(μ_i + Σ_ii/2)
353                let mean_i = (self.mu[i] + self.sigma[[i, i]] / 2.0).exp();
354                let mean_j = (self.mu[j] + self.sigma[[j, j]] / 2.0).exp();
355
356                // Formula from Aitchison & Brown (1957)
357                let term = (self.sigma[[i, j]]).exp() - 1.0;
358                cov[[i, j]] = mean_i * mean_j * term;
359            }
360        }
361
362        cov
363    }
364
365    /// Get the dimension of the distribution.
366    pub fn dim(&self) -> usize {
367        self.dim
368    }
369
370    /// Get the mean vector of the underlying normal distribution.
371    pub fn mu(&self) -> ArrayView1<f64> {
372        self.mu.view()
373    }
374
375    /// Get the covariance matrix of the underlying normal distribution.
376    pub fn sigma(&self) -> ArrayView2<f64> {
377        self.sigma.view()
378    }
379}
380
381/// Create a multivariate lognormal distribution with the given parameters.
382///
383/// This is a convenience function to create a multivariate lognormal distribution with
384/// the given mean vector and covariance matrix of the underlying multivariate normal distribution.
385///
386/// # Arguments
387///
388/// * `mu` - Mean vector of the underlying multivariate normal
389/// * `sigma` - Covariance matrix of the underlying multivariate normal
390///
391/// # Returns
392///
393/// * A multivariate lognormal distribution object
394///
395/// # Examples
396///
397/// ```
398/// use scirs2_core::ndarray::array;
399/// use scirs2_stats::distributions::multivariate;
400///
401/// let mu = array![0.0, 0.0];
402/// let sigma = array![[0.5, 0.2], [0.2, 0.5]];
403/// let mvln = multivariate::multivariate_lognormal(mu, sigma).expect("Operation failed");
404/// ```
405#[allow(dead_code)]
406pub fn multivariate_lognormal<D1, D2>(
407    mu: ArrayBase<D1, Ix1>,
408    sigma: ArrayBase<D2, Ix2>,
409) -> StatsResult<MultivariateLognormal>
410where
411    D1: Data<Elem = f64>,
412    D2: Data<Elem = f64>,
413{
414    MultivariateLognormal::new(mu, sigma)
415}
416
417/// Implementation of SampleableDistribution for MultivariateLognormal
418impl SampleableDistribution<Array1<f64>> for MultivariateLognormal {
419    fn rvs(&self, size: usize) -> StatsResult<Vec<Array1<f64>>> {
420        let samples_matrix = self.rvs(size)?;
421        let mut result = Vec::with_capacity(size);
422
423        for i in 0..size {
424            let row = samples_matrix.slice(s![i, ..]).to_owned();
425            result.push(row);
426        }
427
428        Ok(result)
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use approx::assert_relative_eq;
436    use scirs2_core::ndarray::{array, Axis};
437
438    #[test]
439    fn test_mvln_creation() {
440        // 2D multivariate lognormal
441        let mu = array![0.0, 0.0];
442        let sigma = array![[0.5, 0.0], [0.0, 0.5]];
443        let mvln = MultivariateLognormal::new(mu.clone(), sigma.clone()).expect("Operation failed");
444
445        assert_eq!(mvln.dim, 2);
446        assert_eq!(mvln.mu, mu);
447        assert_eq!(mvln.sigma, sigma);
448
449        // 3D multivariate lognormal
450        let mu3 = array![1.0, 2.0, 3.0];
451        let sigma3 = array![[0.5, 0.1, 0.1], [0.1, 0.5, 0.1], [0.1, 0.1, 0.5]];
452        let mvln3 =
453            MultivariateLognormal::new(mu3.clone(), sigma3.clone()).expect("Operation failed");
454
455        assert_eq!(mvln3.dim, 3);
456        assert_eq!(mvln3.mu, mu3);
457        assert_eq!(mvln3.sigma, sigma3);
458    }
459
460    #[test]
461    fn test_mvln_creation_errors() {
462        // Dimension mismatch
463        let mu = array![0.0, 0.0, 0.0];
464        let sigma = array![[0.5, 0.0], [0.0, 0.5]];
465        assert!(MultivariateLognormal::new(mu, sigma).is_err());
466
467        // Non-positive definite covariance
468        let mu = array![0.0, 0.0];
469        let sigma = array![[1.0, 2.0], [2.0, 1.0]]; // Not positive definite
470        assert!(MultivariateLognormal::new(mu, sigma).is_err());
471    }
472
473    #[test]
474    fn test_mvln_pdf() {
475        // 2D independent lognormal
476        let mu = array![0.0, 0.0];
477        let sigma = array![[0.5, 0.0], [0.0, 0.5]];
478        let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
479
480        // PDF at various points
481        let x1 = array![1.0, 1.0];
482        let pdf1 = mvln.pdf(&x1);
483        assert!(pdf1 > 0.0);
484
485        // PDF at a negative point should be 0
486        let x2 = array![-1.0, 1.0];
487        let pdf2 = mvln.pdf(&x2);
488        assert_eq!(pdf2, 0.0);
489
490        // PDF at a zero point should be 0
491        let x3 = array![0.0, 1.0];
492        let pdf3 = mvln.pdf(&x3);
493        assert_eq!(pdf3, 0.0);
494
495        // Wrong dimension
496        let x4 = array![1.0, 1.0, 1.0];
497        let pdf4 = mvln.pdf(&x4);
498        assert_eq!(pdf4, 0.0);
499    }
500
501    #[test]
502    fn test_mvln_logpdf() {
503        // 2D independent lognormal
504        let mu = array![0.0, 0.0];
505        let sigma = array![[0.5, 0.0], [0.0, 0.5]];
506        let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
507
508        // LogPDF at various points
509        let x1 = array![1.0, 1.0];
510        let pdf1 = mvln.pdf(&x1);
511        let logpdf1 = mvln.logpdf(&x1);
512        assert_relative_eq!(logpdf1.exp(), pdf1, epsilon = 1e-10);
513
514        // LogPDF at a negative point should be -inf
515        let x2 = array![-1.0, 1.0];
516        let logpdf2 = mvln.logpdf(&x2);
517        assert_eq!(logpdf2, f64::NEG_INFINITY);
518    }
519
520    #[test]
521    fn test_mvln_statistics() {
522        // Create a 2D lognormal with known parameters
523        let mu = array![0.0, 0.0];
524        let sigma = array![[0.5, 0.0], [0.0, 0.5]];
525        let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
526
527        // Mean
528        let mean = mvln.mean();
529        let expected_mean = array![(0.5_f64 / 2.0).exp(), (0.5_f64 / 2.0).exp()];
530        assert_relative_eq!(mean[0], expected_mean[0], epsilon = 1e-10);
531        assert_relative_eq!(mean[1], expected_mean[1], epsilon = 1e-10);
532
533        // Median
534        let median = mvln.median();
535        let expected_median = array![1.0, 1.0]; // exp(0.0) = 1.0
536        assert_relative_eq!(median[0], expected_median[0], epsilon = 1e-10);
537        assert_relative_eq!(median[1], expected_median[1], epsilon = 1e-10);
538
539        // Mode
540        let mode = mvln.mode();
541        let expected_mode = array![(-0.5_f64).exp(), (-0.5_f64).exp()]; // exp(μ - σ^2)
542        assert_relative_eq!(mode[0], expected_mode[0], epsilon = 1e-10);
543        assert_relative_eq!(mode[1], expected_mode[1], epsilon = 1e-10);
544
545        // Test covariance for a lognormal distribution with independent variables
546        let cov = mvln.cov();
547
548        // For a lognormal with μ = 0, Var(X_i) = mean_i^2 * (exp(Σ_ii) - 1)
549        // where mean_i = exp(Σ_ii/2)
550        let mean_i = (0.5_f64 / 2.0).exp();
551        let var0 = mean_i * mean_i * ((0.5_f64).exp() - 1.0);
552
553        assert_relative_eq!(cov[[0, 0]], var0, epsilon = 1e-10);
554        assert_relative_eq!(cov[[1, 1]], var0, epsilon = 1e-10);
555
556        // Since our Σ has 0 off-diagonal elements (independent case), the covariance is 0
557        assert_relative_eq!(cov[[0, 1]], 0.0, epsilon = 1e-10);
558        assert_relative_eq!(cov[[1, 0]], 0.0, epsilon = 1e-10);
559    }
560
561    #[test]
562    fn test_mvln_rvs() {
563        // Create a 2D lognormal
564        let mu = array![0.0, 0.0];
565        let sigma = array![[0.5, 0.2], [0.2, 0.5]];
566        let mvln = MultivariateLognormal::new(mu, sigma).expect("Operation failed");
567
568        // Generate samples
569        let n_samples_ = 1000;
570        let samples = mvln.rvs(n_samples_).expect("Operation failed");
571        assert_eq!(samples.shape(), &[n_samples_, 2]);
572
573        // Check all samples are positive
574        for i in 0..n_samples_ {
575            for j in 0..2 {
576                assert!(samples[[i, j]] > 0.0);
577            }
578        }
579
580        // Verify sample statistics (rough check due to randomness)
581        // Calculate sample means
582        let sample_mean = samples.mean_axis(Axis(0)).expect("Operation failed");
583        let expected_mean = mvln.mean();
584        assert_relative_eq!(sample_mean[0], expected_mean[0], epsilon = 0.2);
585        assert_relative_eq!(sample_mean[1], expected_mean[1], epsilon = 0.2);
586
587        // Single sample
588        let single_sample = mvln.rvs_single().expect("Operation failed");
589        assert_eq!(single_sample.len(), 2);
590        assert!(single_sample[0] > 0.0);
591        assert!(single_sample[1] > 0.0);
592    }
593}