Skip to main content

ipfrs_semantic/
embedding_normalizer.rs

1//! Vector normalization and transformation for embeddings.
2//!
3//! Provides various normalization strategies (L1, L2, LInf, MinMax, ZScore, UnitVariance)
4//! for embedding vectors, along with clipping, cosine similarity, and statistics tracking.
5
6/// The type of normalization to apply.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum NormalizationType {
9    /// L1 (Manhattan) normalization: sum of absolute values equals target_norm.
10    L1,
11    /// L2 (Euclidean) normalization: Euclidean length equals target_norm.
12    L2,
13    /// L-infinity normalization: maximum absolute value equals target_norm.
14    LInf,
15    /// Min-max normalization: scales values to [0, 1].
16    MinMax,
17    /// Z-score normalization: transforms to mean=0, std_dev=1.
18    ZScore,
19    /// Unit variance normalization: scales so variance equals 1, preserving mean.
20    UnitVariance,
21}
22
23/// Configuration for the normalizer.
24#[derive(Debug, Clone)]
25pub struct NormalizerConfig {
26    /// The normalization strategy to use.
27    pub norm_type: NormalizationType,
28    /// Small value to avoid division by zero.
29    pub epsilon: f64,
30    /// For L1/L2/LInf: normalize to this magnitude.
31    pub target_norm: f64,
32    /// Optional lower bound for clipping.
33    pub clip_min: Option<f64>,
34    /// Optional upper bound for clipping.
35    pub clip_max: Option<f64>,
36}
37
38impl Default for NormalizerConfig {
39    fn default() -> Self {
40        Self {
41            norm_type: NormalizationType::L2,
42            epsilon: 1e-12,
43            target_norm: 1.0,
44            clip_min: None,
45            clip_max: None,
46        }
47    }
48}
49
50/// Statistics about a single normalization operation.
51#[derive(Debug, Clone)]
52pub struct NormStats {
53    /// Norm of the original vector (using the configured norm type).
54    pub original_norm: f64,
55    /// Norm of the normalized vector.
56    pub normalized_norm: f64,
57    /// Minimum value in the normalized vector.
58    pub min_value: f64,
59    /// Maximum value in the normalized vector.
60    pub max_value: f64,
61    /// Mean of the normalized vector.
62    pub mean: f64,
63    /// Standard deviation of the normalized vector.
64    pub std_dev: f64,
65}
66
67/// Aggregate statistics across multiple normalization operations.
68#[derive(Debug, Clone, Default)]
69pub struct NormalizerStats {
70    /// Total number of vectors normalized.
71    pub total_normalized: u64,
72    /// Total dimensions processed across all vectors.
73    pub total_dimensions: u64,
74    /// Running average of original norms.
75    pub avg_original_norm: f64,
76    /// Running average of normalized norms.
77    pub avg_normalized_norm: f64,
78}
79
80/// Embedding normalizer with configurable strategies and statistics tracking.
81pub struct EmbeddingNormalizer {
82    config: NormalizerConfig,
83    stats: NormalizerStats,
84}
85
86impl EmbeddingNormalizer {
87    /// Create a new normalizer with the given configuration.
88    pub fn new(config: NormalizerConfig) -> Self {
89        Self {
90            config,
91            stats: NormalizerStats::default(),
92        }
93    }
94
95    /// Normalize a single embedding vector in-place, returning statistics.
96    pub fn normalize(&mut self, embedding: &mut [f64]) -> NormStats {
97        let original = embedding.to_vec();
98
99        match self.config.norm_type {
100            NormalizationType::L1 => {
101                let norm = Self::l1_norm(embedding);
102                let divisor = if norm < self.config.epsilon {
103                    self.config.epsilon
104                } else {
105                    norm / self.config.target_norm
106                };
107                for v in embedding.iter_mut() {
108                    *v /= divisor;
109                }
110            }
111            NormalizationType::L2 => {
112                let norm = Self::l2_norm(embedding);
113                let divisor = if norm < self.config.epsilon {
114                    self.config.epsilon
115                } else {
116                    norm / self.config.target_norm
117                };
118                for v in embedding.iter_mut() {
119                    *v /= divisor;
120                }
121            }
122            NormalizationType::LInf => {
123                let norm = Self::linf_norm(embedding);
124                let divisor = if norm < self.config.epsilon {
125                    self.config.epsilon
126                } else {
127                    norm / self.config.target_norm
128                };
129                for v in embedding.iter_mut() {
130                    *v /= divisor;
131                }
132            }
133            NormalizationType::MinMax => {
134                let min_val = embedding.iter().copied().fold(f64::INFINITY, f64::min);
135                let max_val = embedding.iter().copied().fold(f64::NEG_INFINITY, f64::max);
136                let range = max_val - min_val;
137                let divisor = if range < self.config.epsilon {
138                    self.config.epsilon
139                } else {
140                    range
141                };
142                for v in embedding.iter_mut() {
143                    *v = (*v - min_val) / divisor;
144                }
145            }
146            NormalizationType::ZScore => {
147                let mean = Self::compute_mean(embedding);
148                let std_dev = Self::compute_std_dev(embedding, mean);
149                let divisor = if std_dev < self.config.epsilon {
150                    self.config.epsilon
151                } else {
152                    std_dev
153                };
154                for v in embedding.iter_mut() {
155                    *v = (*v - mean) / divisor;
156                }
157            }
158            NormalizationType::UnitVariance => {
159                let mean = Self::compute_mean(embedding);
160                let std_dev = Self::compute_std_dev(embedding, mean);
161                let divisor = if std_dev < self.config.epsilon {
162                    self.config.epsilon
163                } else {
164                    std_dev
165                };
166                for v in embedding.iter_mut() {
167                    *v /= divisor;
168                }
169            }
170        }
171
172        self.clip(embedding);
173
174        let norm_stats = Self::compute_norm_stats(&original, embedding);
175
176        // Update running statistics
177        self.stats.total_normalized += 1;
178        self.stats.total_dimensions += embedding.len() as u64;
179        let n = self.stats.total_normalized as f64;
180        self.stats.avg_original_norm +=
181            (norm_stats.original_norm - self.stats.avg_original_norm) / n;
182        self.stats.avg_normalized_norm +=
183            (norm_stats.normalized_norm - self.stats.avg_normalized_norm) / n;
184
185        norm_stats
186    }
187
188    /// Normalize a batch of embeddings, returning per-vector statistics.
189    pub fn normalize_batch(&mut self, embeddings: &mut [Vec<f64>]) -> Vec<NormStats> {
190        embeddings
191            .iter_mut()
192            .map(|emb| self.normalize(emb))
193            .collect()
194    }
195
196    /// Compute the L1 (Manhattan) norm of a vector.
197    pub fn l1_norm(v: &[f64]) -> f64 {
198        v.iter().map(|x| x.abs()).sum()
199    }
200
201    /// Compute the L2 (Euclidean) norm of a vector.
202    pub fn l2_norm(v: &[f64]) -> f64 {
203        v.iter().map(|x| x * x).sum::<f64>().sqrt()
204    }
205
206    /// Compute the L-infinity norm (maximum absolute value) of a vector.
207    pub fn linf_norm(v: &[f64]) -> f64 {
208        v.iter().map(|x| x.abs()).fold(0.0_f64, f64::max)
209    }
210
211    /// Compute the arithmetic mean of a vector.
212    pub fn compute_mean(v: &[f64]) -> f64 {
213        if v.is_empty() {
214            return 0.0;
215        }
216        v.iter().sum::<f64>() / v.len() as f64
217    }
218
219    /// Compute the population standard deviation given a precomputed mean.
220    pub fn compute_std_dev(v: &[f64], mean: f64) -> f64 {
221        if v.is_empty() {
222            return 0.0;
223        }
224        let variance = v.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / v.len() as f64;
225        variance.sqrt()
226    }
227
228    /// Clip embedding values to the configured bounds (if any).
229    pub fn clip(&self, embedding: &mut [f64]) {
230        if let Some(lo) = self.config.clip_min {
231            for v in embedding.iter_mut() {
232                if *v < lo {
233                    *v = lo;
234                }
235            }
236        }
237        if let Some(hi) = self.config.clip_max {
238            for v in embedding.iter_mut() {
239                if *v > hi {
240                    *v = hi;
241                }
242            }
243        }
244    }
245
246    /// Compute cosine similarity between two vectors.
247    ///
248    /// Returns 0.0 if either vector has zero norm.
249    pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
250        let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
251        let norm_a = Self::l2_norm(a);
252        let norm_b = Self::l2_norm(b);
253        let denom = norm_a * norm_b;
254        if denom < 1e-15 {
255            0.0
256        } else {
257            dot / denom
258        }
259    }
260
261    /// Compute comprehensive statistics comparing original and normalized vectors.
262    pub fn compute_norm_stats(original: &[f64], normalized: &[f64]) -> NormStats {
263        let original_norm = Self::l2_norm(original);
264        let normalized_norm = Self::l2_norm(normalized);
265        let min_value = normalized.iter().copied().fold(f64::INFINITY, f64::min);
266        let max_value = normalized.iter().copied().fold(f64::NEG_INFINITY, f64::max);
267        let mean = Self::compute_mean(normalized);
268        let std_dev = Self::compute_std_dev(normalized, mean);
269
270        // Handle empty vectors gracefully
271        let min_value = if min_value == f64::INFINITY {
272            0.0
273        } else {
274            min_value
275        };
276        let max_value = if max_value == f64::NEG_INFINITY {
277            0.0
278        } else {
279            max_value
280        };
281
282        NormStats {
283            original_norm,
284            normalized_norm,
285            min_value,
286            max_value,
287            mean,
288            std_dev,
289        }
290    }
291
292    /// Get a reference to the aggregate normalizer statistics.
293    pub fn stats(&self) -> &NormalizerStats {
294        &self.stats
295    }
296
297    /// Reset all aggregate statistics to their defaults.
298    pub fn reset_stats(&mut self) {
299        self.stats = NormalizerStats::default();
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    fn make_normalizer(norm_type: NormalizationType) -> EmbeddingNormalizer {
308        EmbeddingNormalizer::new(NormalizerConfig {
309            norm_type,
310            ..NormalizerConfig::default()
311        })
312    }
313
314    // ---- L1 normalization ----
315
316    #[test]
317    fn test_l1_normalization_basic() {
318        let mut n = make_normalizer(NormalizationType::L1);
319        let mut v = vec![1.0, -2.0, 3.0];
320        n.normalize(&mut v);
321        let l1 = EmbeddingNormalizer::l1_norm(&v);
322        assert!((l1 - 1.0).abs() < 1e-10, "L1 norm should be 1.0, got {l1}");
323    }
324
325    #[test]
326    fn test_l1_normalization_signs_preserved() {
327        let mut n = make_normalizer(NormalizationType::L1);
328        let mut v = vec![2.0, -4.0, 6.0];
329        n.normalize(&mut v);
330        assert!(v[0] > 0.0);
331        assert!(v[1] < 0.0);
332        assert!(v[2] > 0.0);
333    }
334
335    #[test]
336    fn test_l1_normalization_uniform() {
337        let mut n = make_normalizer(NormalizationType::L1);
338        let mut v = vec![1.0, 1.0, 1.0, 1.0];
339        n.normalize(&mut v);
340        for val in &v {
341            assert!((*val - 0.25).abs() < 1e-10);
342        }
343    }
344
345    // ---- L2 normalization ----
346
347    #[test]
348    fn test_l2_normalization_unit_vector() {
349        let mut n = make_normalizer(NormalizationType::L2);
350        let mut v = vec![3.0, 4.0];
351        n.normalize(&mut v);
352        let l2 = EmbeddingNormalizer::l2_norm(&v);
353        assert!((l2 - 1.0).abs() < 1e-10, "L2 norm should be 1.0, got {l2}");
354    }
355
356    #[test]
357    fn test_l2_normalization_already_unit() {
358        let mut n = make_normalizer(NormalizationType::L2);
359        let orig = vec![0.6, 0.8]; // already unit
360        let mut v = orig.clone();
361        n.normalize(&mut v);
362        for (a, b) in v.iter().zip(orig.iter()) {
363            assert!((a - b).abs() < 1e-10);
364        }
365    }
366
367    #[test]
368    fn test_l2_normalization_negative_values() {
369        let mut n = make_normalizer(NormalizationType::L2);
370        let mut v = vec![-3.0, -4.0];
371        n.normalize(&mut v);
372        let l2 = EmbeddingNormalizer::l2_norm(&v);
373        assert!((l2 - 1.0).abs() < 1e-10);
374    }
375
376    #[test]
377    fn test_l2_target_norm() {
378        let mut n = EmbeddingNormalizer::new(NormalizerConfig {
379            norm_type: NormalizationType::L2,
380            target_norm: 5.0,
381            ..NormalizerConfig::default()
382        });
383        let mut v = vec![3.0, 4.0];
384        n.normalize(&mut v);
385        let l2 = EmbeddingNormalizer::l2_norm(&v);
386        assert!((l2 - 5.0).abs() < 1e-10, "L2 norm should be 5.0, got {l2}");
387    }
388
389    // ---- LInf normalization ----
390
391    #[test]
392    fn test_linf_normalization() {
393        let mut n = make_normalizer(NormalizationType::LInf);
394        let mut v = vec![1.0, -5.0, 3.0];
395        n.normalize(&mut v);
396        let linf = EmbeddingNormalizer::linf_norm(&v);
397        assert!(
398            (linf - 1.0).abs() < 1e-10,
399            "LInf norm should be 1.0, got {linf}"
400        );
401    }
402
403    #[test]
404    fn test_linf_normalization_positive() {
405        let mut n = make_normalizer(NormalizationType::LInf);
406        let mut v = vec![2.0, 4.0, 8.0];
407        n.normalize(&mut v);
408        assert!((v[2] - 1.0).abs() < 1e-10, "Max element should be 1.0");
409        assert!((v[0] - 0.25).abs() < 1e-10);
410    }
411
412    // ---- MinMax normalization ----
413
414    #[test]
415    fn test_minmax_to_zero_one() {
416        let mut n = make_normalizer(NormalizationType::MinMax);
417        let mut v = vec![10.0, 20.0, 30.0, 40.0];
418        n.normalize(&mut v);
419        assert!((v[0] - 0.0).abs() < 1e-10, "Min should map to 0.0");
420        assert!((v[3] - 1.0).abs() < 1e-10, "Max should map to 1.0");
421        assert!((v[1] - 1.0 / 3.0).abs() < 1e-10);
422    }
423
424    #[test]
425    fn test_minmax_negative_range() {
426        let mut n = make_normalizer(NormalizationType::MinMax);
427        let mut v = vec![-10.0, 0.0, 10.0];
428        n.normalize(&mut v);
429        assert!((v[0] - 0.0).abs() < 1e-10);
430        assert!((v[1] - 0.5).abs() < 1e-10);
431        assert!((v[2] - 1.0).abs() < 1e-10);
432    }
433
434    #[test]
435    fn test_minmax_all_same() {
436        let mut n = make_normalizer(NormalizationType::MinMax);
437        let mut v = vec![5.0, 5.0, 5.0];
438        n.normalize(&mut v);
439        // All same => range ~ 0, epsilon kicks in, all values should be near 0
440        for val in &v {
441            assert!(val.is_finite());
442        }
443    }
444
445    // ---- ZScore normalization ----
446
447    #[test]
448    fn test_zscore_mean_zero() {
449        let mut n = make_normalizer(NormalizationType::ZScore);
450        let mut v = vec![2.0, 4.0, 6.0, 8.0, 10.0];
451        n.normalize(&mut v);
452        let mean = EmbeddingNormalizer::compute_mean(&v);
453        assert!(mean.abs() < 1e-10, "Mean should be ~0, got {mean}");
454    }
455
456    #[test]
457    fn test_zscore_unit_variance() {
458        let mut n = make_normalizer(NormalizationType::ZScore);
459        let mut v = vec![2.0, 4.0, 6.0, 8.0, 10.0];
460        n.normalize(&mut v);
461        let mean = EmbeddingNormalizer::compute_mean(&v);
462        let std_dev = EmbeddingNormalizer::compute_std_dev(&v, mean);
463        assert!(
464            (std_dev - 1.0).abs() < 1e-10,
465            "Std dev should be ~1.0, got {std_dev}"
466        );
467    }
468
469    #[test]
470    fn test_zscore_symmetric() {
471        let mut n = make_normalizer(NormalizationType::ZScore);
472        let mut v = vec![-3.0, -1.0, 1.0, 3.0];
473        n.normalize(&mut v);
474        let mean = EmbeddingNormalizer::compute_mean(&v);
475        assert!(mean.abs() < 1e-10);
476    }
477
478    // ---- UnitVariance normalization ----
479
480    #[test]
481    fn test_unit_variance() {
482        let mut n = make_normalizer(NormalizationType::UnitVariance);
483        let mut v = vec![2.0, 4.0, 6.0, 8.0];
484        n.normalize(&mut v);
485        let mean = EmbeddingNormalizer::compute_mean(&v);
486        let std_dev = EmbeddingNormalizer::compute_std_dev(&v, mean);
487        assert!(
488            (std_dev - 1.0).abs() < 1e-10,
489            "Std dev should be ~1.0, got {std_dev}"
490        );
491    }
492
493    #[test]
494    fn test_unit_variance_preserves_relative_ordering() {
495        let mut n = make_normalizer(NormalizationType::UnitVariance);
496        let mut v = vec![1.0, 3.0, 5.0, 7.0];
497        n.normalize(&mut v);
498        for i in 0..v.len() - 1 {
499            assert!(v[i] < v[i + 1], "Ordering should be preserved");
500        }
501    }
502
503    // ---- Clipping ----
504
505    #[test]
506    fn test_clipping_both_bounds() {
507        let mut n = EmbeddingNormalizer::new(NormalizerConfig {
508            norm_type: NormalizationType::L2,
509            clip_min: Some(-0.5),
510            clip_max: Some(0.5),
511            ..NormalizerConfig::default()
512        });
513        let mut v = vec![10.0, -10.0, 0.1];
514        n.normalize(&mut v);
515        for val in &v {
516            assert!(*val >= -0.5 && *val <= 0.5, "Value {val} out of clip range");
517        }
518    }
519
520    #[test]
521    fn test_clipping_min_only() {
522        let mut n = EmbeddingNormalizer::new(NormalizerConfig {
523            norm_type: NormalizationType::L2,
524            clip_min: Some(0.0),
525            clip_max: None,
526            ..NormalizerConfig::default()
527        });
528        let mut v = vec![3.0, -4.0];
529        n.normalize(&mut v);
530        for val in &v {
531            assert!(*val >= 0.0, "Value {val} should be >= 0.0");
532        }
533    }
534
535    #[test]
536    fn test_clipping_max_only() {
537        let mut n = EmbeddingNormalizer::new(NormalizerConfig {
538            norm_type: NormalizationType::L2,
539            clip_min: None,
540            clip_max: Some(0.3),
541            ..NormalizerConfig::default()
542        });
543        let mut v = vec![3.0, 4.0];
544        n.normalize(&mut v);
545        for val in &v {
546            assert!(*val <= 0.3 + 1e-10, "Value {val} should be <= 0.3");
547        }
548    }
549
550    // ---- Batch normalization ----
551
552    #[test]
553    fn test_batch_normalization() {
554        let mut n = make_normalizer(NormalizationType::L2);
555        let mut batch = vec![vec![3.0, 4.0], vec![1.0, 0.0], vec![0.0, -5.0]];
556        let stats_vec = n.normalize_batch(&mut batch);
557        assert_eq!(stats_vec.len(), 3);
558        for emb in &batch {
559            let l2 = EmbeddingNormalizer::l2_norm(emb);
560            assert!((l2 - 1.0).abs() < 1e-10);
561        }
562    }
563
564    #[test]
565    fn test_batch_stats_tracking() {
566        let mut n = make_normalizer(NormalizationType::L2);
567        let mut batch = vec![vec![3.0, 4.0], vec![6.0, 8.0]];
568        n.normalize_batch(&mut batch);
569        assert_eq!(n.stats().total_normalized, 2);
570        assert_eq!(n.stats().total_dimensions, 4);
571    }
572
573    // ---- Zero vector handling ----
574
575    #[test]
576    fn test_zero_vector_l2() {
577        let mut n = make_normalizer(NormalizationType::L2);
578        let mut v = vec![0.0, 0.0, 0.0];
579        let stats = n.normalize(&mut v);
580        // Should not panic; values may be very small but finite
581        for val in &v {
582            assert!(val.is_finite(), "Expected finite, got {val}");
583        }
584        assert!(stats.original_norm.abs() < 1e-10);
585    }
586
587    #[test]
588    fn test_zero_vector_minmax() {
589        let mut n = make_normalizer(NormalizationType::MinMax);
590        let mut v = vec![0.0, 0.0, 0.0];
591        n.normalize(&mut v);
592        for val in &v {
593            assert!(val.is_finite());
594        }
595    }
596
597    #[test]
598    fn test_zero_vector_zscore() {
599        let mut n = make_normalizer(NormalizationType::ZScore);
600        let mut v = vec![0.0, 0.0, 0.0];
601        n.normalize(&mut v);
602        for val in &v {
603            assert!(val.is_finite());
604        }
605    }
606
607    // ---- Cosine similarity ----
608
609    #[test]
610    fn test_cosine_similarity_identical() {
611        let a = vec![1.0, 2.0, 3.0];
612        let sim = EmbeddingNormalizer::cosine_similarity(&a, &a);
613        assert!(
614            (sim - 1.0).abs() < 1e-10,
615            "Identical vectors => similarity 1.0"
616        );
617    }
618
619    #[test]
620    fn test_cosine_similarity_orthogonal() {
621        let a = vec![1.0, 0.0];
622        let b = vec![0.0, 1.0];
623        let sim = EmbeddingNormalizer::cosine_similarity(&a, &b);
624        assert!(sim.abs() < 1e-10, "Orthogonal => similarity 0.0, got {sim}");
625    }
626
627    #[test]
628    fn test_cosine_similarity_opposite() {
629        let a = vec![1.0, 2.0, 3.0];
630        let b = vec![-1.0, -2.0, -3.0];
631        let sim = EmbeddingNormalizer::cosine_similarity(&a, &b);
632        assert!((sim + 1.0).abs() < 1e-10, "Opposite => similarity -1.0");
633    }
634
635    #[test]
636    fn test_cosine_similarity_zero_vector() {
637        let a = vec![1.0, 2.0];
638        let b = vec![0.0, 0.0];
639        let sim = EmbeddingNormalizer::cosine_similarity(&a, &b);
640        assert!(sim.abs() < 1e-10, "Zero vector => similarity 0.0");
641    }
642
643    // ---- Stats tracking ----
644
645    #[test]
646    fn test_stats_initial() {
647        let n = make_normalizer(NormalizationType::L2);
648        assert_eq!(n.stats().total_normalized, 0);
649        assert_eq!(n.stats().total_dimensions, 0);
650    }
651
652    #[test]
653    fn test_stats_after_normalize() {
654        let mut n = make_normalizer(NormalizationType::L2);
655        let mut v = vec![3.0, 4.0];
656        n.normalize(&mut v);
657        assert_eq!(n.stats().total_normalized, 1);
658        assert_eq!(n.stats().total_dimensions, 2);
659        assert!((n.stats().avg_original_norm - 5.0).abs() < 1e-10);
660        assert!((n.stats().avg_normalized_norm - 1.0).abs() < 1e-10);
661    }
662
663    #[test]
664    fn test_reset_stats() {
665        let mut n = make_normalizer(NormalizationType::L2);
666        let mut v = vec![3.0, 4.0];
667        n.normalize(&mut v);
668        n.reset_stats();
669        assert_eq!(n.stats().total_normalized, 0);
670        assert_eq!(n.stats().total_dimensions, 0);
671    }
672
673    // ---- High-dimensional vectors ----
674
675    #[test]
676    fn test_high_dimensional_l2() {
677        let mut n = make_normalizer(NormalizationType::L2);
678        let mut v: Vec<f64> = (0..768).map(|i| (i as f64) * 0.01).collect();
679        n.normalize(&mut v);
680        let l2 = EmbeddingNormalizer::l2_norm(&v);
681        assert!(
682            (l2 - 1.0).abs() < 1e-8,
683            "High-dim L2 norm should be 1.0, got {l2}"
684        );
685    }
686
687    #[test]
688    fn test_high_dimensional_zscore() {
689        let mut n = make_normalizer(NormalizationType::ZScore);
690        let mut v: Vec<f64> = (0..512).map(|i| (i as f64) * 0.1 - 25.0).collect();
691        n.normalize(&mut v);
692        let mean = EmbeddingNormalizer::compute_mean(&v);
693        assert!(mean.abs() < 1e-8, "High-dim mean should be ~0, got {mean}");
694    }
695
696    // ---- Norm preservation after L2 ----
697
698    #[test]
699    fn test_norm_preservation_after_l2() {
700        let mut n = make_normalizer(NormalizationType::L2);
701        let mut v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
702        n.normalize(&mut v);
703        let l2 = EmbeddingNormalizer::l2_norm(&v);
704        assert!((l2 - 1.0).abs() < 1e-10);
705
706        // Normalizing again should keep it at 1.0
707        n.normalize(&mut v);
708        let l2_again = EmbeddingNormalizer::l2_norm(&v);
709        assert!(
710            (l2_again - 1.0).abs() < 1e-10,
711            "Idempotent L2 normalization"
712        );
713    }
714
715    #[test]
716    fn test_norm_stats_fields() {
717        let mut n = make_normalizer(NormalizationType::L2);
718        let mut v = vec![3.0, 4.0];
719        let stats = n.normalize(&mut v);
720        assert!((stats.original_norm - 5.0).abs() < 1e-10);
721        assert!((stats.normalized_norm - 1.0).abs() < 1e-10);
722        assert!(stats.min_value <= stats.max_value);
723        assert!(stats.std_dev >= 0.0);
724    }
725
726    #[test]
727    fn test_compute_norm_stats_empty() {
728        let stats = EmbeddingNormalizer::compute_norm_stats(&[], &[]);
729        assert!((stats.original_norm).abs() < 1e-10);
730        assert!((stats.mean).abs() < 1e-10);
731    }
732
733    #[test]
734    fn test_single_element_vector() {
735        let mut n = make_normalizer(NormalizationType::L2);
736        let mut v = vec![7.0];
737        n.normalize(&mut v);
738        assert!((v[0] - 1.0).abs() < 1e-10);
739    }
740
741    #[test]
742    fn test_l1_norm_function() {
743        let v = vec![1.0, -2.0, 3.0];
744        assert!((EmbeddingNormalizer::l1_norm(&v) - 6.0).abs() < 1e-10);
745    }
746
747    #[test]
748    fn test_l2_norm_function() {
749        let v = vec![3.0, 4.0];
750        assert!((EmbeddingNormalizer::l2_norm(&v) - 5.0).abs() < 1e-10);
751    }
752
753    #[test]
754    fn test_linf_norm_function() {
755        let v = vec![1.0, -7.0, 3.0];
756        assert!((EmbeddingNormalizer::linf_norm(&v) - 7.0).abs() < 1e-10);
757    }
758
759    #[test]
760    fn test_compute_mean_function() {
761        let v = vec![2.0, 4.0, 6.0];
762        assert!((EmbeddingNormalizer::compute_mean(&v) - 4.0).abs() < 1e-10);
763    }
764
765    #[test]
766    fn test_compute_std_dev_function() {
767        // std_dev of [2,4,6] with mean 4 => sqrt((4+0+4)/3) = sqrt(8/3) ≈ 1.6329...
768        let v = vec![2.0, 4.0, 6.0];
769        let sd = EmbeddingNormalizer::compute_std_dev(&v, 4.0);
770        let expected = (8.0_f64 / 3.0).sqrt();
771        assert!((sd - expected).abs() < 1e-10);
772    }
773}