Skip to main content

causal_hub/random/models/bayesian_network/gaussian/
parameters.rs

1use ndarray::prelude::*;
2use ndarray_linalg::{Cholesky, SVD, UPLO};
3use rand::{Rng, RngExt};
4use rand_distr::Normal;
5
6use crate::{
7    models::{GaussCPD, GaussCPDP},
8    random::Random,
9    types::{Error, Labels, Result},
10};
11
12/// A struct for random Gaussian CPD parameters generation.
13struct RngGaussCPDP<'a, R>
14where
15    R: Rng,
16{
17    rng: &'a mut R,
18    x: usize,
19    z: usize,
20    s_a: f64,
21    s_b: f64,
22    evidence: f64,
23}
24
25impl<'a, R> RngGaussCPDP<'a, R>
26where
27    R: Rng,
28{
29    /// Creates a new `RngGaussCPDP` instance.
30    ///
31    /// # Arguments
32    ///
33    /// * `rng` - A mutable reference to a random number generator.
34    /// * `x` - The number of target variables.
35    /// * `z` - The number of conditioning variables.
36    /// * `s_a` - The standard deviation of the regression coefficients.
37    /// * `s_b` - The standard deviation of the intercept.
38    /// * `e` - A small positive constant for covariance regularization.
39    ///
40    /// # Errors
41    ///
42    /// * If `s_a` is not positive.
43    /// * If `s_b` is not positive.
44    /// * If `e` is not positive.
45    ///
46    /// # Returns
47    ///
48    /// A new `RngGaussCPDP` instance.
49    ///
50    fn new(rng: &'a mut R, x: usize, z: usize, s_a: f64, s_b: f64, evidence: f64) -> Result<Self> {
51        // Check parameters.
52        if s_a <= 0.0 {
53            return Err(Error::InvalidParameter("s_a", "must be positive"));
54        }
55        if s_b <= 0.0 {
56            return Err(Error::InvalidParameter("s_b", "must be positive"));
57        }
58        if evidence <= 0.0 {
59            return Err(Error::InvalidParameter("e", "must be positive"));
60        }
61
62        Ok(Self {
63            rng,
64            x,
65            z,
66            s_a,
67            s_b,
68            evidence,
69        })
70    }
71}
72
73impl<R> Random for RngGaussCPDP<'_, R>
74where
75    R: Rng,
76{
77    type Output = Result<GaussCPDP>;
78
79    fn random(&mut self) -> Self::Output {
80        // 1. Generate coefficient matrix A (x x z)
81        let mut a = if self.x > 0 && self.z > 0 {
82            let dist_a = Normal::new(0.0, self.s_a)
83                .map_err(|evidence| Error::InvalidParameter("s_a", &evidence.to_string()))?;
84            Array2::from_shape_fn((self.x, self.z), |_| self.rng.sample(dist_a))
85        } else {
86            Array2::zeros((self.x, self.z))
87        };
88
89        // 2. Control regression strength: spectral norm ||A||
90        if self.x > 0 && self.z > 0 {
91            let (_, stats, _) = a
92                .svd(false, false)
93                .map_err(|evidence| Error::Linalg(&format!("Failed to compute SVD: {evidence}")))?;
94            let spectral_norm = stats[0];
95            if spectral_norm > 1.0 {
96                a /= spectral_norm;
97            }
98        }
99
100        // 3. Generate intercept vector b (x)
101        let b = if self.x > 0 {
102            let dist_b = Normal::new(0.0, self.s_b)
103                .map_err(|evidence| Error::InvalidParameter("s_b", &evidence.to_string()))?;
104            Array1::from_shape_fn(self.x, |_| self.rng.sample(dist_b))
105        } else {
106            Array1::zeros(self.x)
107        };
108
109        // 4. Generate covariance matrix Sigma (x x x)
110        let stats = if self.x > 0 {
111            let mut stats;
112            // Create the Normal distribution.
113            let dist_m = Normal::new(0.0, 1.0)
114                .map_err(|evidence| Error::InvalidParameter("sigma", &evidence.to_string()))?;
115            loop {
116                // Sample random matrix M (x x x)
117                let model = Array2::from_shape_fn((self.x, self.x), |_| self.rng.sample(dist_m));
118
119                // Compute Sigma = M * M^T
120                stats = model.dot(&model.t());
121
122                // Regularize: Sigma = Sigma + e * I
123                for i in 0..self.x {
124                    stats[[i, i]] += self.evidence;
125                }
126
127                // 5. Validation step: positive definite
128                if stats.cholesky(UPLO::Lower).is_ok() {
129                    break;
130                }
131            }
132            stats
133        } else {
134            Array2::zeros((self.x, self.x))
135        };
136
137        // 6. Construct GaussCPDP
138        GaussCPDP::new(a, b, stats)
139    }
140}
141
142/// A struct for random Gaussian CPD generation.
143pub struct RngGaussCPD<'a, R>
144where
145    R: Rng,
146{
147    rng: &'a mut R,
148    labels: &'a Labels,
149    conditioning_labels: &'a Labels,
150    s_a: f64,
151    s_b: f64,
152    evidence: f64,
153}
154
155impl<'a, R> RngGaussCPD<'a, R>
156where
157    R: Rng,
158{
159    /// Creates a new `RngGaussCPD` instance.
160    ///
161    /// # Arguments
162    ///
163    /// * `rng` - A mutable reference to a random number generator.
164    /// * `labels` - The labels of the target variables.
165    /// * `conditioning_labels` - The labels of the conditioning variables.
166    /// * `s_a` - The standard deviation of the regression coefficients.
167    /// * `s_b` - The standard deviation of the intercept.
168    /// * `e` - A small positive constant for covariance regularization.
169    ///
170    /// # Returns
171    ///
172    /// A new `RngGaussCPD` instance.
173    ///
174    pub fn new(
175        rng: &'a mut R,
176        labels: &'a Labels,
177        conditioning_labels: &'a Labels,
178        s_a: f64,
179        s_b: f64,
180        evidence: f64,
181    ) -> Result<Self> {
182        Ok(Self {
183            rng,
184            labels,
185            conditioning_labels,
186            s_a,
187            s_b,
188            evidence,
189        })
190    }
191}
192
193impl<R> Random for RngGaussCPD<'_, R>
194where
195    R: Rng,
196{
197    type Output = Result<GaussCPD>;
198
199    fn random(&mut self) -> Self::Output {
200        // Generate parameters
201        let mut rng_params = RngGaussCPDP::new(
202            self.rng,
203            self.labels.len(),
204            self.conditioning_labels.len(),
205            self.s_a,
206            self.s_b,
207            self.evidence,
208        )?;
209        let parameters = rng_params.random()?;
210
211        // Construct GaussCPD
212        GaussCPD::new(
213            self.labels.clone(),
214            self.conditioning_labels.clone(),
215            parameters,
216        )
217    }
218}