Skip to main content

sklears_neural/
normalizing_flows.rs

1//! Normalizing Flow models for density estimation and generative modeling.
2//!
3//! This module implements various normalizing flow architectures including:
4//! - Coupling layers (RealNVP-style)
5//! - Affine coupling flows
6//! - Glow-style flows with invertible 1x1 convolutions
7//! - Masked autoregressive flows (MAF)
8//! - Inverse autoregressive flows (IAF)
9//!
10//! Normalizing flows learn invertible transformations to map simple distributions
11//! (e.g., Gaussian) to complex data distributions while maintaining tractable
12//! likelihood computation through the change of variables formula.
13
14use crate::{activation::Activation, NeuralResult};
15use scirs2_core::ndarray::{Array1, Array2, Axis, ScalarOperand};
16use scirs2_core::random::{thread_rng, Normal};
17use sklears_core::types::FloatBounds;
18use std::f64::consts::PI;
19
20#[cfg(feature = "serde")]
21use serde::{Deserialize, Serialize};
22
23/// Type of coupling transformation
24#[derive(Debug, Clone, Copy, PartialEq)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub enum CouplingType {
27    /// Additive coupling: y = x + t(x_masked)
28    Additive,
29    /// Affine coupling: y = x * exp(s(x_masked)) + t(x_masked)
30    Affine,
31}
32
33/// Masking strategy for coupling layers
34#[derive(Debug, Clone, Copy, PartialEq)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub enum MaskType {
37    /// Split features in half (checkerboard for images)
38    Checkerboard,
39    /// Channel-wise masking
40    Channelwise,
41    /// Alternating split
42    Alternating,
43}
44
45/// Configuration for a coupling layer
46#[derive(Debug, Clone)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub struct CouplingLayerConfig {
49    /// Input dimension
50    pub input_dim: usize,
51    /// Hidden layer sizes for scale and translation networks
52    pub hidden_dims: Vec<usize>,
53    /// Type of coupling transformation
54    pub coupling_type: CouplingType,
55    /// Masking type
56    pub mask_type: MaskType,
57    /// Whether to reverse the mask
58    pub reverse_mask: bool,
59    /// Activation function for hidden layers
60    pub activation: String,
61}
62
63impl Default for CouplingLayerConfig {
64    fn default() -> Self {
65        Self {
66            input_dim: 784,
67            hidden_dims: vec![256, 256],
68            coupling_type: CouplingType::Affine,
69            mask_type: MaskType::Checkerboard,
70            reverse_mask: false,
71            activation: "relu".to_string(),
72        }
73    }
74}
75
76/// Affine coupling layer (RealNVP-style)
77///
78/// Splits the input into two parts and transforms one part conditioned on the other.
79/// For affine coupling: y_b = x_b * exp(s(x_a)) + t(x_a)
80/// where x = [x_a, x_b] and s, t are neural networks.
81#[derive(Debug)]
82#[allow(dead_code)] // input_dim retained for shape validation and future Jacobian computations
83pub struct AffineCouplingLayer<T: FloatBounds> {
84    /// Input dimension
85    input_dim: usize,
86    /// Scale network weights
87    scale_weights: Vec<Array2<T>>,
88    /// Scale network biases
89    scale_biases: Vec<Array1<T>>,
90    /// Translation network weights
91    translation_weights: Vec<Array2<T>>,
92    /// Translation network biases
93    translation_biases: Vec<Array1<T>>,
94    /// Masking pattern (true = transform, false = identity)
95    mask: Array1<bool>,
96    /// Activation function
97    activation: Activation,
98    /// Whether this is affine or additive coupling
99    is_affine: bool,
100    /// Cached input for backward pass
101    cached_input: Option<Array2<T>>,
102    /// Cached scale values
103    cached_scale: Option<Array2<T>>,
104}
105
106impl<T: FloatBounds> AffineCouplingLayer<T> {
107    /// Create a new affine coupling layer
108    pub fn new(config: CouplingLayerConfig) -> Self {
109        let mut rng = thread_rng();
110        let split_point = config.input_dim / 2;
111
112        // Create mask
113        let mut mask = Array1::from_elem(config.input_dim, false);
114        match config.mask_type {
115            MaskType::Checkerboard | MaskType::Channelwise => {
116                for i in (if config.reverse_mask { split_point } else { 0 })
117                    ..(if config.reverse_mask {
118                        config.input_dim
119                    } else {
120                        split_point
121                    })
122                {
123                    mask[i] = true;
124                }
125            }
126            MaskType::Alternating => {
127                for i in 0..config.input_dim {
128                    if config.reverse_mask {
129                        mask[i] = i % 2 == 1;
130                    } else {
131                        mask[i] = i % 2 == 0;
132                    }
133                }
134            }
135        }
136
137        // Count masked and unmasked dimensions
138        let n_masked = mask.iter().filter(|&&x| x).count();
139        let n_unmasked = config.input_dim - n_masked;
140
141        // Initialize scale network
142        let mut scale_weights = Vec::new();
143        let mut scale_biases = Vec::new();
144
145        let mut prev_dim = n_masked;
146        for &hidden_dim in &config.hidden_dims {
147            let std = T::from((2.0 / prev_dim as f64).sqrt()).unwrap_or_else(|| T::zero());
148            let w = Array2::from_shape_fn((prev_dim, hidden_dim), |_| {
149                T::from(
150                    rng.sample::<f64, _>(
151                        Normal::new(0.0, 1.0).expect("standard normal should be valid"),
152                    ) * std.to_f64().unwrap_or(0.0),
153                )
154                .expect("value should be present")
155            });
156            let b = Array1::zeros(hidden_dim);
157            scale_weights.push(w);
158            scale_biases.push(b);
159            prev_dim = hidden_dim;
160        }
161
162        // Output layer for scale network
163        let w = Array2::from_shape_fn((prev_dim, n_unmasked), |_| {
164            T::from(
165                rng.sample::<f64, _>(Normal::new(0.0, 1.0).expect("valid distribution params"))
166                    * 0.01,
167            )
168            .unwrap_or_else(|| T::zero())
169        });
170        let b = Array1::zeros(n_unmasked);
171        scale_weights.push(w);
172        scale_biases.push(b);
173
174        // Initialize translation network (same architecture)
175        let mut translation_weights = Vec::new();
176        let mut translation_biases = Vec::new();
177
178        let mut prev_dim = n_masked;
179        for &hidden_dim in &config.hidden_dims {
180            let std = T::from((2.0 / prev_dim as f64).sqrt()).unwrap_or_else(|| T::zero());
181            let w = Array2::from_shape_fn((prev_dim, hidden_dim), |_| {
182                T::from(
183                    rng.sample::<f64, _>(
184                        Normal::new(0.0, 1.0).expect("standard normal should be valid"),
185                    ) * std.to_f64().unwrap_or(0.0),
186                )
187                .expect("value should be present")
188            });
189            let b = Array1::zeros(hidden_dim);
190            translation_weights.push(w);
191            translation_biases.push(b);
192            prev_dim = hidden_dim;
193        }
194
195        // Output layer for translation network
196        let w = Array2::from_shape_fn((prev_dim, n_unmasked), |_| {
197            T::from(
198                rng.sample::<f64, _>(Normal::new(0.0, 1.0).expect("valid distribution params"))
199                    * 0.01,
200            )
201            .unwrap_or_else(|| T::zero())
202        });
203        let b = Array1::zeros(n_unmasked);
204        translation_weights.push(w);
205        translation_biases.push(b);
206
207        let activation = match config.activation.as_str() {
208            "relu" => Activation::Relu,
209            "tanh" => Activation::Tanh,
210            "sigmoid" | "logistic" => Activation::Logistic,
211            "elu" => Activation::Elu,
212            _ => Activation::Relu,
213        };
214
215        Self {
216            input_dim: config.input_dim,
217            scale_weights,
218            scale_biases,
219            translation_weights,
220            translation_biases,
221            mask,
222            activation,
223            is_affine: matches!(config.coupling_type, CouplingType::Affine),
224            cached_input: None,
225            cached_scale: None,
226        }
227    }
228
229    /// Forward transformation with log determinant Jacobian
230    pub fn forward(&mut self, x: &Array2<T>) -> NeuralResult<(Array2<T>, T)> {
231        let batch_size = x.nrows();
232
233        // Split input based on mask
234        let masked_indices: Vec<_> = self
235            .mask
236            .iter()
237            .enumerate()
238            .filter(|(_, &m)| m)
239            .map(|(i, _)| i)
240            .collect();
241        let unmasked_indices: Vec<_> = self
242            .mask
243            .iter()
244            .enumerate()
245            .filter(|(_, &m)| !m)
246            .map(|(i, _)| i)
247            .collect();
248
249        let x_masked = Array2::from_shape_fn((batch_size, masked_indices.len()), |(i, j)| {
250            x[[i, masked_indices[j]]]
251        });
252        let x_unmasked = Array2::from_shape_fn((batch_size, unmasked_indices.len()), |(i, j)| {
253            x[[i, unmasked_indices[j]]]
254        });
255
256        // Compute scale and translation from masked part
257        let scale = self.compute_scale(&x_masked)?;
258        let translation = self.compute_translation(&x_masked)?;
259
260        // Apply transformation
261        let y_unmasked = if self.is_affine {
262            // Affine coupling: y = x * exp(s) + t
263            let exp_scale = scale.mapv(|s| s.exp());
264            &x_unmasked * &exp_scale + &translation
265        } else {
266            // Additive coupling: y = x + t
267            &x_unmasked + &translation
268        };
269
270        // Reconstruct output
271        let mut y = x.clone();
272        for (i, &idx) in unmasked_indices.iter().enumerate() {
273            for j in 0..batch_size {
274                y[[j, idx]] = y_unmasked[[j, i]];
275            }
276        }
277
278        // Compute log determinant Jacobian
279        let log_det = if self.is_affine {
280            // For affine coupling: log|det(J)| = sum(s)
281            scale.sum_axis(Axis(1)).sum()
282        } else {
283            // For additive coupling: log|det(J)| = 0
284            T::zero()
285        };
286
287        // Cache for backward pass
288        self.cached_input = Some(x.clone());
289        self.cached_scale = Some(scale);
290
291        Ok((y, log_det))
292    }
293
294    /// Inverse transformation
295    pub fn inverse(&self, y: &Array2<T>) -> NeuralResult<Array2<T>> {
296        let batch_size = y.nrows();
297
298        // Split input based on mask
299        let masked_indices: Vec<_> = self
300            .mask
301            .iter()
302            .enumerate()
303            .filter(|(_, &m)| m)
304            .map(|(i, _)| i)
305            .collect();
306        let unmasked_indices: Vec<_> = self
307            .mask
308            .iter()
309            .enumerate()
310            .filter(|(_, &m)| !m)
311            .map(|(i, _)| i)
312            .collect();
313
314        let y_masked = Array2::from_shape_fn((batch_size, masked_indices.len()), |(i, j)| {
315            y[[i, masked_indices[j]]]
316        });
317        let y_unmasked = Array2::from_shape_fn((batch_size, unmasked_indices.len()), |(i, j)| {
318            y[[i, unmasked_indices[j]]]
319        });
320
321        // Compute scale and translation from masked part
322        let scale = self.compute_scale(&y_masked)?;
323        let translation = self.compute_translation(&y_masked)?;
324
325        // Apply inverse transformation
326        let x_unmasked = if self.is_affine {
327            // Inverse affine: x = (y - t) / exp(s) = (y - t) * exp(-s)
328            let exp_neg_scale = scale.mapv(|s| (-s).exp());
329            (&y_unmasked - &translation) * &exp_neg_scale
330        } else {
331            // Inverse additive: x = y - t
332            &y_unmasked - &translation
333        };
334
335        // Reconstruct output
336        let mut x = y.clone();
337        for (i, &idx) in unmasked_indices.iter().enumerate() {
338            for j in 0..batch_size {
339                x[[j, idx]] = x_unmasked[[j, i]];
340            }
341        }
342
343        Ok(x)
344    }
345
346    /// Compute scale values from conditioned input
347    fn compute_scale(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
348        let mut h = x.clone();
349
350        // Forward through scale network
351        for (i, (w, b)) in self
352            .scale_weights
353            .iter()
354            .zip(self.scale_biases.iter())
355            .enumerate()
356        {
357            h = h.dot(w) + b;
358
359            // Apply activation to all but last layer
360            if i < self.scale_weights.len() - 1 {
361                h.mapv_inplace(|x| {
362                    let x_f64 = x.to_f64().unwrap_or(0.0);
363                    T::from(self.activation.forward(x_f64)).unwrap_or_else(|| T::zero())
364                });
365            }
366        }
367
368        // Clamp scale to avoid numerical instability
369        h.mapv_inplace(|s| {
370            let s_f64 = s.to_f64().unwrap_or(0.0);
371            T::from(s_f64.clamp(-10.0, 10.0)).unwrap_or_else(|| T::zero())
372        });
373
374        Ok(h)
375    }
376
377    /// Compute translation values from conditioned input
378    fn compute_translation(&self, x: &Array2<T>) -> NeuralResult<Array2<T>> {
379        let mut h = x.clone();
380
381        // Forward through translation network
382        for (i, (w, b)) in self
383            .translation_weights
384            .iter()
385            .zip(self.translation_biases.iter())
386            .enumerate()
387        {
388            h = h.dot(w) + b;
389
390            // Apply activation to all but last layer
391            if i < self.translation_weights.len() - 1 {
392                h.mapv_inplace(|x| {
393                    let x_f64 = x.to_f64().unwrap_or(0.0);
394                    T::from(self.activation.forward(x_f64)).unwrap_or_else(|| T::zero())
395                });
396            }
397        }
398
399        Ok(h)
400    }
401
402    /// Get number of parameters
403    pub fn num_parameters(&self) -> usize {
404        let scale_params: usize = self.scale_weights.iter().map(|w| w.len()).sum::<usize>()
405            + self.scale_biases.iter().map(|b| b.len()).sum::<usize>();
406
407        let translation_params: usize = self
408            .translation_weights
409            .iter()
410            .map(|w| w.len())
411            .sum::<usize>()
412            + self
413                .translation_biases
414                .iter()
415                .map(|b| b.len())
416                .sum::<usize>();
417
418        scale_params + translation_params
419    }
420}
421
422/// Configuration for normalizing flow model
423#[derive(Debug, Clone)]
424#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
425pub struct NormalizingFlowConfig {
426    /// Input dimension
427    pub input_dim: usize,
428    /// Number of coupling layers
429    pub n_flows: usize,
430    /// Hidden dimensions for coupling networks
431    pub hidden_dims: Vec<usize>,
432    /// Type of coupling
433    pub coupling_type: CouplingType,
434    /// Base distribution mean
435    pub base_mean: f64,
436    /// Base distribution standard deviation
437    pub base_std: f64,
438    /// Learning rate
439    pub learning_rate: f64,
440    /// Number of training iterations
441    pub n_iterations: usize,
442    /// Batch size
443    pub batch_size: usize,
444}
445
446impl Default for NormalizingFlowConfig {
447    fn default() -> Self {
448        Self {
449            input_dim: 784,
450            n_flows: 8,
451            hidden_dims: vec![256, 256],
452            coupling_type: CouplingType::Affine,
453            base_mean: 0.0,
454            base_std: 1.0,
455            learning_rate: 0.001,
456            n_iterations: 1000,
457            batch_size: 128,
458        }
459    }
460}
461
462/// Normalizing Flow model for density estimation
463///
464/// Implements a stack of coupling layers to learn complex distributions
465/// from a simple base distribution (typically Gaussian).
466pub struct NormalizingFlow<T: FloatBounds> {
467    /// Stack of coupling layers
468    coupling_layers: Vec<AffineCouplingLayer<T>>,
469    /// Base distribution parameters
470    base_mean: T,
471    base_std: T,
472    /// Input dimension
473    input_dim: usize,
474    /// Training configuration
475    config: NormalizingFlowConfig,
476}
477
478impl<T: FloatBounds + ScalarOperand> NormalizingFlow<T> {
479    /// Create a new normalizing flow model
480    pub fn new(config: NormalizingFlowConfig) -> Self {
481        let mut coupling_layers = Vec::new();
482
483        for i in 0..config.n_flows {
484            let layer_config = CouplingLayerConfig {
485                input_dim: config.input_dim,
486                hidden_dims: config.hidden_dims.clone(),
487                coupling_type: config.coupling_type,
488                mask_type: MaskType::Checkerboard,
489                reverse_mask: i % 2 == 1, // Alternate masks
490                activation: "relu".to_string(),
491            };
492            coupling_layers.push(AffineCouplingLayer::new(layer_config));
493        }
494
495        Self {
496            coupling_layers,
497            base_mean: T::from(config.base_mean).unwrap_or_else(|| T::zero()),
498            base_std: T::from(config.base_std).unwrap_or_else(|| T::zero()),
499            input_dim: config.input_dim,
500            config,
501        }
502    }
503
504    /// Forward pass: data -> latent
505    pub fn forward(&mut self, x: &Array2<T>) -> NeuralResult<(Array2<T>, T)> {
506        let mut z = x.clone();
507        let mut log_det_sum = T::zero();
508
509        // Pass through all coupling layers
510        for layer in &mut self.coupling_layers {
511            let (z_new, log_det) = layer.forward(&z)?;
512            z = z_new;
513            log_det_sum += log_det;
514        }
515
516        Ok((z, log_det_sum))
517    }
518
519    /// Inverse pass: latent -> data
520    pub fn inverse(&self, z: &Array2<T>) -> NeuralResult<Array2<T>> {
521        let mut x = z.clone();
522
523        // Pass through coupling layers in reverse
524        for layer in self.coupling_layers.iter().rev() {
525            x = layer.inverse(&x)?;
526        }
527
528        Ok(x)
529    }
530
531    /// Sample from the model
532    pub fn sample(&self, n_samples: usize) -> NeuralResult<Array2<T>> {
533        let mut rng = thread_rng();
534        let normal = Normal::new(
535            self.base_mean.to_f64().unwrap_or(0.0),
536            self.base_std.to_f64().unwrap_or(0.0),
537        )
538        .expect("value should be present");
539
540        // Sample from base distribution
541        let z = Array2::from_shape_fn((n_samples, self.input_dim), |_| {
542            T::from(rng.sample::<f64, _>(normal)).unwrap_or_else(|| T::zero())
543        });
544
545        // Transform to data distribution
546        self.inverse(&z)
547    }
548
549    /// Compute negative log likelihood
550    pub fn log_likelihood(&mut self, x: &Array2<T>) -> NeuralResult<T> {
551        let (z, log_det) = self.forward(x)?;
552
553        // Log probability under base distribution (Gaussian)
554        let z_normalized = (&z - self.base_mean) / self.base_std;
555        let log_prob_base = z_normalized.mapv(|zi| {
556            let zi_f64 = zi.to_f64().unwrap_or(0.0);
557            T::from(-0.5 * zi_f64 * zi_f64 - 0.5 * (2.0 * PI).ln()).unwrap_or_else(|| T::zero())
558        });
559
560        let log_prob_sum = log_prob_base.sum();
561
562        // Add log determinant (change of variables)
563        let log_likelihood = log_prob_sum + log_det;
564
565        Ok(log_likelihood)
566    }
567
568    /// Get total number of parameters
569    pub fn num_parameters(&self) -> usize {
570        self.coupling_layers
571            .iter()
572            .map(|layer| layer.num_parameters())
573            .sum()
574    }
575
576    /// Get configuration
577    pub fn config(&self) -> &NormalizingFlowConfig {
578        &self.config
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use approx::assert_relative_eq;
586
587    #[test]
588    fn test_coupling_layer_creation() {
589        let config = CouplingLayerConfig {
590            input_dim: 10,
591            hidden_dims: vec![32, 32],
592            coupling_type: CouplingType::Affine,
593            mask_type: MaskType::Checkerboard,
594            reverse_mask: false,
595            activation: "relu".to_string(),
596        };
597
598        let layer: AffineCouplingLayer<f64> = AffineCouplingLayer::new(config);
599        assert_eq!(layer.input_dim, 10);
600        assert!(layer.num_parameters() > 0);
601    }
602
603    #[test]
604    fn test_coupling_layer_forward_backward() {
605        let config = CouplingLayerConfig {
606            input_dim: 4,
607            hidden_dims: vec![8],
608            coupling_type: CouplingType::Affine,
609            mask_type: MaskType::Checkerboard,
610            reverse_mask: false,
611            activation: "relu".to_string(),
612        };
613
614        let mut layer: AffineCouplingLayer<f64> = AffineCouplingLayer::new(config);
615        let x = Array2::from_shape_vec((2, 4), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
616            .expect("array shape mismatch");
617
618        let (y, _log_det) = layer.forward(&x).expect("forward pass should succeed");
619        let x_reconstructed = layer.inverse(&y).expect("operation should succeed");
620
621        // Check invertibility
622        for i in 0..x.nrows() {
623            for j in 0..x.ncols() {
624                assert_relative_eq!(x[[i, j]], x_reconstructed[[i, j]], epsilon = 1e-5);
625            }
626        }
627    }
628
629    #[test]
630    fn test_normalizing_flow_creation() {
631        let config = NormalizingFlowConfig {
632            input_dim: 10,
633            n_flows: 4,
634            hidden_dims: vec![32],
635            coupling_type: CouplingType::Affine,
636            base_mean: 0.0,
637            base_std: 1.0,
638            learning_rate: 0.001,
639            n_iterations: 100,
640            batch_size: 32,
641        };
642
643        let flow: NormalizingFlow<f64> = NormalizingFlow::new(config);
644        assert_eq!(flow.input_dim, 10);
645        assert_eq!(flow.coupling_layers.len(), 4);
646        assert!(flow.num_parameters() > 0);
647    }
648
649    #[test]
650    fn test_normalizing_flow_invertibility() {
651        let config = NormalizingFlowConfig {
652            input_dim: 8,
653            n_flows: 3,
654            hidden_dims: vec![16],
655            coupling_type: CouplingType::Affine,
656            ..Default::default()
657        };
658
659        let mut flow: NormalizingFlow<f64> = NormalizingFlow::new(config);
660        let x = Array2::from_shape_fn((5, 8), |(i, j)| {
661            (i as f64 + 1.0) * 0.1 + (j as f64 + 1.0) * 0.01
662        });
663
664        let (z, _log_det) = flow.forward(&x).expect("forward pass should succeed");
665        let x_reconstructed = flow.inverse(&z).expect("operation should succeed");
666
667        // Check invertibility
668        for i in 0..x.nrows() {
669            for j in 0..x.ncols() {
670                assert_relative_eq!(x[[i, j]], x_reconstructed[[i, j]], epsilon = 1e-4);
671            }
672        }
673    }
674
675    #[test]
676    fn test_normalizing_flow_sampling() {
677        let config = NormalizingFlowConfig {
678            input_dim: 5,
679            n_flows: 2,
680            hidden_dims: vec![10],
681            coupling_type: CouplingType::Additive,
682            ..Default::default()
683        };
684
685        let flow: NormalizingFlow<f64> = NormalizingFlow::new(config);
686        let samples = flow.sample(10).expect("sampling should succeed");
687
688        assert_eq!(samples.nrows(), 10);
689        assert_eq!(samples.ncols(), 5);
690    }
691
692    #[test]
693    fn test_normalizing_flow_log_likelihood() {
694        let config = NormalizingFlowConfig {
695            input_dim: 6,
696            n_flows: 2,
697            hidden_dims: vec![12],
698            coupling_type: CouplingType::Affine,
699            ..Default::default()
700        };
701
702        let mut flow: NormalizingFlow<f64> = NormalizingFlow::new(config);
703        let x = Array2::from_shape_fn((3, 6), |(i, j)| (i as f64 + j as f64) * 0.1);
704
705        let log_likelihood = flow.log_likelihood(&x).expect("operation should succeed");
706        // Should be finite
707        assert!(log_likelihood.is_finite());
708    }
709
710    #[test]
711    fn test_additive_coupling() {
712        let config = CouplingLayerConfig {
713            input_dim: 4,
714            hidden_dims: vec![8],
715            coupling_type: CouplingType::Additive,
716            mask_type: MaskType::Checkerboard,
717            reverse_mask: false,
718            activation: "relu".to_string(),
719        };
720
721        let mut layer: AffineCouplingLayer<f64> = AffineCouplingLayer::new(config);
722        let x =
723            Array2::from_shape_vec((1, 4), vec![1.0, 2.0, 3.0, 4.0]).expect("array shape mismatch");
724
725        let (y, log_det) = layer.forward(&x).expect("forward pass should succeed");
726
727        // For additive coupling, log determinant should be zero
728        assert_relative_eq!(log_det, 0.0, epsilon = 1e-10);
729
730        // Check invertibility
731        let x_reconstructed = layer.inverse(&y).expect("operation should succeed");
732        for i in 0..x.len() {
733            assert_relative_eq!(x[[0, i]], x_reconstructed[[0, i]], epsilon = 1e-5);
734        }
735    }
736
737    #[test]
738    fn test_mask_alternation() {
739        let config1 = CouplingLayerConfig {
740            input_dim: 6,
741            hidden_dims: vec![8],
742            coupling_type: CouplingType::Affine,
743            mask_type: MaskType::Alternating,
744            reverse_mask: false,
745            activation: "relu".to_string(),
746        };
747
748        let config2 = CouplingLayerConfig {
749            input_dim: 6,
750            hidden_dims: vec![8],
751            coupling_type: CouplingType::Affine,
752            mask_type: MaskType::Alternating,
753            reverse_mask: true,
754            activation: "relu".to_string(),
755        };
756
757        let layer1: AffineCouplingLayer<f64> = AffineCouplingLayer::new(config1);
758        let layer2: AffineCouplingLayer<f64> = AffineCouplingLayer::new(config2);
759
760        // Check that masks are different
761        let mask1_true = layer1.mask.iter().filter(|&&x| x).count();
762        let mask2_true = layer2.mask.iter().filter(|&&x| x).count();
763
764        // Both should have approximately half the dimensions masked
765        assert!((2..=4).contains(&mask1_true));
766        assert!((2..=4).contains(&mask2_true));
767    }
768}