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}