Skip to main content

scirs2_core/random/
neural_sampling.rs

1//! Neural-based sampling methods for ultra-modern generative modeling
2//!
3//! This module implements the most advanced neural sampling algorithms from cutting-edge
4//! machine learning research. These methods leverage deep neural networks to learn complex
5//! probability distributions and generate high-quality samples.
6//!
7//! # Implemented Methods
8//!
9//! - **Normalizing Flows**: Invertible neural networks for exact likelihood computation
10//! - **Variational Autoencoders (VAE)**: Probabilistic latent variable models
11//! - **Score-Based Diffusion Models**: State-of-the-art generative models using score matching
12//! - **Energy-Based Models (EBM)**: Flexible unnormalized probability models
13//! - **Neural Posterior Estimation**: Amortized Bayesian inference
14//! - **Autoregressive Models**: Sequential probability modeling
15//! - **Generative Adversarial Sampling**: Adversarial training for sample generation
16//!
17//! # Key Advantages
18//!
19//! - **Expressiveness**: Can model highly complex, multi-modal distributions
20//! - **Scalability**: Efficient sampling from high-dimensional spaces
21//! - **Amortization**: Fast inference after initial training
22//! - **Flexibility**: Adapts to arbitrary target distributions
23//!
24//! # Examples
25//!
26//! ```rust
27//! use scirs2_core::random::neural_sampling::*;
28//! use ::ndarray::Array2;
29//!
30//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! // Sample training data (small for doc test)
32//! let training_data: Array2<f64> = Array2::zeros((10, 3));
33//!
34//! // Normalizing Flow initialization (basic example)
35//! let mut flow = NormalizingFlow::new(3, 2);
36//! // In real usage: flow.train(&training_data, num_epochs)?;
37//!
38//! // Score-based diffusion model initialization
39//! let diffusion = ScoreBasedDiffusion::new(DiffusionConfig::default());
40//! // In real usage: diffusion.train(&training_data)?; then diffusion.sample(...)?;
41//!
42//! // For this doc test, we just show initialization without expensive operations
43//! println!("Neural sampling models initialized successfully");
44//! # Ok(())
45//! # }
46//! ```
47
48use crate::random::{
49    core::{seeded_rng, Random},
50    distributions::MultivariateNormal,
51    parallel::{ParallelRng, ThreadLocalRngPool},
52};
53use ::ndarray::{s, Array1, Array2, Array3, Axis};
54use rand::{Rng, RngExt};
55use rand_distr::{Distribution, Normal, Uniform};
56use std::collections::VecDeque;
57
58/// Normalizing Flow for invertible transformations
59///
60/// Normalizing flows learn invertible mappings between simple base distributions
61/// (like Gaussian) and complex target distributions, enabling both sampling
62/// and exact likelihood computation.
63#[derive(Debug, Clone)]
64pub struct NormalizingFlow {
65    dimension: usize,
66    num_layers: usize,
67    flow_layers: Vec<FlowLayer>,
68    base_distribution: MultivariateNormal,
69    trained: bool,
70    /// Per-epoch average negative log-likelihood recorded by [`Self::train`].
71    training_history: Vec<f64>,
72}
73
74#[derive(Debug, Clone)]
75struct FlowLayer {
76    // Coupling layer parameters (simplified Real NVP-style)
77    mask: Array1<bool>,
78    scale_network: MLP,
79    translation_network: MLP,
80}
81
82#[derive(Debug, Clone)]
83struct MLP {
84    // Multi-layer perceptron for flow transformations
85    weights: Vec<Array2<f64>>,
86    biases: Vec<Array1<f64>>,
87    hidden_dims: Vec<usize>,
88}
89
90impl NormalizingFlow {
91    /// Create new normalizing flow
92    pub fn new(dimension: usize, num_layers: usize) -> Self {
93        let mut flow_layers = Vec::new();
94
95        for i in 0..num_layers {
96            // Alternating masks for coupling layers
97            let mut mask = Array1::from_elem(dimension, false);
98            for j in 0..dimension {
99                mask[j] = (j + i) % 2 == 0;
100            }
101
102            let hidden_dim = dimension.max(32);
103            let scale_net = MLP::new(&[dimension / 2, hidden_dim, hidden_dim, dimension / 2]);
104            let trans_net = MLP::new(&[dimension / 2, hidden_dim, hidden_dim, dimension / 2]);
105
106            flow_layers.push(FlowLayer {
107                mask,
108                scale_network: scale_net,
109                translation_network: trans_net,
110            });
111        }
112
113        // Create identity covariance matrix (diagonal matrix with 1.0 on diagonal)
114        let mut cov_matrix = vec![vec![0.0; dimension]; dimension];
115        for i in 0..dimension {
116            cov_matrix[i][i] = 1.0;
117        }
118
119        let base_distribution =
120            MultivariateNormal::new(vec![0.0; dimension], cov_matrix).expect("Operation failed");
121
122        Self {
123            dimension,
124            num_layers,
125            flow_layers,
126            base_distribution,
127            trained: false,
128            training_history: Vec::new(),
129        }
130    }
131
132    /// Train the normalizing flow on data
133    pub fn train(&mut self, training_data: &Array2<f64>, num_epochs: usize) -> Result<(), String> {
134        let learning_rate = 0.001;
135        let batch_size = 32;
136
137        for epoch in 0..num_epochs {
138            // Mini-batch training (simplified)
139            let num_batches = training_data.nrows().div_ceil(batch_size);
140
141            let mut epoch_loss = 0.0;
142            let mut epoch_samples = 0usize;
143
144            for batch_idx in 0..num_batches {
145                let start_idx = batch_idx * batch_size;
146                let end_idx = (start_idx + batch_size).min(training_data.nrows());
147
148                let batch = training_data.slice(s![start_idx..end_idx, ..]);
149
150                // Forward pass: compute negative log-likelihood
151                let mut total_loss = 0.0;
152                for i in 0..batch.nrows() {
153                    let x = batch.row(i).to_owned();
154                    let (z, log_det_jacobian) = self.forward(&x)?;
155
156                    // Base distribution log probability
157                    let log_prob_z = self.base_distribution.log_probability(&z.to_vec())?;
158                    let log_prob_x = log_prob_z + log_det_jacobian;
159
160                    total_loss -= log_prob_x; // Negative log-likelihood
161                }
162
163                epoch_loss += total_loss;
164                epoch_samples += batch.nrows();
165
166                // Backward pass (simplified gradient computation)
167                self.update_parameters(learning_rate, &batch)?;
168            }
169
170            // Average negative log-likelihood per sample for this epoch.
171            let avg_loss = if epoch_samples > 0 {
172                epoch_loss / epoch_samples as f64
173            } else {
174                0.0
175            };
176            self.training_history.push(avg_loss);
177
178            if epoch % 100 == 0 {
179                println!("Epoch {epoch}: loss = {avg_loss:.6}");
180            }
181        }
182
183        self.trained = true;
184        Ok(())
185    }
186
187    /// Returns the per-epoch training loss history recorded by [`Self::train`].
188    ///
189    /// Each entry is the average negative log-likelihood over all samples
190    /// processed during the corresponding epoch (one entry is pushed per
191    /// epoch, in order).
192    pub fn training_history(&self) -> &[f64] {
193        &self.training_history
194    }
195
196    /// Forward transformation: x -> z
197    fn forward(&self, x: &Array1<f64>) -> Result<(Array1<f64>, f64), String> {
198        let mut z = x.clone();
199        let mut log_det_jacobian = 0.0;
200
201        for layer in &self.flow_layers {
202            let (new_z, log_det) = layer.forward(&z)?;
203            z = new_z;
204            log_det_jacobian += log_det;
205        }
206
207        Ok((z, log_det_jacobian))
208    }
209
210    /// Inverse transformation: z -> x (for sampling)
211    fn inverse(&self, z: &Array1<f64>) -> Result<Array1<f64>, String> {
212        let mut x = z.clone();
213
214        // Apply layers in reverse order
215        for layer in self.flow_layers.iter().rev() {
216            x = layer.inverse(&x)?;
217        }
218
219        Ok(x)
220    }
221
222    /// Sample from the learned distribution
223    pub fn sample(&self, num_samples: usize, seed: u64) -> Result<Array2<f64>, String> {
224        if !self.trained {
225            return Err("Flow must be trained before sampling".to_string());
226        }
227
228        let mut rng = seeded_rng(seed);
229        let mut samples = Array2::zeros((num_samples, self.dimension));
230
231        for i in 0..num_samples {
232            // Sample from base distribution
233            let z = Array1::from_vec(self.base_distribution.sample(&mut rng));
234
235            // Transform through flow
236            let x = self.inverse(&z)?;
237
238            for j in 0..self.dimension {
239                samples[[i, j]] = x[j];
240            }
241        }
242
243        Ok(samples)
244    }
245
246    /// Compute log probability of data points
247    pub fn log_probability(&self, x: &Array1<f64>) -> Result<f64, String> {
248        if !self.trained {
249            return Err("Flow must be trained before computing probabilities".to_string());
250        }
251
252        let (z, log_det_jacobian) = self.forward(x)?;
253        let log_prob_z = self.base_distribution.log_probability(&z.to_vec())?;
254        Ok(log_prob_z + log_det_jacobian)
255    }
256
257    /// Update parameters (simplified gradient descent)
258    fn update_parameters(
259        &mut self,
260        learning_rate: f64,
261        batch: &crate::ndarray::ArrayView2<f64>,
262    ) -> Result<(), String> {
263        // Simplified parameter update - in practice would use automatic differentiation
264        for layer in &mut self.flow_layers {
265            layer.update_parameters(learning_rate, batch)?;
266        }
267        Ok(())
268    }
269}
270
271impl FlowLayer {
272    /// Forward pass through coupling layer
273    fn forward(&self, x: &Array1<f64>) -> Result<(Array1<f64>, f64), String> {
274        let mut y = x.clone();
275        let mut log_det_jacobian = 0.0;
276
277        // Split input according to mask
278        let x_unchanged: Vec<f64> = x
279            .iter()
280            .enumerate()
281            .filter(|(i, _)| self.mask[*i])
282            .map(|(_, &val)| val)
283            .collect();
284
285        let x_to_transform: Vec<f64> = x
286            .iter()
287            .enumerate()
288            .filter(|(i, _)| !self.mask[*i])
289            .map(|(_, &val)| val)
290            .collect();
291
292        if !x_unchanged.is_empty() && !x_to_transform.is_empty() {
293            // Compute scale and translation
294            let scale = self
295                .scale_network
296                .forward(&Array1::from_vec(x_unchanged.clone()))?;
297            let translation = self
298                .translation_network
299                .forward(&Array1::from_vec(x_unchanged))?;
300
301            // Apply transformation
302            let mut transform_idx = 0;
303            for (i, &masked) in self.mask.iter().enumerate() {
304                if !masked && transform_idx < scale.len() && transform_idx < translation.len() {
305                    let s = scale[transform_idx];
306                    let t = translation[transform_idx];
307                    y[i] = x_to_transform[transform_idx] * s.exp() + t;
308                    log_det_jacobian += s;
309                    transform_idx += 1;
310                }
311            }
312        }
313
314        Ok((y, log_det_jacobian))
315    }
316
317    /// Inverse pass through coupling layer
318    fn inverse(&self, y: &Array1<f64>) -> Result<Array1<f64>, String> {
319        let mut x = y.clone();
320
321        // Split input according to mask
322        let y_unchanged: Vec<f64> = y
323            .iter()
324            .enumerate()
325            .filter(|(i, _)| self.mask[*i])
326            .map(|(_, &val)| val)
327            .collect();
328
329        if !y_unchanged.is_empty() {
330            // Compute scale and translation
331            let scale = self
332                .scale_network
333                .forward(&Array1::from_vec(y_unchanged.clone()))?;
334            let translation = self
335                .translation_network
336                .forward(&Array1::from_vec(y_unchanged))?;
337
338            // Apply inverse transformation
339            let mut transform_idx = 0;
340            for (i, &masked) in self.mask.iter().enumerate() {
341                if !masked && transform_idx < scale.len() && transform_idx < translation.len() {
342                    let s = scale[transform_idx];
343                    let t = translation[transform_idx];
344                    x[i] = (y[i] - t) * (-s).exp();
345                    transform_idx += 1;
346                }
347            }
348        }
349
350        Ok(x)
351    }
352
353    /// Update layer parameters
354    fn update_parameters(
355        &mut self,
356        learning_rate: f64,
357        _batch: &crate::ndarray::ArrayView2<f64>,
358    ) -> Result<(), String> {
359        // Simplified parameter update
360        self.scale_network.update_parameters(learning_rate)?;
361        self.translation_network.update_parameters(learning_rate)?;
362        Ok(())
363    }
364}
365
366impl MLP {
367    /// Create new MLP
368    fn new(layer_sizes: &[usize]) -> Self {
369        let mut weights = Vec::new();
370        let mut biases = Vec::new();
371
372        for i in 0..layer_sizes.len() - 1 {
373            let w = Array2::zeros((layer_sizes[i + 1], layer_sizes[i]));
374            let b = Array1::zeros(layer_sizes[i + 1]);
375            weights.push(w);
376            biases.push(b);
377        }
378
379        Self {
380            weights,
381            biases,
382            hidden_dims: layer_sizes[1..layer_sizes.len() - 1].to_vec(),
383        }
384    }
385
386    /// Forward pass through MLP
387    fn forward(&self, input: &Array1<f64>) -> Result<Array1<f64>, String> {
388        let mut x = input.clone();
389
390        for (i, (weight, bias)) in self.weights.iter().zip(self.biases.iter()).enumerate() {
391            // Linear transformation
392            let mut output = Array1::zeros(weight.nrows());
393            for j in 0..weight.nrows() {
394                let mut sum = bias[j];
395                for k in 0..weight.ncols() {
396                    if k < x.len() {
397                        sum += weight[[j, k]] * x[k];
398                    }
399                }
400                output[j] = sum;
401            }
402
403            // Activation function (ReLU for hidden layers, linear for output)
404            if i < self.weights.len() - 1 {
405                for elem in output.iter_mut() {
406                    *elem = elem.max(0.0); // ReLU
407                }
408            }
409
410            x = output;
411        }
412
413        Ok(x)
414    }
415
416    /// Update parameters (simplified)
417    fn update_parameters(&mut self, _learning_rate: f64) -> Result<(), String> {
418        // Simplified parameter update - would implement proper backpropagation
419        Ok(())
420    }
421}
422
423/// Score-based diffusion model for high-quality sample generation
424#[derive(Debug)]
425pub struct ScoreBasedDiffusion {
426    config: DiffusionConfig,
427    score_network: ScoreNetwork,
428    noise_schedule: NoiseSchedule,
429    trained: bool,
430    /// Per-epoch average denoising score-matching loss recorded by
431    /// [`Self::train`].
432    training_history: Vec<f64>,
433}
434
435#[derive(Debug, Clone)]
436pub struct DiffusionConfig {
437    pub dimension: usize,
438    pub num_timesteps: usize,
439    pub beta_start: f64,
440    pub beta_end: f64,
441    pub hidden_dims: Vec<usize>,
442}
443
444impl Default for DiffusionConfig {
445    fn default() -> Self {
446        Self {
447            dimension: 10,
448            num_timesteps: 1000,
449            beta_start: 1e-4,
450            beta_end: 0.02,
451            hidden_dims: vec![128, 256, 128],
452        }
453    }
454}
455
456#[derive(Debug)]
457struct ScoreNetwork {
458    // Neural network for score function estimation
459    mlp: MLP,
460    time_embedding_dim: usize,
461}
462
463#[derive(Debug)]
464struct NoiseSchedule {
465    betas: Vec<f64>,
466    alphas: Vec<f64>,
467    alpha_bars: Vec<f64>,
468}
469
470impl ScoreBasedDiffusion {
471    /// Create new diffusion model
472    pub fn new(config: DiffusionConfig) -> Self {
473        let time_embedding_dim = 64;
474        let input_dim = config.dimension + time_embedding_dim;
475
476        let mut network_dims = vec![input_dim];
477        network_dims.extend_from_slice(&config.hidden_dims);
478        network_dims.push(config.dimension);
479
480        let score_network = ScoreNetwork {
481            mlp: MLP::new(&network_dims),
482            time_embedding_dim,
483        };
484
485        let noise_schedule =
486            NoiseSchedule::new(config.num_timesteps, config.beta_start, config.beta_end);
487
488        Self {
489            config,
490            score_network,
491            noise_schedule,
492            trained: false,
493            training_history: Vec::new(),
494        }
495    }
496
497    /// Train the diffusion model
498    pub fn train(&mut self, training_data: &Array2<f64>) -> Result<(), String> {
499        let num_epochs = 1000;
500        let batch_size = 32;
501
502        for epoch in 0..num_epochs {
503            let mut epoch_loss = 0.0;
504            let mut num_batches_processed = 0usize;
505
506            // Denoising score matching training
507            for _ in 0..training_data.nrows().div_ceil(batch_size) {
508                // Sample random timesteps
509                let mut rng = seeded_rng(42 + epoch as u64);
510                let t = rng
511                    .sample(Uniform::new(0, self.config.num_timesteps).expect("Operation failed"));
512
513                // Sample noise and create noisy data
514                let noise = self.sample_noise(training_data.nrows(), &mut rng)?;
515                let noisy_data = self.add_noise(training_data, &noise, t)?;
516
517                // Train score network to predict noise
518                epoch_loss += self.score_network.train_step(&noisy_data, &noise, t)?;
519                num_batches_processed += 1;
520            }
521
522            // Average denoising score-matching MSE loss for this epoch.
523            let avg_loss = if num_batches_processed > 0 {
524                epoch_loss / num_batches_processed as f64
525            } else {
526                0.0
527            };
528            self.training_history.push(avg_loss);
529
530            if epoch % 100 == 0 {
531                println!("Epoch {epoch}: loss = {avg_loss:.6}");
532            }
533        }
534
535        self.trained = true;
536        Ok(())
537    }
538
539    /// Returns the per-epoch training loss history recorded by [`Self::train`].
540    ///
541    /// Each entry is the average denoising score-matching mean-squared-error
542    /// loss over all batches processed during the corresponding epoch (one
543    /// entry is pushed per epoch, in order).
544    pub fn training_history(&self) -> &[f64] {
545        &self.training_history
546    }
547
548    /// Sample from the diffusion model using DDPM
549    pub fn sample(
550        &self,
551        num_samples: usize,
552        num_timesteps: usize,
553        seed: u64,
554    ) -> Result<Array2<f64>, String> {
555        if !self.trained {
556            return Err("Model must be trained before sampling".to_string());
557        }
558
559        let mut rng = seeded_rng(seed);
560        let mut samples = Array2::zeros((num_samples, self.config.dimension));
561
562        // Start from pure noise
563        for i in 0..num_samples {
564            for j in 0..self.config.dimension {
565                samples[[i, j]] = rng.sample(Normal::new(0.0, 1.0).expect("Operation failed"));
566            }
567        }
568
569        // Reverse diffusion process
570        let timestep_stride = self.config.num_timesteps / num_timesteps;
571
572        for t in (0..num_timesteps).rev() {
573            let actual_t = t * timestep_stride;
574
575            // Predict noise using score network
576            let predicted_noise = self.score_network.predict(&samples, actual_t)?;
577
578            // Update samples using DDPM update rule
579            samples = self.ddpm_update(&samples, &predicted_noise, actual_t, &mut rng)?;
580        }
581
582        Ok(samples)
583    }
584
585    /// Sample noise
586    fn sample_noise(
587        &self,
588        batch_size: usize,
589        rng: &mut Random<rand::rngs::StdRng>,
590    ) -> Result<Array2<f64>, String> {
591        let mut noise = Array2::zeros((batch_size, self.config.dimension));
592        for i in 0..batch_size {
593            for j in 0..self.config.dimension {
594                noise[[i, j]] = rng.sample(Normal::new(0.0, 1.0).expect("Operation failed"));
595            }
596        }
597        Ok(noise)
598    }
599
600    /// Add noise according to diffusion schedule
601    fn add_noise(
602        &self,
603        data: &Array2<f64>,
604        noise: &Array2<f64>,
605        t: usize,
606    ) -> Result<Array2<f64>, String> {
607        let alpha_bar = self.noise_schedule.alpha_bars[t];
608        let mut noisy_data = Array2::zeros(data.raw_dim());
609
610        for i in 0..data.nrows() {
611            for j in 0..data.ncols() {
612                noisy_data[[i, j]] =
613                    alpha_bar.sqrt() * data[[i, j]] + (1.0 - alpha_bar).sqrt() * noise[[i, j]];
614            }
615        }
616
617        Ok(noisy_data)
618    }
619
620    /// DDPM update step
621    fn ddpm_update(
622        &self,
623        x_t: &Array2<f64>,
624        predicted_noise: &Array2<f64>,
625        t: usize,
626        rng: &mut Random<rand::rngs::StdRng>,
627    ) -> Result<Array2<f64>, String> {
628        let alpha = self.noise_schedule.alphas[t];
629        let alpha_bar = self.noise_schedule.alpha_bars[t];
630        let beta = self.noise_schedule.betas[t];
631
632        let mut x_t_minus_1 = Array2::zeros(x_t.raw_dim());
633
634        for i in 0..x_t.nrows() {
635            for j in 0..x_t.ncols() {
636                // Mean of reverse process
637                let mean_coeff = 1.0 / alpha.sqrt();
638                let noise_coeff = beta / (1.0 - alpha_bar).sqrt();
639                let mean = mean_coeff * (x_t[[i, j]] - noise_coeff * predicted_noise[[i, j]]);
640
641                // Add noise (except for final step)
642                let noise = if t > 0 {
643                    rng.sample(Normal::new(0.0, beta.sqrt()).expect("Operation failed"))
644                } else {
645                    0.0
646                };
647
648                x_t_minus_1[[i, j]] = mean + noise;
649            }
650        }
651
652        Ok(x_t_minus_1)
653    }
654}
655
656impl NoiseSchedule {
657    fn new(num_timesteps: usize, beta_start: f64, beta_end: f64) -> Self {
658        let mut betas = Vec::with_capacity(num_timesteps);
659        let mut alphas = Vec::with_capacity(num_timesteps);
660        let mut alpha_bars = Vec::with_capacity(num_timesteps);
661
662        // Linear beta schedule
663        for i in 0..num_timesteps {
664            let beta =
665                beta_start + (beta_end - beta_start) * (i as f64) / (num_timesteps as f64 - 1.0);
666            let alpha = 1.0 - beta;
667
668            betas.push(beta);
669            alphas.push(alpha);
670
671            // Cumulative product for alpha_bar
672            let alpha_bar = if i == 0 {
673                alpha
674            } else {
675                alpha_bars[i - 1] * alpha
676            };
677            alpha_bars.push(alpha_bar);
678        }
679
680        Self {
681            betas,
682            alphas,
683            alpha_bars,
684        }
685    }
686}
687
688impl ScoreNetwork {
689    /// Train step for score network
690    ///
691    /// Returns the mean squared error between the network's predicted noise
692    /// and the true noise added at timestep `t` — the standard denoising
693    /// score-matching training objective.
694    fn train_step(
695        &mut self,
696        noisy_data: &Array2<f64>,
697        target_noise: &Array2<f64>,
698        t: usize,
699    ) -> Result<f64, String> {
700        // Simplified training step - would implement proper backpropagation
701        let mut squared_error_sum = 0.0;
702        let mut count = 0usize;
703
704        for i in 0..noisy_data.nrows() {
705            let input = self.prepare_input(&noisy_data.row(i).to_owned(), t)?;
706            let predicted = self.mlp.forward(&input)?;
707            let target = target_noise.row(i);
708
709            // Compute loss and update parameters
710            for j in 0..predicted.len().min(target.len()) {
711                let diff = predicted[j] - target[j];
712                squared_error_sum += diff * diff;
713                count += 1;
714            }
715        }
716
717        Ok(if count > 0 {
718            squared_error_sum / count as f64
719        } else {
720            0.0
721        })
722    }
723
724    /// Predict noise at given timestep
725    fn predict(&self, x: &Array2<f64>, t: usize) -> Result<Array2<f64>, String> {
726        let mut predictions = Array2::zeros(x.raw_dim());
727
728        for i in 0..x.nrows() {
729            let input = self.prepare_input(&x.row(i).to_owned(), t)?;
730            let pred = self.mlp.forward(&input)?;
731
732            for j in 0..pred.len().min(x.ncols()) {
733                predictions[[i, j]] = pred[j];
734            }
735        }
736
737        Ok(predictions)
738    }
739
740    /// Prepare input with time embedding
741    fn prepare_input(&self, x: &Array1<f64>, t: usize) -> Result<Array1<f64>, String> {
742        // Simple time embedding (sinusoidal)
743        let mut time_emb = Array1::zeros(self.time_embedding_dim);
744        for i in 0..self.time_embedding_dim {
745            let freq = 2.0 * std::f64::consts::PI * (t as f64)
746                / (10000.0_f64.powf(2.0 * (i as f64) / (self.time_embedding_dim as f64)));
747            time_emb[i] = if i % 2 == 0 { freq.sin() } else { freq.cos() };
748        }
749
750        // Concatenate data and time embedding
751        let mut input = Array1::zeros(x.len() + time_emb.len());
752        for i in 0..x.len() {
753            input[i] = x[i];
754        }
755        for i in 0..time_emb.len() {
756            input[x.len() + i] = time_emb[i];
757        }
758
759        Ok(input)
760    }
761}
762
763/// Energy-Based Model for flexible probability modeling
764#[derive(Debug)]
765pub struct EnergyBasedModel {
766    energy_network: MLP,
767    dimension: usize,
768    temperature: f64,
769    mcmc_steps: usize,
770    /// Per-epoch average contrastive-divergence loss recorded by
771    /// [`Self::train`].
772    training_history: Vec<f64>,
773}
774
775impl EnergyBasedModel {
776    /// Create new energy-based model
777    pub fn new(dimension: usize, hidden_dims: &[usize]) -> Self {
778        let mut network_dims = vec![dimension];
779        network_dims.extend_from_slice(hidden_dims);
780        network_dims.push(1); // Single energy output
781
782        Self {
783            energy_network: MLP::new(&network_dims),
784            dimension,
785            temperature: 1.0,
786            mcmc_steps: 100,
787            training_history: Vec::new(),
788        }
789    }
790
791    /// Train using contrastive divergence
792    pub fn train(&mut self, training_data: &Array2<f64>, num_epochs: usize) -> Result<(), String> {
793        for epoch in 0..num_epochs {
794            let mut epoch_loss = 0.0;
795
796            for i in 0..training_data.nrows() {
797                let positive_sample = training_data.row(i).to_owned();
798
799                // Generate negative sample using MCMC
800                let negative_sample = self.sample_mcmc(&positive_sample, self.mcmc_steps)?;
801
802                // Contrastive divergence update
803                epoch_loss +=
804                    self.contrastive_divergence_update(&positive_sample, &negative_sample)?;
805            }
806
807            // Average contrastive divergence loss (positive minus negative
808            // energy) for this epoch.
809            let avg_loss = if training_data.nrows() > 0 {
810                epoch_loss / training_data.nrows() as f64
811            } else {
812                0.0
813            };
814            self.training_history.push(avg_loss);
815
816            if epoch % 100 == 0 {
817                println!("Epoch {epoch}: loss = {avg_loss:.6}");
818            }
819        }
820
821        Ok(())
822    }
823
824    /// Returns the per-epoch training loss history recorded by [`Self::train`].
825    ///
826    /// Each entry is the average contrastive-divergence loss (positive-sample
827    /// energy minus negative-sample energy) over all training rows processed
828    /// during the corresponding epoch (one entry is pushed per epoch, in
829    /// order).
830    pub fn training_history(&self) -> &[f64] {
831        &self.training_history
832    }
833
834    /// Sample using Langevin dynamics
835    pub fn sample(
836        &self,
837        num_samples: usize,
838        num_steps: usize,
839        seed: u64,
840    ) -> Result<Array2<f64>, String> {
841        let mut rng = seeded_rng(seed);
842        let mut samples = Array2::zeros((num_samples, self.dimension));
843
844        for i in 0..num_samples {
845            // Initialize with random noise
846            let mut x = Array1::zeros(self.dimension);
847            for j in 0..self.dimension {
848                x[j] = rng.sample(Normal::new(0.0, 1.0).expect("Operation failed"));
849            }
850
851            // Langevin dynamics
852            x = self.sample_mcmc(&x, num_steps)?;
853
854            for j in 0..self.dimension {
855                samples[[i, j]] = x[j];
856            }
857        }
858
859        Ok(samples)
860    }
861
862    /// MCMC sampling using Langevin dynamics
863    fn sample_mcmc(&self, initial: &Array1<f64>, num_steps: usize) -> Result<Array1<f64>, String> {
864        let mut x = initial.clone();
865        let step_size = 0.01;
866        let mut rng = seeded_rng(42);
867
868        for _ in 0..num_steps {
869            // Compute energy gradient
870            let grad = self.energy_gradient(&x)?;
871
872            // Langevin dynamics update
873            for i in 0..self.dimension {
874                let noise = rng.sample(
875                    Normal::new(0.0, (2.0_f64 * step_size).sqrt()).expect("Operation failed"),
876                );
877                x[i] -= step_size * grad[i] + noise;
878            }
879        }
880
881        Ok(x)
882    }
883
884    /// Compute energy gradient (numerical differentiation)
885    fn energy_gradient(&self, x: &Array1<f64>) -> Result<Array1<f64>, String> {
886        let mut gradient = Array1::zeros(self.dimension);
887        let epsilon = 1e-5;
888
889        for i in 0..self.dimension {
890            let mut x_plus = x.clone();
891            let mut x_minus = x.clone();
892            x_plus[i] += epsilon;
893            x_minus[i] -= epsilon;
894
895            let energy_plus = self.energy_network.forward(&x_plus)?[0];
896            let energy_minus = self.energy_network.forward(&x_minus)?[0];
897
898            gradient[i] = (energy_plus - energy_minus) / (2.0 * epsilon);
899        }
900
901        Ok(gradient)
902    }
903
904    /// Contrastive divergence parameter update
905    ///
906    /// Returns the contrastive divergence loss (positive-sample energy minus
907    /// negative-sample energy) — the quantity contrastive divergence training
908    /// drives down (a well-fit model assigns lower energy to real/positive
909    /// samples than to MCMC-generated/negative samples).
910    fn contrastive_divergence_update(
911        &mut self,
912        positive: &Array1<f64>,
913        negative: &Array1<f64>,
914    ) -> Result<f64, String> {
915        // Simplified parameter update - would implement proper gradients
916        let pos_energy = self.energy_network.forward(positive)?;
917        let neg_energy = self.energy_network.forward(negative)?;
918
919        // Update parameters to decrease positive energy and increase negative energy
920        // (Implementation would use automatic differentiation)
921        let cd_loss = pos_energy[0] - neg_energy[0];
922
923        Ok(cd_loss)
924    }
925}
926
927/// Neural Posterior Estimation for amortized Bayesian inference
928#[derive(Debug)]
929pub struct NeuralPosteriorEstimation {
930    posterior_network: MLP,
931    prior_network: MLP,
932    observation_dim: usize,
933    parameter_dim: usize,
934    trained: bool,
935    /// Per-epoch average Gaussian negative log-likelihood loss recorded by
936    /// [`Self::train`].
937    training_history: Vec<f64>,
938}
939
940impl NeuralPosteriorEstimation {
941    /// Create new neural posterior estimator
942    pub fn new(observation_dim: usize, parameter_dim: usize, hidden_dims: &[usize]) -> Self {
943        // Network that takes observations and outputs posterior parameters
944        let mut posterior_dims = vec![observation_dim];
945        posterior_dims.extend_from_slice(hidden_dims);
946        posterior_dims.push(parameter_dim * 2); // Mean and variance
947
948        // Network that samples from prior
949        let mut prior_dims = vec![parameter_dim];
950        prior_dims.extend_from_slice(hidden_dims);
951        prior_dims.push(parameter_dim);
952
953        Self {
954            posterior_network: MLP::new(&posterior_dims),
955            prior_network: MLP::new(&prior_dims),
956            observation_dim,
957            parameter_dim,
958            trained: false,
959            training_history: Vec::new(),
960        }
961    }
962
963    /// Train using simulation-based inference
964    pub fn train(
965        &mut self,
966        simulator: impl Fn(&Array1<f64>) -> Array1<f64>,
967        num_simulations: usize,
968    ) -> Result<(), String> {
969        let mut rng = seeded_rng(42);
970
971        for epoch in 0..1000 {
972            let mut epoch_loss = 0.0;
973            let mut num_steps = 0usize;
974
975            for _ in 0..num_simulations / 1000 {
976                // Sample from prior
977                let mut theta = Array1::zeros(self.parameter_dim);
978                for i in 0..self.parameter_dim {
979                    theta[i] = rng.sample(Normal::new(0.0, 1.0).expect("Operation failed"));
980                }
981
982                // Simulate observation
983                let x = simulator(&theta);
984
985                // Train posterior network
986                epoch_loss += self.train_posterior_step(&x, &theta)?;
987                num_steps += 1;
988            }
989
990            // Average Gaussian negative log-likelihood loss for this epoch.
991            let avg_loss = if num_steps > 0 {
992                epoch_loss / num_steps as f64
993            } else {
994                0.0
995            };
996            self.training_history.push(avg_loss);
997
998            if epoch % 100 == 0 {
999                println!("Epoch {epoch}: loss = {avg_loss:.6}");
1000            }
1001        }
1002
1003        self.trained = true;
1004        Ok(())
1005    }
1006
1007    /// Returns the per-epoch training loss history recorded by [`Self::train`].
1008    ///
1009    /// Each entry is the average Gaussian negative log-likelihood loss over
1010    /// all simulations processed during the corresponding epoch (one entry is
1011    /// pushed per epoch, in order — `train` always runs a fixed 1000-epoch
1012    /// schedule).
1013    pub fn training_history(&self) -> &[f64] {
1014        &self.training_history
1015    }
1016
1017    /// Estimate posterior given observation
1018    pub fn posterior(
1019        &self,
1020        observation: &Array1<f64>,
1021        num_samples: usize,
1022        seed: u64,
1023    ) -> Result<Array2<f64>, String> {
1024        if !self.trained {
1025            return Err("Model must be trained before inference".to_string());
1026        }
1027
1028        // Get posterior parameters from network
1029        let posterior_params = self.posterior_network.forward(observation)?;
1030
1031        let mean_start = 0;
1032        let var_start = self.parameter_dim;
1033
1034        let mut rng = seeded_rng(seed);
1035        let mut samples = Array2::zeros((num_samples, self.parameter_dim));
1036
1037        for i in 0..num_samples {
1038            for j in 0..self.parameter_dim {
1039                let mean = posterior_params[mean_start + j];
1040                let var = posterior_params[var_start + j].exp(); // Ensure positive variance
1041
1042                samples[[i, j]] =
1043                    rng.sample(Normal::new(mean, var.sqrt()).expect("Operation failed"));
1044            }
1045        }
1046
1047        Ok(samples)
1048    }
1049
1050    /// Train posterior network step
1051    ///
1052    /// Returns the Gaussian negative log-likelihood of `true_parameter` under
1053    /// the predicted posterior mean/variance — the amortized inference loss
1054    /// this step is meant to minimize.
1055    fn train_posterior_step(
1056        &mut self,
1057        observation: &Array1<f64>,
1058        true_parameter: &Array1<f64>,
1059    ) -> Result<f64, String> {
1060        // Get predicted posterior parameters
1061        let predicted_params = self.posterior_network.forward(observation)?;
1062
1063        let mean_start = 0;
1064        let var_start = self.parameter_dim;
1065
1066        // Compute loss (negative log-likelihood) and update
1067        // (Implementation would use automatic differentiation)
1068        let mut nll = 0.0;
1069        for j in 0..self.parameter_dim {
1070            let mean = predicted_params[mean_start + j];
1071            let var = predicted_params[var_start + j].exp().max(1e-12); // Ensure positive variance
1072            let diff = true_parameter[j] - mean;
1073            nll += 0.5 * diff * diff / var + 0.5 * var.ln();
1074        }
1075        nll += 0.5 * (self.parameter_dim as f64) * (2.0 * std::f64::consts::PI).ln();
1076
1077        Ok(nll)
1078    }
1079}
1080
1081// Helper trait for extending base distribution with log probability
1082trait LogProbability {
1083    fn log_probability(&self, x: &[f64]) -> Result<f64, String>;
1084}
1085
1086impl LogProbability for MultivariateNormal {
1087    fn log_probability(&self, x: &[f64]) -> Result<f64, String> {
1088        if x.len() != self.dimension() {
1089            return Err("Dimension mismatch".to_string());
1090        }
1091
1092        // Simplified log probability computation
1093        let mut log_prob = 0.0;
1094        for &xi in x {
1095            log_prob += -0.5 * xi * xi; // Assume standard normal for simplicity
1096        }
1097        log_prob += -0.5 * (x.len() as f64) * (2.0 * std::f64::consts::PI).ln();
1098
1099        Ok(log_prob)
1100    }
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105    use super::*;
1106    use approx::assert_relative_eq;
1107
1108    #[test]
1109    fn test_normalizing_flow_creation() {
1110        let flow = NormalizingFlow::new(5, 3);
1111        assert_eq!(flow.dimension, 5);
1112        assert_eq!(flow.num_layers, 3);
1113        assert!(!flow.trained);
1114    }
1115
1116    #[test]
1117    fn test_diffusion_model_creation() {
1118        let config = DiffusionConfig {
1119            dimension: 10,
1120            num_timesteps: 100,
1121            beta_start: 1e-4,
1122            beta_end: 0.02,
1123            hidden_dims: vec![32, 64, 32],
1124        };
1125
1126        let diffusion = ScoreBasedDiffusion::new(config);
1127        assert_eq!(diffusion.config.dimension, 10);
1128        assert_eq!(diffusion.config.num_timesteps, 100);
1129    }
1130
1131    #[test]
1132    fn test_energy_based_model() {
1133        let ebm = EnergyBasedModel::new(5, &[32, 32]);
1134        assert_eq!(ebm.dimension, 5);
1135        assert_eq!(ebm.mcmc_steps, 100);
1136    }
1137
1138    #[test]
1139    fn test_neural_posterior_estimation() {
1140        let npe = NeuralPosteriorEstimation::new(10, 5, &[32, 32]);
1141        assert_eq!(npe.observation_dim, 10);
1142        assert_eq!(npe.parameter_dim, 5);
1143        assert!(!npe.trained);
1144    }
1145
1146    #[test]
1147    fn test_mlp_forward() {
1148        let mlp = MLP::new(&[3, 5, 2]);
1149        let input = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1150        let output = mlp.forward(&input).expect("Operation failed");
1151        assert_eq!(output.len(), 2);
1152    }
1153
1154    #[test]
1155    fn test_noise_schedule() {
1156        let schedule = NoiseSchedule::new(100, 1e-4, 0.02);
1157        assert_eq!(schedule.betas.len(), 100);
1158        assert_eq!(schedule.alphas.len(), 100);
1159        assert_eq!(schedule.alpha_bars.len(), 100);
1160
1161        // Check that alpha_bars are decreasing
1162        for i in 1..schedule.alpha_bars.len() {
1163            assert!(schedule.alpha_bars[i] <= schedule.alpha_bars[i - 1]);
1164        }
1165    }
1166
1167    #[test]
1168    fn test_normalizing_flow_training_history() {
1169        let mut flow = NormalizingFlow::new(4, 2);
1170        let training_data =
1171            Array2::from_shape_fn((8, 4), |(i, j)| (i as f64 * 0.3 + j as f64 * 0.1) - 1.0);
1172        let num_epochs = 5;
1173
1174        flow.train(&training_data, num_epochs)
1175            .expect("training should succeed on well-formed synthetic data");
1176
1177        let history = flow.training_history();
1178        assert_eq!(
1179            history.len(),
1180            num_epochs,
1181            "training_history should have exactly one entry per epoch"
1182        );
1183        for (i, &loss) in history.iter().enumerate() {
1184            assert!(
1185                loss.is_finite(),
1186                "epoch {i} loss should be finite, got {loss}"
1187            );
1188        }
1189    }
1190
1191    #[test]
1192    fn test_score_based_diffusion_training_history() {
1193        let config = DiffusionConfig {
1194            dimension: 3,
1195            num_timesteps: 20,
1196            beta_start: 1e-4,
1197            beta_end: 0.02,
1198            hidden_dims: vec![8, 8],
1199        };
1200        let mut diffusion = ScoreBasedDiffusion::new(config);
1201        let training_data = Array2::from_shape_fn((6, 3), |(i, j)| (i as f64 - j as f64) * 0.2);
1202
1203        diffusion
1204            .train(&training_data)
1205            .expect("training should succeed on well-formed synthetic data");
1206
1207        let history = diffusion.training_history();
1208        // `train` has no epoch parameter and always runs a fixed 1000-epoch
1209        // schedule internally.
1210        assert_eq!(
1211            history.len(),
1212            1000,
1213            "training_history should have exactly one entry per (fixed) epoch"
1214        );
1215        for (i, &loss) in history.iter().enumerate() {
1216            assert!(
1217                loss.is_finite(),
1218                "epoch {i} loss should be finite, got {loss}"
1219            );
1220        }
1221    }
1222
1223    #[test]
1224    fn test_energy_based_model_training_history() {
1225        let mut ebm = EnergyBasedModel::new(3, &[8, 8]);
1226        let training_data = Array2::from_shape_fn((4, 3), |(i, j)| (i as f64 - j as f64) * 0.15);
1227        let num_epochs = 4;
1228
1229        ebm.train(&training_data, num_epochs)
1230            .expect("training should succeed on well-formed synthetic data");
1231
1232        let history = ebm.training_history();
1233        assert_eq!(
1234            history.len(),
1235            num_epochs,
1236            "training_history should have exactly one entry per epoch"
1237        );
1238        for (i, &loss) in history.iter().enumerate() {
1239            assert!(
1240                loss.is_finite(),
1241                "epoch {i} loss should be finite, got {loss}"
1242            );
1243        }
1244    }
1245
1246    #[test]
1247    fn test_neural_posterior_estimation_training_history() {
1248        let mut npe = NeuralPosteriorEstimation::new(4, 2, &[8, 8]);
1249        let simulator = |theta: &Array1<f64>| -> Array1<f64> {
1250            Array1::from_vec(vec![
1251                theta[0],
1252                theta[1],
1253                theta[0] + theta[1],
1254                theta[0] - theta[1],
1255            ])
1256        };
1257
1258        npe.train(simulator, 2000)
1259            .expect("training should succeed on well-formed synthetic simulator");
1260
1261        let history = npe.training_history();
1262        // `train` has no epoch parameter and always runs a fixed 1000-epoch
1263        // schedule internally.
1264        assert_eq!(
1265            history.len(),
1266            1000,
1267            "training_history should have exactly one entry per (fixed) epoch"
1268        );
1269        for (i, &loss) in history.iter().enumerate() {
1270            assert!(
1271                loss.is_finite(),
1272                "epoch {i} loss should be finite, got {loss}"
1273            );
1274        }
1275    }
1276}