1use std::collections::VecDeque;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum AggregatorError {
38 EmptyInput,
40 DimensionMismatch {
42 expected: usize,
44 got: usize,
46 },
47 WeightCountMismatch {
49 expected: usize,
51 got: usize,
53 },
54 InvalidQuery,
56}
57
58impl std::fmt::Display for AggregatorError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::EmptyInput => write!(f, "no input embeddings were provided"),
62 Self::DimensionMismatch { expected, got } => {
63 write!(f, "dimension mismatch: expected {expected}, got {got}")
64 }
65 Self::WeightCountMismatch { expected, got } => write!(
66 f,
67 "weight count mismatch: expected {expected} weights, got {got}"
68 ),
69 Self::InvalidQuery => {
70 write!(f, "attention query vector is invalid or has wrong length")
71 }
72 }
73 }
74}
75
76impl std::error::Error for AggregatorError {}
77
78#[derive(Debug, Clone)]
84pub struct AggregationInput {
85 pub id: String,
87 pub embedding: Vec<f64>,
89 pub weight: f64,
92}
93
94impl AggregationInput {
95 pub fn new(id: impl Into<String>, embedding: Vec<f64>, weight: f64) -> Self {
97 Self {
98 id: id.into(),
99 embedding,
100 weight,
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
111pub enum AggregationMethod {
112 Mean,
114 WeightedMean {
117 weights: Vec<f64>,
119 },
120 Max,
122 Min,
124 Sum,
126 GeometricMean,
128 AttentionPooling {
130 query: Vec<f64>,
132 },
133}
134
135impl AggregationMethod {
136 pub fn name(&self) -> &'static str {
138 match self {
139 Self::Mean => "Mean",
140 Self::WeightedMean { .. } => "WeightedMean",
141 Self::Max => "Max",
142 Self::Min => "Min",
143 Self::Sum => "Sum",
144 Self::GeometricMean => "GeometricMean",
145 Self::AttentionPooling { .. } => "AttentionPooling",
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
156pub struct AggregationResult {
157 pub method: String,
159 pub output: Vec<f64>,
161 pub input_count: usize,
163 pub dims: usize,
165 pub norm: f64,
167}
168
169#[derive(Debug, Clone, Default)]
175pub struct EmbeddingAggregatorConfig {
176 pub normalize_output: bool,
178 pub handle_zeros: bool,
181}
182
183#[derive(Debug, Clone)]
189pub struct EaAggregatorStats {
190 pub total_aggregations: u64,
192 pub avg_input_count: f64,
194 pub avg_output_norm: f64,
196 pub method_name: String,
198}
199
200pub struct EmbeddingAggregator {
209 pub config: EmbeddingAggregatorConfig,
211 pub method: AggregationMethod,
213 pub history: VecDeque<AggregationResult>,
215 pub max_history: usize,
217}
218
219impl EmbeddingAggregator {
220 pub fn new(method: AggregationMethod, config: EmbeddingAggregatorConfig) -> Self {
224 Self {
225 config,
226 method,
227 history: VecDeque::new(),
228 max_history: 1_000,
229 }
230 }
231
232 pub fn with_history_capacity(
234 method: AggregationMethod,
235 config: EmbeddingAggregatorConfig,
236 max_history: usize,
237 ) -> Self {
238 Self {
239 config,
240 method,
241 history: VecDeque::with_capacity(max_history.min(1_000_000)),
242 max_history,
243 }
244 }
245
246 pub fn aggregate(
255 &mut self,
256 inputs: &[AggregationInput],
257 ) -> Result<AggregationResult, AggregatorError> {
258 if inputs.is_empty() {
259 return Err(AggregatorError::EmptyInput);
260 }
261
262 let dims = inputs[0].embedding.len();
263 for inp in inputs.iter().skip(1) {
264 if inp.embedding.len() != dims {
265 return Err(AggregatorError::DimensionMismatch {
266 expected: dims,
267 got: inp.embedding.len(),
268 });
269 }
270 }
271
272 let embeddings: Vec<Vec<f64>> = if self.config.handle_zeros {
274 inputs
275 .iter()
276 .map(|inp| {
277 if Self::l2_norm(&inp.embedding) < f64::EPSILON {
278 vec![1.0 / (dims as f64).sqrt(); dims]
279 } else {
280 inp.embedding.clone()
281 }
282 })
283 .collect()
284 } else {
285 inputs.iter().map(|inp| inp.embedding.clone()).collect()
286 };
287
288 let method_name = self.method.name().to_owned();
289 let output = match &self.method {
290 AggregationMethod::Mean => compute_mean(&embeddings),
291 AggregationMethod::WeightedMean { weights } => {
292 if weights.len() != inputs.len() {
293 return Err(AggregatorError::WeightCountMismatch {
294 expected: inputs.len(),
295 got: weights.len(),
296 });
297 }
298 compute_weighted_mean(&embeddings, weights)
299 }
300 AggregationMethod::Max => compute_max(&embeddings),
301 AggregationMethod::Min => compute_min(&embeddings),
302 AggregationMethod::Sum => compute_sum(&embeddings),
303 AggregationMethod::GeometricMean => compute_geometric_mean(&embeddings),
304 AggregationMethod::AttentionPooling { query } => {
305 if query.len() != dims {
306 return Err(AggregatorError::InvalidQuery);
307 }
308 let scores = attention_scores_impl(query, &embeddings);
309 compute_weighted_mean_with_weights(&embeddings, &scores)
310 }
311 };
312
313 let output = if self.config.normalize_output {
322 Self::l2_normalize(&output)
323 } else {
324 output
325 };
326
327 let norm = Self::l2_norm(&output);
328 let result = AggregationResult {
329 method: method_name,
330 output,
331 input_count: inputs.len(),
332 dims,
333 norm,
334 };
335
336 self.push_history(result.clone());
337 Ok(result)
338 }
339
340 pub fn aggregate_raw(
343 &mut self,
344 embeddings: &[Vec<f64>],
345 ) -> Result<AggregationResult, AggregatorError> {
346 let inputs: Vec<AggregationInput> = embeddings
347 .iter()
348 .enumerate()
349 .map(|(i, e)| AggregationInput::new(format!("raw_{i}"), e.clone(), 1.0))
350 .collect();
351 self.aggregate(&inputs)
352 }
353
354 pub fn aggregate_with_input_weights(
358 &mut self,
359 inputs: &[AggregationInput],
360 ) -> Result<AggregationResult, AggregatorError> {
361 if inputs.is_empty() {
362 return Err(AggregatorError::EmptyInput);
363 }
364 let dims = inputs[0].embedding.len();
365 for inp in inputs.iter().skip(1) {
366 if inp.embedding.len() != dims {
367 return Err(AggregatorError::DimensionMismatch {
368 expected: dims,
369 got: inp.embedding.len(),
370 });
371 }
372 }
373 let embeddings: Vec<Vec<f64>> = inputs.iter().map(|i| i.embedding.clone()).collect();
374 let weights: Vec<f64> = inputs.iter().map(|i| i.weight).collect();
375 let output = compute_weighted_mean(&embeddings, &weights);
376 let output = if self.config.normalize_output {
377 Self::l2_normalize(&output)
378 } else {
379 output
380 };
381 let norm = Self::l2_norm(&output);
382 let result = AggregationResult {
383 method: "WeightedMeanInputWeights".to_owned(),
384 output,
385 input_count: inputs.len(),
386 dims,
387 norm,
388 };
389 self.push_history(result.clone());
390 Ok(result)
391 }
392
393 pub fn merge_results(
396 results: &[AggregationResult],
397 method: AggregationMethod,
398 ) -> Result<AggregationResult, AggregatorError> {
399 if results.is_empty() {
400 return Err(AggregatorError::EmptyInput);
401 }
402 let dims = results[0].dims;
403 for r in results.iter().skip(1) {
404 if r.dims != dims {
405 return Err(AggregatorError::DimensionMismatch {
406 expected: dims,
407 got: r.dims,
408 });
409 }
410 }
411 let embeddings: Vec<Vec<f64>> = results.iter().map(|r| r.output.clone()).collect();
412 let method_name = method.name().to_owned();
413 let output = match &method {
414 AggregationMethod::Mean => compute_mean(&embeddings),
415 AggregationMethod::WeightedMean { weights } => {
416 if weights.len() != results.len() {
417 return Err(AggregatorError::WeightCountMismatch {
418 expected: results.len(),
419 got: weights.len(),
420 });
421 }
422 compute_weighted_mean(&embeddings, weights)
423 }
424 AggregationMethod::Max => compute_max(&embeddings),
425 AggregationMethod::Min => compute_min(&embeddings),
426 AggregationMethod::Sum => compute_sum(&embeddings),
427 AggregationMethod::GeometricMean => compute_geometric_mean(&embeddings),
428 AggregationMethod::AttentionPooling { query } => {
429 if query.len() != dims {
430 return Err(AggregatorError::InvalidQuery);
431 }
432 let scores = attention_scores_impl(query, &embeddings);
433 compute_weighted_mean_with_weights(&embeddings, &scores)
434 }
435 };
436 let norm = Self::l2_norm(&output);
437 Ok(AggregationResult {
438 method: method_name,
439 output,
440 input_count: results.len(),
441 dims,
442 norm,
443 })
444 }
445
446 pub fn l2_normalize(v: &[f64]) -> Vec<f64> {
452 let n = Self::l2_norm(v);
453 if n < f64::EPSILON {
454 return v.to_vec();
455 }
456 v.iter().map(|x| x / n).collect()
457 }
458
459 pub fn l2_norm(v: &[f64]) -> f64 {
461 v.iter().map(|x| x * x).sum::<f64>().sqrt()
462 }
463
464 pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
468 let na = Self::l2_norm(a);
469 let nb = Self::l2_norm(b);
470 if na < f64::EPSILON || nb < f64::EPSILON {
471 return 0.0;
472 }
473 let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
474 (dot / (na * nb)).clamp(-1.0, 1.0)
475 }
476
477 pub fn attention_scores(query: &[f64], embeddings: &[Vec<f64>]) -> Vec<f64> {
479 attention_scores_impl(query, embeddings)
480 }
481
482 pub fn recent_history(&self, n: usize) -> Vec<&AggregationResult> {
488 let skip = self.history.len().saturating_sub(n);
489 self.history.iter().skip(skip).collect()
490 }
491
492 pub fn stats(&self) -> EaAggregatorStats {
494 let total = self.history.len() as u64;
495 let avg_input_count = if self.history.is_empty() {
496 0.0
497 } else {
498 self.history
499 .iter()
500 .map(|r| r.input_count as f64)
501 .sum::<f64>()
502 / total as f64
503 };
504 let avg_output_norm = if self.history.is_empty() {
505 0.0
506 } else {
507 self.history.iter().map(|r| r.norm).sum::<f64>() / total as f64
508 };
509 EaAggregatorStats {
510 total_aggregations: total,
511 avg_input_count,
512 avg_output_norm,
513 method_name: self.method.name().to_owned(),
514 }
515 }
516
517 fn push_history(&mut self, result: AggregationResult) {
522 if self.history.len() >= self.max_history && self.max_history > 0 {
523 self.history.pop_front();
524 }
525 if self.max_history > 0 {
526 self.history.push_back(result);
527 }
528 }
529}
530
531fn compute_mean(embeddings: &[Vec<f64>]) -> Vec<f64> {
536 let n = embeddings.len() as f64;
537 let dims = embeddings[0].len();
538 let mut out = vec![0.0_f64; dims];
539 for emb in embeddings {
540 for (j, v) in emb.iter().enumerate() {
541 out[j] += v;
542 }
543 }
544 out.iter_mut().for_each(|x| *x /= n);
545 out
546}
547
548fn compute_weighted_mean(embeddings: &[Vec<f64>], weights: &[f64]) -> Vec<f64> {
549 let total: f64 = weights.iter().sum();
550 let denom = if total.abs() < f64::EPSILON {
551 1.0
552 } else {
553 total
554 };
555 let dims = embeddings[0].len();
556 let mut out = vec![0.0_f64; dims];
557 for (emb, &w) in embeddings.iter().zip(weights.iter()) {
558 for (j, v) in emb.iter().enumerate() {
559 out[j] += w * v;
560 }
561 }
562 out.iter_mut().for_each(|x| *x /= denom);
563 out
564}
565
566fn compute_weighted_mean_with_weights(embeddings: &[Vec<f64>], weights: &[f64]) -> Vec<f64> {
567 compute_weighted_mean(embeddings, weights)
568}
569
570fn compute_max(embeddings: &[Vec<f64>]) -> Vec<f64> {
571 let dims = embeddings[0].len();
572 let mut out = vec![f64::NEG_INFINITY; dims];
573 for emb in embeddings {
574 for (j, &v) in emb.iter().enumerate() {
575 if v > out[j] {
576 out[j] = v;
577 }
578 }
579 }
580 out
581}
582
583fn compute_min(embeddings: &[Vec<f64>]) -> Vec<f64> {
584 let dims = embeddings[0].len();
585 let mut out = vec![f64::INFINITY; dims];
586 for emb in embeddings {
587 for (j, &v) in emb.iter().enumerate() {
588 if v < out[j] {
589 out[j] = v;
590 }
591 }
592 }
593 out
594}
595
596fn compute_sum(embeddings: &[Vec<f64>]) -> Vec<f64> {
597 let dims = embeddings[0].len();
598 let mut out = vec![0.0_f64; dims];
599 for emb in embeddings {
600 for (j, &v) in emb.iter().enumerate() {
601 out[j] += v;
602 }
603 }
604 out
605}
606
607fn compute_geometric_mean(embeddings: &[Vec<f64>]) -> Vec<f64> {
609 const EPS: f64 = 1e-10;
610 let n = embeddings.len() as f64;
611 let dims = embeddings[0].len();
612 let mut out = vec![0.0_f64; dims];
613
614 for j in 0..dims {
615 let arith_mean: f64 = embeddings.iter().map(|e| e[j]).sum::<f64>() / n;
617 let sign = if arith_mean >= 0.0 { 1.0_f64 } else { -1.0_f64 };
618
619 let log_mean: f64 = embeddings
621 .iter()
622 .map(|e| (e[j].abs() + EPS).ln())
623 .sum::<f64>()
624 / n;
625 out[j] = log_mean.exp() * sign;
626 }
627 out
628}
629
630fn attention_scores_impl(query: &[f64], embeddings: &[Vec<f64>]) -> Vec<f64> {
632 let dims = query.len() as f64;
633 let scale = dims.sqrt();
634
635 let raw: Vec<f64> = embeddings
636 .iter()
637 .map(|emb| {
638 let dot: f64 = query.iter().zip(emb.iter()).map(|(q, e)| q * e).sum();
639 dot / scale
640 })
641 .collect();
642
643 softmax(&raw)
644}
645
646fn softmax(logits: &[f64]) -> Vec<f64> {
647 if logits.is_empty() {
648 return Vec::new();
649 }
650 let max_val = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
651 let exps: Vec<f64> = logits.iter().map(|&x| (x - max_val).exp()).collect();
652 let sum: f64 = exps.iter().sum();
653 let denom = if sum < f64::EPSILON { 1.0 } else { sum };
654 exps.iter().map(|e| e / denom).collect()
655}
656
657#[cfg(test)]
662mod tests {
663 use super::{
664 AggregationInput, AggregationMethod, AggregationResult, AggregatorError,
665 EmbeddingAggregator, EmbeddingAggregatorConfig,
666 };
667
668 fn uniform(val: f64, dim: usize) -> Vec<f64> {
670 vec![val; dim]
671 }
672
673 fn make_agg(method: AggregationMethod) -> EmbeddingAggregator {
674 EmbeddingAggregator::new(method, EmbeddingAggregatorConfig::default())
675 }
676
677 fn make_inputs(embeddings: Vec<Vec<f64>>) -> Vec<AggregationInput> {
678 embeddings
679 .into_iter()
680 .enumerate()
681 .map(|(i, e)| AggregationInput::new(format!("i{i}"), e, 1.0))
682 .collect()
683 }
684
685 fn result_near(result: &AggregationResult, expected: &[f64], tol: f64) -> bool {
686 result
687 .output
688 .iter()
689 .zip(expected.iter())
690 .all(|(a, b)| (a - b).abs() < tol)
691 }
692
693 #[test]
698 fn test_mean_basic() {
699 let mut agg = make_agg(AggregationMethod::Mean);
700 let inputs = make_inputs(vec![vec![2.0, 4.0], vec![0.0, 0.0]]);
701 let r = agg
702 .aggregate(&inputs)
703 .expect("test: aggregate should succeed");
704 assert!(result_near(&r, &[1.0, 2.0], 1e-10));
705 }
706
707 #[test]
708 fn test_mean_single_input() {
709 let mut agg = make_agg(AggregationMethod::Mean);
710 let inputs = make_inputs(vec![vec![3.0, 7.0, 1.0]]);
711 let r = agg
712 .aggregate(&inputs)
713 .expect("test: aggregate should succeed");
714 assert!(result_near(&r, &[3.0, 7.0, 1.0], 1e-10));
715 }
716
717 #[test]
718 fn test_mean_three_equal_vecs() {
719 let mut agg = make_agg(AggregationMethod::Mean);
720 let inputs = make_inputs(vec![vec![1.0, 2.0], vec![1.0, 2.0], vec![1.0, 2.0]]);
721 let r = agg
722 .aggregate(&inputs)
723 .expect("test: aggregate should succeed");
724 assert!(result_near(&r, &[1.0, 2.0], 1e-10));
725 }
726
727 #[test]
732 fn test_weighted_mean_basic() {
733 let mut agg = make_agg(AggregationMethod::WeightedMean {
734 weights: vec![3.0, 1.0],
735 });
736 let inputs = make_inputs(vec![vec![1.0, 0.0], vec![0.0, 1.0]]);
737 let r = agg
738 .aggregate(&inputs)
739 .expect("test: aggregate should succeed");
740 assert!((r.output[0] - 0.75).abs() < 1e-10);
742 assert!((r.output[1] - 0.25).abs() < 1e-10);
743 }
744
745 #[test]
746 fn test_weighted_mean_equal_weights() {
747 let mut agg = make_agg(AggregationMethod::WeightedMean {
748 weights: vec![1.0, 1.0],
749 });
750 let inputs = make_inputs(vec![vec![2.0, 4.0], vec![0.0, 0.0]]);
751 let r = agg
752 .aggregate(&inputs)
753 .expect("test: aggregate should succeed");
754 assert!(result_near(&r, &[1.0, 2.0], 1e-10));
755 }
756
757 #[test]
758 fn test_weighted_mean_count_mismatch_error() {
759 let mut agg = make_agg(AggregationMethod::WeightedMean {
760 weights: vec![1.0, 2.0, 3.0],
761 });
762 let inputs = make_inputs(vec![vec![1.0], vec![2.0]]);
763 let err = agg
764 .aggregate(&inputs)
765 .expect_err("test: aggregate should return error");
766 assert_eq!(
767 err,
768 AggregatorError::WeightCountMismatch {
769 expected: 2,
770 got: 3,
771 }
772 );
773 }
774
775 #[test]
780 fn test_max_basic() {
781 let mut agg = make_agg(AggregationMethod::Max);
782 let inputs = make_inputs(vec![vec![1.0, 5.0], vec![3.0, 2.0]]);
783 let r = agg
784 .aggregate(&inputs)
785 .expect("test: aggregate should succeed");
786 assert!(result_near(&r, &[3.0, 5.0], 1e-10));
787 }
788
789 #[test]
790 fn test_max_negative_values() {
791 let mut agg = make_agg(AggregationMethod::Max);
792 let inputs = make_inputs(vec![vec![-1.0, -5.0], vec![-3.0, -2.0]]);
793 let r = agg
794 .aggregate(&inputs)
795 .expect("test: aggregate should succeed");
796 assert!(result_near(&r, &[-1.0, -2.0], 1e-10));
797 }
798
799 #[test]
804 fn test_min_basic() {
805 let mut agg = make_agg(AggregationMethod::Min);
806 let inputs = make_inputs(vec![vec![1.0, 5.0], vec![3.0, 2.0]]);
807 let r = agg
808 .aggregate(&inputs)
809 .expect("test: aggregate should succeed");
810 assert!(result_near(&r, &[1.0, 2.0], 1e-10));
811 }
812
813 #[test]
814 fn test_min_all_equal() {
815 let mut agg = make_agg(AggregationMethod::Min);
816 let inputs = make_inputs(vec![vec![4.0, 4.0], vec![4.0, 4.0]]);
817 let r = agg
818 .aggregate(&inputs)
819 .expect("test: aggregate should succeed");
820 assert!(result_near(&r, &[4.0, 4.0], 1e-10));
821 }
822
823 #[test]
828 fn test_sum_basic() {
829 let mut agg = make_agg(AggregationMethod::Sum);
830 let inputs = make_inputs(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
831 let r = agg
832 .aggregate(&inputs)
833 .expect("test: aggregate should succeed");
834 assert!(result_near(&r, &[4.0, 6.0], 1e-10));
835 }
836
837 #[test]
838 fn test_sum_three_vectors() {
839 let mut agg = make_agg(AggregationMethod::Sum);
840 let inputs = make_inputs(vec![vec![1.0], vec![2.0], vec![3.0]]);
841 let r = agg
842 .aggregate(&inputs)
843 .expect("test: aggregate should succeed");
844 assert!(result_near(&r, &[6.0], 1e-10));
845 }
846
847 #[test]
852 fn test_geometric_mean_positive_values() {
853 let mut agg = make_agg(AggregationMethod::GeometricMean);
854 let inputs = make_inputs(vec![vec![1.0], vec![4.0]]);
856 let r = agg
857 .aggregate(&inputs)
858 .expect("test: aggregate should succeed");
859 assert!((r.output[0] - 2.0).abs() < 0.01);
861 }
862
863 #[test]
864 fn test_geometric_mean_negative_sign() {
865 let mut agg = make_agg(AggregationMethod::GeometricMean);
866 let inputs = make_inputs(vec![vec![-4.0], vec![-1.0]]);
868 let r = agg
869 .aggregate(&inputs)
870 .expect("test: aggregate should succeed");
871 assert!(r.output[0] < 0.0);
872 }
873
874 #[test]
875 fn test_geometric_mean_mixed_sign() {
876 let mut agg = make_agg(AggregationMethod::GeometricMean);
877 let inputs = make_inputs(vec![vec![-1.0], vec![1.0]]);
879 let r = agg
880 .aggregate(&inputs)
881 .expect("test: aggregate should succeed");
882 let _ = r.output[0];
884 }
885
886 #[test]
891 fn test_attention_pooling_uniform_query() {
892 let q = uniform(1.0, 4);
894 let mut agg = make_agg(AggregationMethod::AttentionPooling { query: q });
895 let inputs = make_inputs(vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0]]);
896 let mean_agg = make_inputs(vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0]]);
897 let r = agg
898 .aggregate(&inputs)
899 .expect("test: aggregate should succeed");
900 let mut mean_agg2 = make_agg(AggregationMethod::Mean);
901 let rm = mean_agg2
902 .aggregate(&mean_agg)
903 .expect("test: aggregate should succeed");
904 assert_eq!(r.output.len(), 4);
907 let _ = rm;
908 }
909
910 #[test]
911 fn test_attention_pooling_query_dim_mismatch() {
912 let q = vec![1.0, 0.0]; let mut agg = make_agg(AggregationMethod::AttentionPooling { query: q });
914 let inputs = make_inputs(vec![vec![1.0, 2.0, 3.0]]);
915 assert_eq!(
916 agg.aggregate(&inputs)
917 .expect_err("test: aggregate should return error"),
918 AggregatorError::InvalidQuery
919 );
920 }
921
922 #[test]
923 fn test_attention_pooling_single_input() {
924 let q = vec![1.0, 0.0];
925 let mut agg = make_agg(AggregationMethod::AttentionPooling { query: q });
926 let inputs = make_inputs(vec![vec![3.0, 7.0]]);
927 let r = agg
928 .aggregate(&inputs)
929 .expect("test: aggregate should succeed");
930 assert!(result_near(&r, &[3.0, 7.0], 1e-9));
931 }
932
933 #[test]
938 fn test_empty_input_error() {
939 let mut agg = make_agg(AggregationMethod::Mean);
940 let err = agg
941 .aggregate(&[])
942 .expect_err("test: aggregate should return error");
943 assert_eq!(err, AggregatorError::EmptyInput);
944 }
945
946 #[test]
947 fn test_dimension_mismatch_error() {
948 let mut agg = make_agg(AggregationMethod::Mean);
949 let inputs = vec![
950 AggregationInput::new("a", vec![1.0, 2.0], 1.0),
951 AggregationInput::new("b", vec![1.0, 2.0, 3.0], 1.0),
952 ];
953 let err = agg
954 .aggregate(&inputs)
955 .expect_err("test: aggregate should return error");
956 assert_eq!(
957 err,
958 AggregatorError::DimensionMismatch {
959 expected: 2,
960 got: 3
961 }
962 );
963 }
964
965 #[test]
966 fn test_aggregate_raw_empty() {
967 let mut agg = make_agg(AggregationMethod::Mean);
968 let err = agg
969 .aggregate_raw(&[])
970 .expect_err("test: aggregate_raw with empty input should return error");
971 assert_eq!(err, AggregatorError::EmptyInput);
972 }
973
974 #[test]
975 fn test_aggregate_raw_basic() {
976 let mut agg = make_agg(AggregationMethod::Mean);
977 let r = agg
978 .aggregate_raw(&[vec![2.0, 4.0], vec![0.0, 0.0]])
979 .expect("test: aggregate_raw should succeed");
980 assert!(result_near(&r, &[1.0, 2.0], 1e-10));
981 }
982
983 #[test]
988 fn test_l2_norm_zero_vec() {
989 let norm = EmbeddingAggregator::l2_norm(&[0.0, 0.0, 0.0]);
990 assert_eq!(norm, 0.0);
991 }
992
993 #[test]
994 fn test_l2_norm_unit_vec() {
995 let norm = EmbeddingAggregator::l2_norm(&[1.0, 0.0, 0.0]);
996 assert!((norm - 1.0).abs() < 1e-12);
997 }
998
999 #[test]
1000 fn test_l2_normalize_already_unit() {
1001 let v = vec![1.0, 0.0, 0.0];
1002 let n = EmbeddingAggregator::l2_normalize(&v);
1003 assert!((EmbeddingAggregator::l2_norm(&n) - 1.0).abs() < 1e-10);
1004 }
1005
1006 #[test]
1007 fn test_l2_normalize_zero_vec_unchanged() {
1008 let v = vec![0.0, 0.0];
1009 let n = EmbeddingAggregator::l2_normalize(&v);
1010 assert_eq!(n, vec![0.0, 0.0]);
1011 }
1012
1013 #[test]
1014 fn test_l2_normalize_scales_correctly() {
1015 let v = vec![3.0, 4.0];
1016 let n = EmbeddingAggregator::l2_normalize(&v);
1017 let norm = EmbeddingAggregator::l2_norm(&n);
1018 assert!((norm - 1.0).abs() < 1e-10);
1019 }
1020
1021 #[test]
1026 fn test_cosine_similarity_identical() {
1027 let v = vec![1.0, 2.0, 3.0];
1028 let sim = EmbeddingAggregator::cosine_similarity(&v, &v);
1029 assert!((sim - 1.0).abs() < 1e-10);
1030 }
1031
1032 #[test]
1033 fn test_cosine_similarity_orthogonal() {
1034 let a = vec![1.0, 0.0];
1035 let b = vec![0.0, 1.0];
1036 let sim = EmbeddingAggregator::cosine_similarity(&a, &b);
1037 assert!(sim.abs() < 1e-10);
1038 }
1039
1040 #[test]
1041 fn test_cosine_similarity_opposite() {
1042 let a = vec![1.0, 0.0];
1043 let b = vec![-1.0, 0.0];
1044 let sim = EmbeddingAggregator::cosine_similarity(&a, &b);
1045 assert!((sim + 1.0).abs() < 1e-10);
1046 }
1047
1048 #[test]
1049 fn test_cosine_similarity_zero_vec() {
1050 let a = vec![1.0, 0.0];
1051 let b = vec![0.0, 0.0];
1052 let sim = EmbeddingAggregator::cosine_similarity(&a, &b);
1053 assert_eq!(sim, 0.0);
1054 }
1055
1056 #[test]
1061 fn test_attention_scores_sum_to_one() {
1062 let query = vec![1.0, 0.0, 1.0];
1063 let embeddings = vec![
1064 vec![1.0, 0.0, 0.0],
1065 vec![0.0, 1.0, 0.0],
1066 vec![0.0, 0.0, 1.0],
1067 ];
1068 let scores = EmbeddingAggregator::attention_scores(&query, &embeddings);
1069 let total: f64 = scores.iter().sum();
1070 assert!((total - 1.0).abs() < 1e-10);
1071 }
1072
1073 #[test]
1074 fn test_attention_scores_length() {
1075 let query = vec![1.0, 1.0];
1076 let embeddings = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1077 let scores = EmbeddingAggregator::attention_scores(&query, &embeddings);
1078 assert_eq!(scores.len(), 3);
1079 }
1080
1081 #[test]
1086 fn test_normalize_output_enabled() {
1087 let config = EmbeddingAggregatorConfig {
1088 normalize_output: true,
1089 handle_zeros: false,
1090 };
1091 let mut agg = EmbeddingAggregator::new(AggregationMethod::Sum, config);
1092 let r = agg
1093 .aggregate_raw(&[vec![3.0, 4.0], vec![0.0, 0.0]])
1094 .expect("test: aggregate_raw should succeed");
1095 let norm = EmbeddingAggregator::l2_norm(&r.output);
1096 assert!((norm - 1.0).abs() < 1e-10);
1097 }
1098
1099 #[test]
1100 fn test_handle_zeros_replaces_zero_vec() {
1101 let config = EmbeddingAggregatorConfig {
1102 normalize_output: false,
1103 handle_zeros: true,
1104 };
1105 let mut agg = EmbeddingAggregator::new(AggregationMethod::Mean, config);
1106 let inputs = vec![
1107 AggregationInput::new("z", vec![0.0, 0.0], 1.0),
1108 AggregationInput::new("v", vec![1.0, 0.0], 1.0),
1109 ];
1110 let r = agg
1111 .aggregate(&inputs)
1112 .expect("test: aggregate should succeed");
1113 let expected_0 = (1.0 / 2.0_f64.sqrt() + 1.0) / 2.0;
1115 let expected_1 = 1.0 / (2.0 * 2.0_f64.sqrt());
1116 assert!((r.output[0] - expected_0).abs() < 1e-9);
1117 assert!((r.output[1] - expected_1).abs() < 1e-9);
1118 }
1119
1120 #[test]
1125 fn test_result_input_count() {
1126 let mut agg = make_agg(AggregationMethod::Mean);
1127 let inputs = make_inputs(vec![vec![1.0], vec![2.0], vec![3.0]]);
1128 let r = agg
1129 .aggregate(&inputs)
1130 .expect("test: aggregate should succeed");
1131 assert_eq!(r.input_count, 3);
1132 }
1133
1134 #[test]
1135 fn test_result_dims() {
1136 let mut agg = make_agg(AggregationMethod::Sum);
1137 let inputs = make_inputs(vec![vec![1.0, 2.0, 3.0, 4.0]]);
1138 let r = agg
1139 .aggregate(&inputs)
1140 .expect("test: aggregate should succeed");
1141 assert_eq!(r.dims, 4);
1142 }
1143
1144 #[test]
1145 fn test_result_norm_matches_output() {
1146 let mut agg = make_agg(AggregationMethod::Mean);
1147 let inputs = make_inputs(vec![vec![3.0, 4.0]]);
1148 let r = agg
1149 .aggregate(&inputs)
1150 .expect("test: aggregate should succeed");
1151 let expected_norm = EmbeddingAggregator::l2_norm(&r.output);
1152 assert!((r.norm - expected_norm).abs() < 1e-10);
1153 }
1154
1155 #[test]
1156 fn test_result_method_name() {
1157 let mut agg = make_agg(AggregationMethod::Max);
1158 let inputs = make_inputs(vec![vec![1.0]]);
1159 let r = agg
1160 .aggregate(&inputs)
1161 .expect("test: aggregate should succeed");
1162 assert_eq!(r.method, "Max");
1163 }
1164
1165 #[test]
1170 fn test_history_grows() {
1171 let mut agg = make_agg(AggregationMethod::Mean);
1172 for _ in 0..5 {
1173 agg.aggregate_raw(&[vec![1.0]])
1174 .expect("test: aggregate_raw should succeed");
1175 }
1176 assert_eq!(agg.history.len(), 5);
1177 }
1178
1179 #[test]
1180 fn test_history_capped_at_max() {
1181 let mut agg = EmbeddingAggregator::with_history_capacity(
1182 AggregationMethod::Mean,
1183 EmbeddingAggregatorConfig::default(),
1184 3,
1185 );
1186 for _ in 0..10 {
1187 agg.aggregate_raw(&[vec![1.0]])
1188 .expect("test: aggregate_raw should succeed");
1189 }
1190 assert_eq!(agg.history.len(), 3);
1191 }
1192
1193 #[test]
1194 fn test_recent_history_returns_n() {
1195 let mut agg = make_agg(AggregationMethod::Sum);
1196 for _ in 0..8 {
1197 agg.aggregate_raw(&[vec![1.0]])
1198 .expect("test: aggregate_raw should succeed");
1199 }
1200 let recent = agg.recent_history(3);
1201 assert_eq!(recent.len(), 3);
1202 }
1203
1204 #[test]
1205 fn test_recent_history_more_than_total() {
1206 let mut agg = make_agg(AggregationMethod::Sum);
1207 agg.aggregate_raw(&[vec![1.0]])
1208 .expect("test: aggregate_raw should succeed");
1209 let recent = agg.recent_history(100);
1210 assert_eq!(recent.len(), 1);
1211 }
1212
1213 #[test]
1218 fn test_stats_empty_history() {
1219 let agg = make_agg(AggregationMethod::Mean);
1220 let s = agg.stats();
1221 assert_eq!(s.total_aggregations, 0);
1222 assert_eq!(s.avg_input_count, 0.0);
1223 assert_eq!(s.avg_output_norm, 0.0);
1224 assert_eq!(s.method_name, "Mean");
1225 }
1226
1227 #[test]
1228 fn test_stats_total_aggregations() {
1229 let mut agg = make_agg(AggregationMethod::Min);
1230 for _ in 0..7 {
1231 agg.aggregate_raw(&[vec![1.0]])
1232 .expect("test: aggregate_raw should succeed");
1233 }
1234 assert_eq!(agg.stats().total_aggregations, 7);
1235 }
1236
1237 #[test]
1238 fn test_stats_avg_input_count() {
1239 let mut agg = make_agg(AggregationMethod::Mean);
1240 agg.aggregate_raw(&[vec![1.0], vec![2.0]])
1241 .expect("test: aggregate_raw should succeed"); agg.aggregate_raw(&[vec![1.0]])
1243 .expect("test: aggregate_raw should succeed"); let s = agg.stats();
1245 assert!((s.avg_input_count - 1.5).abs() < 1e-10);
1246 }
1247
1248 #[test]
1253 fn test_merge_results_mean() {
1254 let mut agg = make_agg(AggregationMethod::Mean);
1255 let r1 = agg
1256 .aggregate_raw(&[vec![2.0, 0.0]])
1257 .expect("test: aggregate_raw should succeed");
1258 let r2 = agg
1259 .aggregate_raw(&[vec![0.0, 4.0]])
1260 .expect("test: aggregate_raw should succeed");
1261 let merged = EmbeddingAggregator::merge_results(&[r1, r2], AggregationMethod::Mean)
1262 .expect("test: merge_results should succeed");
1263 assert!(result_near(&merged, &[1.0, 2.0], 1e-10));
1264 }
1265
1266 #[test]
1267 fn test_merge_results_empty_error() {
1268 let err = EmbeddingAggregator::merge_results(&[], AggregationMethod::Sum)
1269 .expect_err("test: merge_results with empty input should return error");
1270 assert_eq!(err, AggregatorError::EmptyInput);
1271 }
1272
1273 #[test]
1274 fn test_merge_results_dim_mismatch() {
1275 let r1 = AggregationResult {
1276 method: "Mean".into(),
1277 output: vec![1.0, 2.0],
1278 input_count: 1,
1279 dims: 2,
1280 norm: 1.0,
1281 };
1282 let r2 = AggregationResult {
1283 method: "Mean".into(),
1284 output: vec![1.0],
1285 input_count: 1,
1286 dims: 1,
1287 norm: 1.0,
1288 };
1289 let err = EmbeddingAggregator::merge_results(&[r1, r2], AggregationMethod::Sum)
1290 .expect_err("test: merge_results with dim mismatch should return error");
1291 assert_eq!(
1292 err,
1293 AggregatorError::DimensionMismatch {
1294 expected: 2,
1295 got: 1
1296 }
1297 );
1298 }
1299
1300 #[test]
1305 fn test_aggregate_with_input_weights() {
1306 let mut agg = make_agg(AggregationMethod::Mean);
1307 let inputs = vec![
1308 AggregationInput::new("a", vec![1.0, 0.0], 3.0),
1309 AggregationInput::new("b", vec![0.0, 1.0], 1.0),
1310 ];
1311 let r = agg
1312 .aggregate_with_input_weights(&inputs)
1313 .expect("test: aggregate_with_input_weights should succeed");
1314 assert!((r.output[0] - 0.75).abs() < 1e-10);
1315 assert!((r.output[1] - 0.25).abs() < 1e-10);
1316 }
1317
1318 #[test]
1319 fn test_aggregate_with_input_weights_empty_error() {
1320 let mut agg = make_agg(AggregationMethod::Mean);
1321 let err = agg
1322 .aggregate_with_input_weights(&[])
1323 .expect_err("test: aggregate_with_input_weights with empty input should return error");
1324 assert_eq!(err, AggregatorError::EmptyInput);
1325 }
1326
1327 #[test]
1332 fn test_error_display_empty_input() {
1333 let e = AggregatorError::EmptyInput;
1334 assert!(!e.to_string().is_empty());
1335 }
1336
1337 #[test]
1338 fn test_error_display_dimension_mismatch() {
1339 let e = AggregatorError::DimensionMismatch {
1340 expected: 4,
1341 got: 3,
1342 };
1343 assert!(e.to_string().contains('4'));
1344 }
1345
1346 #[test]
1347 fn test_error_display_weight_count() {
1348 let e = AggregatorError::WeightCountMismatch {
1349 expected: 2,
1350 got: 5,
1351 };
1352 assert!(e.to_string().contains('5'));
1353 }
1354
1355 #[test]
1356 fn test_error_display_invalid_query() {
1357 let e = AggregatorError::InvalidQuery;
1358 assert!(!e.to_string().is_empty());
1359 }
1360
1361 #[test]
1366 fn test_method_names() {
1367 let methods: Vec<(&str, AggregationMethod)> = vec![
1368 ("Mean", AggregationMethod::Mean),
1369 ("Max", AggregationMethod::Max),
1370 ("Min", AggregationMethod::Min),
1371 ("Sum", AggregationMethod::Sum),
1372 ("GeometricMean", AggregationMethod::GeometricMean),
1373 (
1374 "WeightedMean",
1375 AggregationMethod::WeightedMean { weights: vec![1.0] },
1376 ),
1377 (
1378 "AttentionPooling",
1379 AggregationMethod::AttentionPooling { query: vec![1.0] },
1380 ),
1381 ];
1382 for (expected, method) in methods {
1383 assert_eq!(method.name(), expected);
1384 }
1385 }
1386
1387 #[test]
1392 fn test_zero_history_capacity() {
1393 let mut agg = EmbeddingAggregator::with_history_capacity(
1394 AggregationMethod::Sum,
1395 EmbeddingAggregatorConfig::default(),
1396 0,
1397 );
1398 agg.aggregate_raw(&[vec![1.0]])
1399 .expect("test: aggregate_raw should succeed");
1400 assert_eq!(agg.history.len(), 0);
1401 let s = agg.stats();
1402 assert_eq!(s.total_aggregations, 0);
1403 }
1404}