## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/vector/ops/normalization.rs

    Line
    Count
    Source
    1
    //! Normalization operations for Vector<f32>
    2
    //!
    3
    //! This module provides normalization methods:
    4
    //! - `zscore()` - Z-score normalization (standardization)
    5
    //! - `minmax_normalize()` - Min-max normalization to [0, 1]
    6
    //! - `layer_norm()` - Layer normalization with learnable parameters
    7
    //! - `layer_norm_simple()` - Layer normalization without learnable parameters
    8
    //! - `normalize()` - Normalize to unit length (L2 norm = 1)
    9
    10
    use crate::{Result, TruenoError, Vector};
    11
    12
    impl Vector<f32> {
    13
        /// Z-score normalization (standardization)
    14
        ///
    15
        /// Transforms the vector to have mean = 0 and standard deviation = 1.
    16
        /// Each element is transformed as: z\[i\] = (x\[i\] - μ) / σ
    17
        ///
    18
        /// This is a fundamental preprocessing step in machine learning and statistics,
    19
        /// ensuring features have comparable scales and are centered around zero.
    20
        ///
    21
        /// # Performance
    22
        ///
    23
        /// Uses optimized SIMD implementations via mean() and stddev(), then applies
    24
        /// element-wise operations (sub, scale) which also use SIMD.
    25
        ///
    26
        /// # Examples
    27
        ///
    28
        /// ```
    29
        /// use trueno::Vector;
    30
        ///
    31
        /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
    32
        /// let z = v.zscore().unwrap();
    33
        ///
    34
        /// // Verify mean ≈ 0
    35
        /// let mean = z.mean().unwrap();
    36
        /// assert!(mean.abs() < 1e-5);
    37
        ///
    38
        /// // Verify stddev ≈ 1
    39
        /// let std = z.stddev().unwrap();
    40
        /// assert!((std - 1.0).abs() < 1e-5);
    41
        /// ```
    42
        ///
    43
        /// # Empty vectors
    44
        ///
    45
        /// Returns EmptyVector error for empty vectors (cannot compute mean/stddev).
    46
        ///
    47
        /// # Division by zero
    48
        ///
    49
        /// Returns DivisionByZero error if the vector has zero standard deviation
    50
        /// (i.e., all elements are identical/constant).
    51
        ///
    52
        /// ```
    53
        /// use trueno::{Vector, TruenoError};
    54
        ///
    55
        /// let v = Vector::from_slice(&[5.0, 5.0, 5.0]); // Constant
    56
        /// assert!(matches!(v.zscore(), Err(TruenoError::DivisionByZero)));
    57
        /// ```
    58
    0
        pub fn zscore(&self) -> Result<Self> {
    59
    0
            if self.as_slice().is_empty() {
    60
    0
                return Err(TruenoError::EmptyVector);
    61
    0
            }
    62
    63
    0
            let mean_val = self.mean()?;
    64
    0
            let std_val = self.stddev()?;
    65
    66
            // Check for zero standard deviation (constant vector)
    67
    0
            if std_val.abs() < 1e-10 {
    68
    0
                return Err(TruenoError::DivisionByZero);
    69
    0
            }
    70
    71
            // Transform: z[i] = (x[i] - μ) / σ
    72
    0
            let inv_std = 1.0 / std_val;
    73
    0
            let data: Vec<f32> = self
    74
    0
                .as_slice()
    75
    0
                .iter()
    76
    0
                .map(|&x| (x - mean_val) * inv_std)
    77
    0
                .collect();
    78
    79
    0
            Ok(Vector::from_vec(data))
    80
    0
        }
    81
    82
        /// Min-max normalization (scaling to [0, 1] range)
    83
        ///
    84
        /// Transforms the vector so that the minimum value becomes 0 and the maximum
    85
        /// value becomes 1, with all other values scaled proportionally.
    86
        /// Formula: x'\[i\] = (x\[i\] - min) / (max - min)
    87
        ///
    88
        /// This is a fundamental preprocessing technique in machine learning, especially
    89
        /// for algorithms sensitive to feature magnitudes (e.g., neural networks, k-NN).
    90
        ///
    91
        /// # Performance
    92
        ///
    93
        /// Uses optimized SIMD implementations via min() and max() operations, then
    94
        /// applies element-wise transformation.
    95
        ///
    96
        /// # Examples
    97
        ///
    98
        /// ```
    99
        /// use trueno::Vector;
    100
        ///
    101
        /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
    102
        /// let normalized = v.minmax_normalize().unwrap();
    103
        ///
    104
        /// // Verify range [0, 1]
    105
        /// let min = normalized.min().unwrap();
    106
        /// let max = normalized.max().unwrap();
    107
        /// assert!((min - 0.0).abs() < 1e-5);
    108
        /// assert!((max - 1.0).abs() < 1e-5);
    109
        /// ```
    110
        ///
    111
        /// # Empty vectors
    112
        ///
    113
        /// Returns EmptyVector error for empty vectors (cannot compute min/max).
    114
        ///
    115
        /// # Division by zero
    116
        ///
    117
        /// Returns DivisionByZero error if the vector has all identical elements
    118
        /// (i.e., min = max, causing division by zero in the normalization formula).
    119
        ///
    120
        /// ```
    121
        /// use trueno::{Vector, TruenoError};
    122
        ///
    123
        /// let v = Vector::from_slice(&[5.0, 5.0, 5.0]); // Constant
    124
        /// assert!(matches!(v.minmax_normalize(), Err(TruenoError::DivisionByZero)));
    125
        /// ```
    126
    0
        pub fn minmax_normalize(&self) -> Result<Self> {
    127
    0
            if self.as_slice().is_empty() {
    128
    0
                return Err(TruenoError::EmptyVector);
    129
    0
            }
    130
    131
    0
            let min_val = self.min()?;
    132
    0
            let max_val = self.max()?;
    133
    0
            let range = max_val - min_val;
    134
    135
            // Check for zero range (constant vector)
    136
    0
            if range.abs() < 1e-10 {
    137
    0
                return Err(TruenoError::DivisionByZero);
    138
    0
            }
    139
    140
            // Transform: x'[i] = (x[i] - min) / (max - min)
    141
    0
            let inv_range = 1.0 / range;
    142
    0
            let data: Vec<f32> = self
    143
    0
                .as_slice()
    144
    0
                .iter()
    145
    0
                .map(|&x| (x - min_val) * inv_range)
    146
    0
                .collect();
    147
    148
    0
            Ok(Vector::from_vec(data))
    149
    0
        }
    150
    151
        /// Layer normalization with learnable parameters (Issue #61: ML primitives)
    152
        ///
    153
        /// Applies layer normalization: `y = gamma * (x - mean) / sqrt(variance + eps) + beta`
    154
        ///
    155
        /// This is a fundamental normalization technique in transformers and other
    156
        /// modern neural network architectures. Unlike batch normalization, layer norm
    157
        /// normalizes across the feature dimension, making it suitable for sequence models.
    158
        ///
    159
        /// # Arguments
    160
        ///
    161
        /// * `gamma` - Scale parameter (typically learned, initialized to 1.0)
    162
        /// * `beta` - Shift parameter (typically learned, initialized to 0.0)
    163
        /// * `eps` - Small constant for numerical stability (typically 1e-5 or 1e-6)
    164
        ///
    165
        /// # Returns
    166
        ///
    167
        /// Normalized vector with the same shape as input
    168
        ///
    169
        /// # Errors
    170
        ///
    171
        /// Returns `SizeMismatch` if gamma or beta have different lengths than self
    172
        /// Returns `EmptyVector` if input is empty
    173
        ///
    174
        /// # Example
    175
        ///
    176
        /// ```
    177
        /// use trueno::Vector;
    178
        ///
    179
        /// let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
    180
        /// let gamma = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0]); // Scale = 1
    181
        /// let beta = Vector::from_slice(&[0.0, 0.0, 0.0, 0.0]);  // Shift = 0
    182
        ///
    183
        /// let y = x.layer_norm(&gamma, &beta, 1e-5).unwrap();
    184
        ///
    185
        /// // Output should be approximately standardized (mean ≈ 0, std ≈ 1)
    186
        /// let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
    187
        /// assert!(mean.abs() < 1e-5);
    188
        /// ```
    189
        ///
    190
        /// # Performance
    191
        ///
    192
        /// Single-pass computation using Welford's algorithm for numerical stability.
    193
        /// Time complexity: O(n), Space complexity: O(n).
    194
    0
        pub fn layer_norm(&self, gamma: &Self, beta: &Self, eps: f32) -> Result<Self> {
    195
    0
            if self.as_slice().is_empty() {
    196
    0
                return Err(TruenoError::EmptyVector);
    197
    0
            }
    198
    199
    0
            if self.len() != gamma.len() {
    200
    0
                return Err(TruenoError::SizeMismatch {
    201
    0
                    expected: self.len(),
    202
    0
                    actual: gamma.len(),
    203
    0
                });
    204
    0
            }
    205
    206
    0
            if self.len() != beta.len() {
    207
    0
                return Err(TruenoError::SizeMismatch {
    208
    0
                    expected: self.len(),
    209
    0
                    actual: beta.len(),
    210
    0
                });
    211
    0
            }
    212
    213
            // Compute mean
    214
    0
            let mean_val = self.mean()?;
    215
    216
            // Compute variance: E[(x - mean)^2]
    217
    0
            let variance: f32 = self
    218
    0
                .as_slice()
    219
    0
                .iter()
    220
    0
                .map(|&x| {
    221
    0
                    let diff = x - mean_val;
    222
    0
                    diff * diff
    223
    0
                })
    224
    0
                .sum::<f32>()
    225
    0
                / self.len() as f32;
    226
    227
            // Compute inverse standard deviation for numerical stability
    228
    0
            let inv_std = 1.0 / (variance + eps).sqrt();
    229
    230
            // Apply normalization: y = gamma * (x - mean) * inv_std + beta
    231
    0
            let data: Vec<f32> = self
    232
    0
                .as_slice()
    233
    0
                .iter()
    234
    0
                .zip(gamma.as_slice().iter())
    235
    0
                .zip(beta.as_slice().iter())
    236
    0
                .map(|((&x, &g), &b)| g * (x - mean_val) * inv_std + b)
    237
    0
                .collect();
    238
    239
    0
            Ok(Vector::from_vec(data))
    240
    0
        }
    241
    242
        /// Layer normalization without learnable parameters
    243
        ///
    244
        /// Simplified version that just standardizes the input: `y = (x - mean) / sqrt(variance + eps)`
    245
        ///
    246
        /// This is equivalent to calling `layer_norm` with gamma=1 and beta=0.
    247
        ///
    248
        /// # Arguments
    249
        ///
    250
        /// * `eps` - Small constant for numerical stability (typically 1e-5)
    251
        ///
    252
        /// # Example
    253
        ///
    254
        /// ```
    255
        /// use trueno::Vector;
    256
        ///
    257
        /// let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
    258
        /// let y = x.layer_norm_simple(1e-5).unwrap();
    259
        ///
    260
        /// // Output should be standardized
    261
        /// let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
    262
        /// assert!(mean.abs() < 1e-5);
    263
        /// ```
    264
    0
        pub fn layer_norm_simple(&self, eps: f32) -> Result<Self> {
    265
    0
            if self.as_slice().is_empty() {
    266
    0
                return Err(TruenoError::EmptyVector);
    267
    0
            }
    268
    269
    0
            let mean_val = self.mean()?;
    270
    271
            // Compute variance
    272
    0
            let variance: f32 = self
    273
    0
                .as_slice()
    274
    0
                .iter()
    275
    0
                .map(|&x| {
    276
    0
                    let diff = x - mean_val;
    277
    0
                    diff * diff
    278
    0
                })
    279
    0
                .sum::<f32>()
    280
    0
                / self.len() as f32;
    281
    282
    0
            let inv_std = 1.0 / (variance + eps).sqrt();
    283
    284
    0
            let data: Vec<f32> = self
    285
    0
                .as_slice()
    286
    0
                .iter()
    287
    0
                .map(|&x| (x - mean_val) * inv_std)
    288
    0
                .collect();
    289
    290
    0
            Ok(Vector::from_vec(data))
    291
    0
        }
    292
    293
        /// Normalize the vector to unit length (L2 norm = 1)
    294
        ///
    295
        /// Returns a new vector in the same direction but with magnitude 1.
    296
        ///
    297
        /// # Errors
    298
        ///
    299
        /// Returns `TruenoError::DivisionByZero` if the vector has zero norm (cannot normalize zero vector).
    300
        ///
    301
        /// # Examples
    302
        ///
    303
        /// ```
    304
        /// use trueno::Vector;
    305
        ///
    306
        /// let v = Vector::from_slice(&[3.0, 4.0]);
    307
        /// let unit = v.normalize().unwrap();
    308
        ///
    309
        /// // Result is [0.6, 0.8] (a unit vector)
    310
        /// assert!((unit.as_slice()[0] - 0.6).abs() < 1e-5);
    311
        /// assert!((unit.as_slice()[1] - 0.8).abs() < 1e-5);
    312
        ///
    313
        /// // Verify it's a unit vector (norm = 1)
    314
        /// assert!((unit.norm_l2().unwrap() - 1.0).abs() < 1e-5);
    315
        /// ```
    316
        ///
    317
        /// # Zero Vector Error
    318
        ///
    319
        /// ```
    320
        /// use trueno::{Vector, TruenoError};
    321
        ///
    322
        /// let v = Vector::from_slice(&[0.0, 0.0]);
    323
        /// assert!(matches!(v.normalize(), Err(TruenoError::DivisionByZero)));
    324
        /// ```
    325
    0
        pub fn normalize(&self) -> Result<Vector<f32>> {
    326
    0
            let norm = self.norm_l2()?;
    327
    328
            // Check for zero or near-zero norm (cannot normalize zero vector)
    329
    0
            if norm.abs() < 1e-10 {
    330
    0
                return Err(TruenoError::DivisionByZero);
    331
    0
            }
    332
    333
            // Divide each element by the norm using scalar multiplication
    334
            // This avoids creating an intermediate vector
    335
    0
            self.scale(1.0 / norm)
    336
    0
        }
    337
    }
    338
    339
    #[cfg(test)]
    340
    mod tests {
    341
        use super::*;
    342
    343
        #[test]
    344
        fn test_zscore_basic() {
    345
            let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
    346
            let z = v.zscore().unwrap();
    347
    348
            // Mean should be ~0
    349
            let mean = z.mean().unwrap();
    350
            assert!(mean.abs() < 1e-5);
    351
    352
            // Stddev should be ~1
    353
            let std = z.stddev().unwrap();
    354
            assert!((std - 1.0).abs() < 1e-5);
    355
        }
    356
    357
        #[test]
    358
        fn test_zscore_empty() {
    359
            let v: Vector<f32> = Vector::from_slice(&[]);
    360
            assert!(matches!(v.zscore(), Err(TruenoError::EmptyVector)));
    361
        }
    362
    363
        #[test]
    364
        fn test_zscore_constant() {
    365
            let v = Vector::from_slice(&[5.0, 5.0, 5.0]);
    366
            assert!(matches!(v.zscore(), Err(TruenoError::DivisionByZero)));
    367
        }
    368
    369
        #[test]
    370
        fn test_minmax_normalize_basic() {
    371
            let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
    372
            let normalized = v.minmax_normalize().unwrap();
    373
    374
            assert!((normalized.min().unwrap() - 0.0).abs() < 1e-5);
    375
            assert!((normalized.max().unwrap() - 1.0).abs() < 1e-5);
    376
        }
    377
    378
        #[test]
    379
        fn test_minmax_normalize_empty() {
    380
            let v: Vector<f32> = Vector::from_slice(&[]);
    381
            assert!(matches!(v.minmax_normalize(), Err(TruenoError::EmptyVector)));
    382
        }
    383
    384
        #[test]
    385
        fn test_minmax_normalize_constant() {
    386
            let v = Vector::from_slice(&[5.0, 5.0, 5.0]);
    387
            assert!(matches!(
    388
                v.minmax_normalize(),
    389
                Err(TruenoError::DivisionByZero)
    390
            ));
    391
        }
    392
    393
        #[test]
    394
        fn test_layer_norm() {
    395
            let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
    396
            let gamma = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0]);
    397
            let beta = Vector::from_slice(&[0.0, 0.0, 0.0, 0.0]);
    398
    399
            let y = x.layer_norm(&gamma, &beta, 1e-5).unwrap();
    400
    401
            // Mean should be ~0
    402
            let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
    403
            assert!(mean.abs() < 1e-5);
    404
        }
    405
    406
        #[test]
    407
        fn test_layer_norm_size_mismatch() {
    408
            let x = Vector::from_slice(&[1.0, 2.0, 3.0]);
    409
            let gamma = Vector::from_slice(&[1.0, 1.0]); // Wrong size
    410
            let beta = Vector::from_slice(&[0.0, 0.0, 0.0]);
    411
    412
            assert!(matches!(
    413
                x.layer_norm(&gamma, &beta, 1e-5),
    414
                Err(TruenoError::SizeMismatch { .. })
    415
            ));
    416
        }
    417
    418
        #[test]
    419
        fn test_layer_norm_simple() {
    420
            let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
    421
            let y = x.layer_norm_simple(1e-5).unwrap();
    422
    423
            let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
    424
            assert!(mean.abs() < 1e-5);
    425
        }
    426
    427
        #[test]
    428
        fn test_normalize_unit_vector() {
    429
            let v = Vector::from_slice(&[3.0, 4.0]);
    430
            let unit = v.normalize().unwrap();
    431
    432
            assert!((unit.as_slice()[0] - 0.6).abs() < 1e-5);
    433
            assert!((unit.as_slice()[1] - 0.8).abs() < 1e-5);
    434
            assert!((unit.norm_l2().unwrap() - 1.0).abs() < 1e-5);
    435
        }
    436
    437
        #[test]
    438
        fn test_normalize_zero_vector() {
    439
            let v = Vector::from_slice(&[0.0, 0.0]);
    440
            assert!(matches!(v.normalize(), Err(TruenoError::DivisionByZero)));
    441
        }
    442
    }