1use std::collections::HashMap;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11use thiserror::Error;
12
13#[derive(Debug, Error)]
19pub enum PipelineError {
20 #[error("empty input")]
22 EmptyInput,
23
24 #[error("dimension mismatch: expected {expected}, got {got}")]
27 DimensionMismatch { expected: usize, got: usize },
28
29 #[error("invalid vector: {0}")]
31 InvalidVector(String),
32}
33
34#[derive(Debug, Clone)]
40pub enum EmbeddingInput {
41 RawBytes { data: Vec<u8>, mime_type: String },
43 Text {
45 content: String,
46 language: Option<String>,
47 },
48 Structured { fields: HashMap<String, String> },
50 Embedding { vector: Vec<f32> },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Default)]
60pub enum NormalizationStrategy {
61 None,
63 #[default]
65 L2,
66 MinMax,
68 ZScore,
70}
71
72#[derive(Debug, Clone)]
78pub struct EmbeddingPipelineConfig {
79 pub dimensions: usize,
81 pub normalization: NormalizationStrategy,
83 pub truncate_to_dims: bool,
85 pub pad_value: f32,
87}
88
89impl Default for EmbeddingPipelineConfig {
90 fn default() -> Self {
91 Self {
92 dimensions: 128,
93 normalization: NormalizationStrategy::L2,
94 truncate_to_dims: true,
95 pad_value: 0.0,
96 }
97 }
98}
99
100#[derive(Debug, Default)]
106pub struct PipelineStats {
107 pub total_processed: AtomicU64,
109 pub total_bytes_processed: AtomicU64,
111 pub total_errors: AtomicU64,
113}
114
115impl PipelineStats {
116 pub fn snapshot(&self) -> PipelineStatsSnapshot {
118 PipelineStatsSnapshot {
119 total_processed: self.total_processed.load(Ordering::Relaxed),
120 total_bytes_processed: self.total_bytes_processed.load(Ordering::Relaxed),
121 total_errors: self.total_errors.load(Ordering::Relaxed),
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct PipelineStatsSnapshot {
129 pub total_processed: u64,
130 pub total_bytes_processed: u64,
131 pub total_errors: u64,
132}
133
134pub fn fnv1a_hash_f32(data: &[u8], target_len: usize) -> Vec<f32> {
145 if data.is_empty() || target_len == 0 {
146 return vec![0.0_f32; target_len];
147 }
148
149 let mut result = Vec::with_capacity(target_len);
150
151 const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
153 const FNV_PRIME: u64 = 1_099_511_628_211;
154
155 for i in 0..target_len {
156 let mut hash: u64 = FNV_OFFSET_BASIS;
158 hash ^= i as u64;
159 hash = hash.wrapping_mul(FNV_PRIME);
160
161 let step = data.len().max(1);
164 let start = (i * step) % data.len();
165 for j in 0..step {
166 let byte = data[(start + j) % data.len()];
167 hash ^= u64::from(byte);
168 hash = hash.wrapping_mul(FNV_PRIME);
169 }
170
171 let normalized = (hash % 256) as f32 / 255.0_f32;
173 result.push(normalized);
174 }
175
176 result
177}
178
179#[derive(Debug)]
186pub struct EmbeddingPipeline {
187 pub config: EmbeddingPipelineConfig,
189 pub stats: Arc<PipelineStats>,
191}
192
193impl EmbeddingPipeline {
194 pub fn new(config: EmbeddingPipelineConfig) -> Self {
196 Self {
197 config,
198 stats: Arc::new(PipelineStats::default()),
199 }
200 }
201
202 pub fn with_defaults() -> Self {
204 Self::new(EmbeddingPipelineConfig::default())
205 }
206
207 pub fn process(&self, input: EmbeddingInput) -> Result<Vec<f32>, PipelineError> {
213 let result = self.process_inner(input);
214 match &result {
215 Ok(_) => {
216 self.stats.total_processed.fetch_add(1, Ordering::Relaxed);
217 }
218 Err(_) => {
219 self.stats.total_errors.fetch_add(1, Ordering::Relaxed);
220 }
221 }
222 result
223 }
224
225 pub fn batch_process(
227 &self,
228 inputs: Vec<EmbeddingInput>,
229 ) -> Vec<Result<Vec<f32>, PipelineError>> {
230 inputs.into_iter().map(|inp| self.process(inp)).collect()
231 }
232
233 pub fn normalize(&self, v: &mut [f32]) {
239 match self.config.normalization {
240 NormalizationStrategy::None => {}
241 NormalizationStrategy::L2 => normalize_l2(v),
242 NormalizationStrategy::MinMax => normalize_minmax(v),
243 NormalizationStrategy::ZScore => normalize_zscore(v),
244 }
245 }
246
247 fn process_inner(&self, input: EmbeddingInput) -> Result<Vec<f32>, PipelineError> {
252 let mut vec = match input {
253 EmbeddingInput::RawBytes { data, .. } => {
254 if data.is_empty() {
255 return Err(PipelineError::EmptyInput);
256 }
257 let byte_count = data.len() as u64;
258 self.stats
259 .total_bytes_processed
260 .fetch_add(byte_count, Ordering::Relaxed);
261 fnv1a_hash_f32(&data, self.config.dimensions)
262 }
263
264 EmbeddingInput::Text { content, .. } => {
265 if content.is_empty() {
266 return Err(PipelineError::EmptyInput);
267 }
268 const MAX_CP: f32 = 0x10FFFF as f32;
270 content
271 .chars()
272 .map(|c| c as u32 as f32 / MAX_CP)
273 .collect::<Vec<f32>>()
274 }
275
276 EmbeddingInput::Structured { fields } => {
277 if fields.is_empty() {
278 return Err(PipelineError::EmptyInput);
279 }
280 let mut pairs: Vec<(&String, &String)> = fields.iter().collect();
282 pairs.sort_by_key(|(k, _)| k.as_str());
283 let combined: String = pairs.iter().map(|(k, v)| format!("{}={} ", k, v)).collect();
284 const MAX_CP: f32 = 0x10FFFF as f32;
286 combined
287 .chars()
288 .map(|c| c as u32 as f32 / MAX_CP)
289 .collect::<Vec<f32>>()
290 }
291
292 EmbeddingInput::Embedding { vector } => {
293 if vector.is_empty() {
294 return Err(PipelineError::EmptyInput);
295 }
296 vector
297 }
298 };
299
300 validate_finite(&vec)?;
302
303 if self.config.truncate_to_dims {
305 adjust_dimensions(&mut vec, self.config.dimensions, self.config.pad_value);
306 } else if vec.len() != self.config.dimensions {
307 return Err(PipelineError::DimensionMismatch {
308 expected: self.config.dimensions,
309 got: vec.len(),
310 });
311 }
312
313 self.normalize(&mut vec);
315
316 Ok(vec)
317 }
318}
319
320fn adjust_dimensions(v: &mut Vec<f32>, target: usize, pad: f32) {
325 match v.len().cmp(&target) {
326 std::cmp::Ordering::Greater => v.truncate(target),
327 std::cmp::Ordering::Less => v.resize(target, pad),
328 std::cmp::Ordering::Equal => {}
329 }
330}
331
332fn validate_finite(v: &[f32]) -> Result<(), PipelineError> {
333 for (i, &val) in v.iter().enumerate() {
334 if !val.is_finite() {
335 return Err(PipelineError::InvalidVector(format!(
336 "non-finite value at index {i}: {val}"
337 )));
338 }
339 }
340 Ok(())
341}
342
343fn normalize_l2(v: &mut [f32]) {
344 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
345 if norm >= 1e-10 {
346 let inv = 1.0 / norm;
347 for x in v.iter_mut() {
348 *x *= inv;
349 }
350 }
351}
352
353fn normalize_minmax(v: &mut [f32]) {
354 let min = v.iter().cloned().fold(f32::INFINITY, f32::min);
355 let max = v.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
356 let range = max - min;
357 if range.abs() >= f32::EPSILON {
358 for x in v.iter_mut() {
359 *x = (*x - min) / range;
360 }
361 }
362}
363
364fn normalize_zscore(v: &mut [f32]) {
365 let n = v.len() as f32;
366 if n < 1.0 {
367 return;
368 }
369 let mean = v.iter().sum::<f32>() / n;
370 let variance = v.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / n;
371 let std_dev = variance.sqrt();
372 if std_dev >= 1e-10 {
373 for x in v.iter_mut() {
374 *x = (*x - mean) / std_dev;
375 }
376 }
377}
378
379#[derive(Clone, Debug, PartialEq)]
385pub enum PipelineStage {
386 Normalize,
388 Scale { factor: f32 },
390 Clamp { min: f32, max: f32 },
392 PadOrTruncate { target_dim: usize },
394 AddBias { bias: Vec<f32> },
399}
400
401#[derive(Clone, Debug)]
403pub struct PipelineResult {
404 pub output: Vec<f32>,
406 pub stages_applied: usize,
408 pub input_dim: usize,
410 pub output_dim: usize,
412}
413
414impl PipelineResult {
415 pub fn dimension_changed(&self) -> bool {
417 self.input_dim != self.output_dim
418 }
419}
420
421#[derive(Clone, Debug, Default)]
423pub struct SemanticPipelineStats {
424 pub total_processed: u64,
426 pub total_stage_applications: u64,
428 pub avg_output_dim: f64,
432}
433
434#[derive(Debug)]
454pub struct SemanticEmbeddingPipeline {
455 pub stages: Vec<PipelineStage>,
457 total_processed: u64,
459 total_stage_applications: u64,
460 total_output_dims: u64,
461}
462
463impl SemanticEmbeddingPipeline {
464 pub fn new() -> Self {
466 Self {
467 stages: Vec::new(),
468 total_processed: 0,
469 total_stage_applications: 0,
470 total_output_dims: 0,
471 }
472 }
473
474 pub fn add_stage(&mut self, stage: PipelineStage) -> &mut Self {
476 self.stages.push(stage);
477 self
478 }
479
480 pub fn clear_stages(&mut self) {
482 self.stages.clear();
483 }
484
485 pub fn stage_count(&self) -> usize {
487 self.stages.len()
488 }
489
490 pub fn process(&mut self, input: Vec<f32>) -> PipelineResult {
492 let input_dim = input.len();
493 let mut vec = input;
494
495 for stage in &self.stages {
496 Self::apply_stage(stage, &mut vec);
497 }
498
499 let output_dim = vec.len();
500 let stages_applied = self.stages.len();
501
502 self.total_processed += 1;
503 self.total_stage_applications += stages_applied as u64;
504 self.total_output_dims += output_dim as u64;
505
506 PipelineResult {
507 output: vec,
508 stages_applied,
509 input_dim,
510 output_dim,
511 }
512 }
513
514 pub fn process_batch(&mut self, inputs: Vec<Vec<f32>>) -> Vec<PipelineResult> {
516 inputs.into_iter().map(|v| self.process(v)).collect()
517 }
518
519 pub fn stats(&self) -> SemanticPipelineStats {
521 let avg_output_dim = if self.total_processed == 0 {
522 0.0
523 } else {
524 self.total_output_dims as f64 / self.total_processed as f64
525 };
526 SemanticPipelineStats {
527 total_processed: self.total_processed,
528 total_stage_applications: self.total_stage_applications,
529 avg_output_dim,
530 }
531 }
532
533 fn apply_stage(stage: &PipelineStage, vec: &mut Vec<f32>) {
538 match stage {
539 PipelineStage::Normalize => {
540 let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
541 if norm >= 1e-8 {
542 let inv = 1.0 / norm;
543 for x in vec.iter_mut() {
544 *x *= inv;
545 }
546 }
547 }
548
549 PipelineStage::Scale { factor } => {
550 for x in vec.iter_mut() {
551 *x *= factor;
552 }
553 }
554
555 PipelineStage::Clamp { min, max } => {
556 for x in vec.iter_mut() {
557 *x = x.clamp(*min, *max);
558 }
559 }
560
561 PipelineStage::PadOrTruncate { target_dim } => {
562 let current = vec.len();
563 match current.cmp(target_dim) {
564 std::cmp::Ordering::Less => {
565 vec.resize(*target_dim, 0.0_f32);
566 }
567 std::cmp::Ordering::Greater => {
568 vec.truncate(*target_dim);
569 }
570 std::cmp::Ordering::Equal => {}
571 }
572 }
573
574 PipelineStage::AddBias { bias } => {
575 for (i, x) in vec.iter_mut().enumerate() {
576 let b = if i < bias.len() { bias[i] } else { 0.0 };
577 *x += b;
578 }
579 }
580 }
581 }
582}
583
584impl Default for SemanticEmbeddingPipeline {
585 fn default() -> Self {
586 Self::new()
587 }
588}
589
590#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn default_pipeline() -> EmbeddingPipeline {
599 EmbeddingPipeline::with_defaults()
600 }
601
602 fn pipeline_with(
603 dims: usize,
604 norm: NormalizationStrategy,
605 truncate: bool,
606 ) -> EmbeddingPipeline {
607 EmbeddingPipeline::new(EmbeddingPipelineConfig {
608 dimensions: dims,
609 normalization: norm,
610 truncate_to_dims: truncate,
611 pad_value: 0.0,
612 })
613 }
614
615 #[test]
617 fn raw_bytes_correct_length() {
618 let p = default_pipeline();
619 let input = EmbeddingInput::RawBytes {
620 data: vec![1u8, 2, 3, 4, 5, 6, 7, 8],
621 mime_type: "application/octet-stream".to_string(),
622 };
623 let v = p.process(input).expect("should succeed");
624 assert_eq!(v.len(), 128);
625 }
626
627 #[test]
629 fn text_correct_length() {
630 let p = default_pipeline();
631 let input = EmbeddingInput::Text {
632 content: "hello world".to_string(),
633 language: None,
634 };
635 let v = p.process(input).expect("should succeed");
636 assert_eq!(v.len(), 128);
637 }
638
639 #[test]
641 fn structured_deterministic() {
642 let p = default_pipeline();
643 let mut fields = HashMap::new();
644 fields.insert("name".to_string(), "alice".to_string());
645 fields.insert("age".to_string(), "30".to_string());
646
647 let v1 = p
648 .process(EmbeddingInput::Structured {
649 fields: fields.clone(),
650 })
651 .expect("first call");
652 let v2 = p
653 .process(EmbeddingInput::Structured { fields })
654 .expect("second call");
655 assert_eq!(v1, v2, "structured input must be deterministic");
656 }
657
658 #[test]
660 fn embedding_passthrough() {
661 let p = default_pipeline();
662 let vec: Vec<f32> = (0..128).map(|i| i as f32 / 128.0).collect();
663 let input = EmbeddingInput::Embedding {
664 vector: vec.clone(),
665 };
666 let result = p.process(input).expect("should succeed");
667 assert_eq!(result.len(), 128);
668 }
669
670 #[test]
672 fn l2_unit_norm() {
673 let p = pipeline_with(16, NormalizationStrategy::L2, true);
674 let input = EmbeddingInput::Embedding {
675 vector: vec![1.0_f32; 16],
676 };
677 let v = p.process(input).expect("should succeed");
678 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
679 assert!(
680 (norm - 1.0).abs() < 1e-6,
681 "L2 norm should be 1.0, got {norm}"
682 );
683 }
684
685 #[test]
687 fn minmax_in_range() {
688 let p = pipeline_with(16, NormalizationStrategy::MinMax, true);
689 let input = EmbeddingInput::Embedding {
690 vector: (0..16).map(|i| i as f32 * 3.7 - 10.0).collect(),
691 };
692 let v = p.process(input).expect("should succeed");
693 for &val in &v {
694 assert!(
695 (0.0..=1.0 + 1e-6).contains(&val),
696 "minmax value out of range: {val}"
697 );
698 }
699 }
700
701 #[test]
703 fn zscore_zero_mean() {
704 let p = pipeline_with(32, NormalizationStrategy::ZScore, true);
705 let input = EmbeddingInput::Embedding {
706 vector: (0..32).map(|i| i as f32).collect(),
707 };
708 let v = p.process(input).expect("should succeed");
709 let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
710 assert!(mean.abs() < 1e-5, "zscore mean should be ~0, got {mean}");
711 }
712
713 #[test]
715 fn truncation_to_dims() {
716 let p = pipeline_with(8, NormalizationStrategy::None, true);
717 let input = EmbeddingInput::Embedding {
718 vector: vec![0.1_f32; 64],
719 };
720 let v = p.process(input).expect("should succeed");
721 assert_eq!(v.len(), 8, "should be truncated to 8");
722 }
723
724 #[test]
726 fn padding_to_dims() {
727 let p = EmbeddingPipeline::new(EmbeddingPipelineConfig {
728 dimensions: 32,
729 normalization: NormalizationStrategy::None,
730 truncate_to_dims: true,
731 pad_value: -1.0,
732 });
733 let input = EmbeddingInput::Embedding {
734 vector: vec![0.5_f32; 4],
735 };
736 let v = p.process(input).expect("should succeed");
737 assert_eq!(v.len(), 32);
738 for &val in &v[4..] {
740 assert!((val - (-1.0)).abs() < 1e-7, "pad value mismatch: {val}");
741 }
742 }
743
744 #[test]
746 fn batch_process_multiple() {
747 let p = default_pipeline();
748 let inputs = vec![
749 EmbeddingInput::Text {
750 content: "alpha".to_string(),
751 language: None,
752 },
753 EmbeddingInput::RawBytes {
754 data: vec![42u8; 20],
755 mime_type: "application/octet-stream".to_string(),
756 },
757 EmbeddingInput::Embedding {
758 vector: vec![0.3_f32; 128],
759 },
760 ];
761 let results = p.batch_process(inputs);
762 assert_eq!(results.len(), 3);
763 for r in &results {
764 assert!(r.is_ok(), "batch result should be Ok: {r:?}");
765 }
766 }
767
768 #[test]
770 fn stats_accumulate() {
771 let p = default_pipeline();
772 for _ in 0..5 {
773 let _ = p.process(EmbeddingInput::Text {
774 content: "test".to_string(),
775 language: None,
776 });
777 }
778 let _ = p.process(EmbeddingInput::Text {
780 content: String::new(),
781 language: None,
782 });
783 let snap = p.stats.snapshot();
784 assert_eq!(snap.total_processed, 5);
785 assert_eq!(snap.total_errors, 1);
786 }
787
788 #[test]
790 fn empty_text_error() {
791 let p = default_pipeline();
792 let result = p.process(EmbeddingInput::Text {
793 content: String::new(),
794 language: None,
795 });
796 assert!(matches!(result, Err(PipelineError::EmptyInput)));
797 }
798
799 #[test]
801 fn empty_bytes_error() {
802 let p = default_pipeline();
803 let result = p.process(EmbeddingInput::RawBytes {
804 data: vec![],
805 mime_type: "application/octet-stream".to_string(),
806 });
807 assert!(matches!(result, Err(PipelineError::EmptyInput)));
808 }
809
810 #[test]
812 fn dimension_mismatch_error() {
813 let p = pipeline_with(64, NormalizationStrategy::None, false);
814 let result = p.process(EmbeddingInput::Embedding {
815 vector: vec![1.0_f32; 32],
816 });
817 assert!(
818 matches!(
819 result,
820 Err(PipelineError::DimensionMismatch {
821 expected: 64,
822 got: 32
823 })
824 ),
825 "expected DimensionMismatch, got {result:?}"
826 );
827 }
828
829 #[test]
831 fn raw_bytes_stats_counter() {
832 let p = default_pipeline();
833 let _ = p.process(EmbeddingInput::RawBytes {
834 data: vec![0xAA; 100],
835 mime_type: "application/octet-stream".to_string(),
836 });
837 let snap = p.stats.snapshot();
838 assert_eq!(snap.total_bytes_processed, 100);
839 }
840
841 #[test]
843 fn none_normalization_preserves_values() {
844 let p = pipeline_with(4, NormalizationStrategy::None, true);
845 let vals = vec![2.0_f32, 4.0, 6.0, 8.0];
846 let input = EmbeddingInput::Embedding {
847 vector: vals.clone(),
848 };
849 let v = p.process(input).expect("should succeed");
850 for (a, b) in vals.iter().zip(v.iter()) {
851 assert!((a - b).abs() < 1e-7);
852 }
853 }
854
855 #[test]
861 fn sep_new_starts_empty() {
862 let p = SemanticEmbeddingPipeline::new();
863 assert_eq!(p.stage_count(), 0);
864 assert!(p.stages.is_empty());
865 }
866
867 #[test]
869 fn sep_add_stage_builder() {
870 let mut p = SemanticEmbeddingPipeline::new();
871 p.add_stage(PipelineStage::Normalize)
872 .add_stage(PipelineStage::Scale { factor: 2.0 });
873 assert_eq!(p.stage_count(), 2);
874 }
875
876 #[test]
878 fn sep_stage_count_correct() {
879 let mut p = SemanticEmbeddingPipeline::new();
880 assert_eq!(p.stage_count(), 0);
881 p.add_stage(PipelineStage::Normalize);
882 assert_eq!(p.stage_count(), 1);
883 p.add_stage(PipelineStage::Scale { factor: 1.0 });
884 assert_eq!(p.stage_count(), 2);
885 }
886
887 #[test]
889 fn sep_clear_stages_resets() {
890 let mut p = SemanticEmbeddingPipeline::new();
891 p.add_stage(PipelineStage::Normalize)
892 .add_stage(PipelineStage::Scale { factor: 2.0 });
893 p.clear_stages();
894 assert_eq!(p.stage_count(), 0);
895 }
896
897 #[test]
899 fn sep_normalize_unit_vector() {
900 let mut p = SemanticEmbeddingPipeline::new();
901 p.add_stage(PipelineStage::Normalize);
902 let result = p.process(vec![3.0, 4.0]);
903 let norm: f32 = result.output.iter().map(|x| x * x).sum::<f32>().sqrt();
904 assert!((norm - 1.0).abs() < 1e-6, "expected unit norm, got {norm}");
905 }
906
907 #[test]
909 fn sep_normalize_zero_vector_unchanged() {
910 let mut p = SemanticEmbeddingPipeline::new();
911 p.add_stage(PipelineStage::Normalize);
912 let result = p.process(vec![0.0, 0.0, 0.0]);
913 assert_eq!(result.output, vec![0.0_f32, 0.0, 0.0]);
914 }
915
916 #[test]
918 fn sep_scale_multiplies_correctly() {
919 let mut p = SemanticEmbeddingPipeline::new();
920 p.add_stage(PipelineStage::Scale { factor: 3.0 });
921 let result = p.process(vec![1.0, 2.0, 3.0]);
922 assert_eq!(result.output, vec![3.0_f32, 6.0, 9.0]);
923 }
924
925 #[test]
927 fn sep_clamp_restricts_values() {
928 let mut p = SemanticEmbeddingPipeline::new();
929 p.add_stage(PipelineStage::Clamp {
930 min: -1.0,
931 max: 1.0,
932 });
933 let result = p.process(vec![-5.0, 0.5, 3.0]);
934 assert_eq!(result.output, vec![-1.0_f32, 0.5, 1.0]);
935 }
936
937 #[test]
939 fn sep_pad_or_truncate_pads_shorter() {
940 let mut p = SemanticEmbeddingPipeline::new();
941 p.add_stage(PipelineStage::PadOrTruncate { target_dim: 6 });
942 let result = p.process(vec![1.0, 2.0, 3.0]);
943 assert_eq!(result.output.len(), 6);
944 assert_eq!(&result.output[3..], &[0.0_f32, 0.0, 0.0]);
945 }
946
947 #[test]
949 fn sep_pad_or_truncate_truncates_longer() {
950 let mut p = SemanticEmbeddingPipeline::new();
951 p.add_stage(PipelineStage::PadOrTruncate { target_dim: 2 });
952 let result = p.process(vec![1.0, 2.0, 3.0, 4.0]);
953 assert_eq!(result.output, vec![1.0_f32, 2.0]);
954 }
955
956 #[test]
958 fn sep_pad_or_truncate_exact_unchanged() {
959 let mut p = SemanticEmbeddingPipeline::new();
960 p.add_stage(PipelineStage::PadOrTruncate { target_dim: 3 });
961 let result = p.process(vec![1.0, 2.0, 3.0]);
962 assert_eq!(result.output, vec![1.0_f32, 2.0, 3.0]);
963 }
964
965 #[test]
967 fn sep_add_bias_element_wise() {
968 let mut p = SemanticEmbeddingPipeline::new();
969 p.add_stage(PipelineStage::AddBias {
970 bias: vec![0.1, 0.2, 0.3],
971 });
972 let result = p.process(vec![1.0, 1.0, 1.0]);
973 let expected = [1.1_f32, 1.2, 1.3];
974 for (a, b) in result.output.iter().zip(expected.iter()) {
975 assert!((a - b).abs() < 1e-6, "got {a}, expected {b}");
976 }
977 }
978
979 #[test]
981 fn sep_add_bias_shorter_bias() {
982 let mut p = SemanticEmbeddingPipeline::new();
983 p.add_stage(PipelineStage::AddBias {
984 bias: vec![1.0], });
986 let result = p.process(vec![0.0, 0.0, 0.0]);
987 assert!((result.output[0] - 1.0).abs() < 1e-6);
988 assert!((result.output[1] - 0.0).abs() < 1e-6);
989 assert!((result.output[2] - 0.0).abs() < 1e-6);
990 }
991
992 #[test]
994 fn sep_add_bias_longer_bias_truncated() {
995 let mut p = SemanticEmbeddingPipeline::new();
996 p.add_stage(PipelineStage::AddBias {
997 bias: vec![1.0, 2.0, 3.0, 4.0, 5.0], });
999 let result = p.process(vec![0.0, 0.0]);
1000 assert_eq!(result.output.len(), 2);
1002 assert!((result.output[0] - 1.0).abs() < 1e-6);
1003 assert!((result.output[1] - 2.0).abs() < 1e-6);
1004 }
1005
1006 #[test]
1008 fn sep_multi_stage_order() {
1009 let mut p = SemanticEmbeddingPipeline::new();
1010 p.add_stage(PipelineStage::Scale { factor: 2.0 })
1012 .add_stage(PipelineStage::Clamp { min: 0.0, max: 3.0 });
1013 let result = p.process(vec![-1.0, 1.0, 2.0]);
1014 assert_eq!(result.output, vec![0.0_f32, 2.0, 3.0]);
1017 }
1018
1019 #[test]
1021 fn sep_result_stages_applied_correct() {
1022 let mut p = SemanticEmbeddingPipeline::new();
1023 p.add_stage(PipelineStage::Normalize)
1024 .add_stage(PipelineStage::Scale { factor: 1.0 })
1025 .add_stage(PipelineStage::Clamp {
1026 min: -1.0,
1027 max: 1.0,
1028 });
1029 let result = p.process(vec![1.0, 0.0]);
1030 assert_eq!(result.stages_applied, 3);
1031 }
1032
1033 #[test]
1035 fn sep_result_dims_no_change() {
1036 let mut p = SemanticEmbeddingPipeline::new();
1037 p.add_stage(PipelineStage::Normalize);
1038 let result = p.process(vec![1.0, 2.0, 3.0]);
1039 assert_eq!(result.input_dim, 3);
1040 assert_eq!(result.output_dim, 3);
1041 }
1042
1043 #[test]
1045 fn sep_dimension_changed_true() {
1046 let mut p = SemanticEmbeddingPipeline::new();
1047 p.add_stage(PipelineStage::PadOrTruncate { target_dim: 8 });
1048 let result = p.process(vec![1.0, 2.0]);
1049 assert!(result.dimension_changed());
1050 assert_eq!(result.input_dim, 2);
1051 assert_eq!(result.output_dim, 8);
1052 }
1053
1054 #[test]
1056 fn sep_dimension_changed_false() {
1057 let mut p = SemanticEmbeddingPipeline::new();
1058 p.add_stage(PipelineStage::Normalize);
1059 let result = p.process(vec![1.0, 0.0, 0.0]);
1060 assert!(!result.dimension_changed());
1061 }
1062
1063 #[test]
1065 fn sep_process_batch_count() {
1066 let mut p = SemanticEmbeddingPipeline::new();
1067 p.add_stage(PipelineStage::Normalize);
1068 let inputs = vec![
1069 vec![1.0, 0.0],
1070 vec![0.0, 1.0],
1071 vec![1.0, 1.0],
1072 vec![2.0, 3.0],
1073 ];
1074 let results = p.process_batch(inputs);
1075 assert_eq!(results.len(), 4);
1076 }
1077
1078 #[test]
1080 fn sep_stats_total_processed_increments() {
1081 let mut p = SemanticEmbeddingPipeline::new();
1082 p.add_stage(PipelineStage::Normalize);
1083 for _ in 0..7 {
1084 p.process(vec![1.0, 2.0]);
1085 }
1086 let s = p.stats();
1087 assert_eq!(s.total_processed, 7);
1088 assert_eq!(s.total_stage_applications, 7);
1089 }
1090
1091 #[test]
1093 fn sep_stats_avg_output_dim_correct() {
1094 let mut p = SemanticEmbeddingPipeline::new();
1095 p.add_stage(PipelineStage::PadOrTruncate { target_dim: 4 });
1098 p.process(vec![1.0, 2.0]); p.process(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); let s = p.stats();
1101 assert_eq!(s.total_processed, 2);
1102 assert!((s.avg_output_dim - 4.0).abs() < 1e-9);
1103 }
1104
1105 #[test]
1107 fn sep_stats_avg_output_dim_zero_when_empty() {
1108 let p = SemanticEmbeddingPipeline::new();
1109 let s = p.stats();
1110 assert_eq!(s.total_processed, 0);
1111 assert_eq!(s.avg_output_dim, 0.0);
1112 }
1113}