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
}