1#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum NormalizationType {
9 L1,
11 L2,
13 LInf,
15 MinMax,
17 ZScore,
19 UnitVariance,
21}
22
23#[derive(Debug, Clone)]
25pub struct NormalizerConfig {
26 pub norm_type: NormalizationType,
28 pub epsilon: f64,
30 pub target_norm: f64,
32 pub clip_min: Option<f64>,
34 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#[derive(Debug, Clone)]
52pub struct NormStats {
53 pub original_norm: f64,
55 pub normalized_norm: f64,
57 pub min_value: f64,
59 pub max_value: f64,
61 pub mean: f64,
63 pub std_dev: f64,
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct NormalizerStats {
70 pub total_normalized: u64,
72 pub total_dimensions: u64,
74 pub avg_original_norm: f64,
76 pub avg_normalized_norm: f64,
78}
79
80pub struct EmbeddingNormalizer {
82 config: NormalizerConfig,
83 stats: NormalizerStats,
84}
85
86impl EmbeddingNormalizer {
87 pub fn new(config: NormalizerConfig) -> Self {
89 Self {
90 config,
91 stats: NormalizerStats::default(),
92 }
93 }
94
95 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 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 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 pub fn l1_norm(v: &[f64]) -> f64 {
198 v.iter().map(|x| x.abs()).sum()
199 }
200
201 pub fn l2_norm(v: &[f64]) -> f64 {
203 v.iter().map(|x| x * x).sum::<f64>().sqrt()
204 }
205
206 pub fn linf_norm(v: &[f64]) -> f64 {
208 v.iter().map(|x| x.abs()).fold(0.0_f64, f64::max)
209 }
210
211 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 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 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 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 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 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 pub fn stats(&self) -> &NormalizerStats {
294 &self.stats
295 }
296
297 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 #[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 #[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]; 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 #[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 #[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 for val in &v {
441 assert!(val.is_finite());
442 }
443 }
444
445 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 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}