1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ReductionMethod {
12 RandomProjection,
14 PCA,
16 Truncation,
18}
19
20#[derive(Debug, Clone)]
22pub struct ReducerConfig {
23 pub input_dim: usize,
25 pub output_dim: usize,
27 pub method: ReductionMethod,
29 pub seed: u64,
31}
32
33#[derive(Debug, Clone)]
35pub struct ReductionResult {
36 pub original_dim: usize,
38 pub reduced_dim: usize,
40 pub reconstruction_error: Option<f64>,
42}
43
44#[derive(Debug, Clone)]
46pub struct ReducerStats {
47 pub input_dim: usize,
49 pub output_dim: usize,
51 pub method: ReductionMethod,
53 pub fitted: bool,
55 pub reductions_performed: u64,
57}
58
59pub struct SemanticDimensionReducer {
61 config: ReducerConfig,
62 projection_matrix: Option<Vec<Vec<f64>>>,
64 fitted: bool,
65 reductions_performed: u64,
66}
67
68struct FnvPrng {
70 state: u64,
71}
72
73impl FnvPrng {
74 fn new(seed: u64) -> Self {
75 Self {
76 state: seed ^ 0xcbf29ce484222325,
77 }
78 }
79
80 fn next_u64(&mut self) -> u64 {
82 self.state ^= self.state.wrapping_shr(13);
84 self.state = self.state.wrapping_mul(0x100000001b3);
85 self.state ^= self.state.wrapping_shr(7);
86 self.state = self.state.wrapping_mul(0x100000001b3);
87 self.state ^= self.state.wrapping_shr(17);
88 self.state
89 }
90
91 fn next_gaussian(&mut self) -> f64 {
94 let mut sum = 0.0f64;
97 for _ in 0..12 {
98 let u = (self.next_u64() as f64) / (u64::MAX as f64);
99 sum += u;
100 }
101 sum - 6.0
102 }
103}
104
105impl SemanticDimensionReducer {
106 pub fn new(config: ReducerConfig) -> Self {
108 Self {
109 config,
110 projection_matrix: None,
111 fitted: false,
112 reductions_performed: 0,
113 }
114 }
115
116 pub fn fit(&mut self, embeddings: &[Vec<f64>]) -> Result<(), String> {
122 if self.config.output_dim > self.config.input_dim {
123 return Err(format!(
124 "output_dim ({}) must be <= input_dim ({})",
125 self.config.output_dim, self.config.input_dim
126 ));
127 }
128
129 for (i, emb) in embeddings.iter().enumerate() {
131 if emb.len() != self.config.input_dim {
132 return Err(format!(
133 "embedding at index {} has dimension {} but expected {}",
134 i,
135 emb.len(),
136 self.config.input_dim
137 ));
138 }
139 }
140
141 match self.config.method {
142 ReductionMethod::RandomProjection => {
143 self.fit_random_projection()?;
144 }
145 ReductionMethod::PCA => {
146 self.fit_pca(embeddings)?;
147 }
148 ReductionMethod::Truncation => {
149 }
151 }
152
153 self.fitted = true;
154 Ok(())
155 }
156
157 fn fit_random_projection(&mut self) -> Result<(), String> {
159 let input_dim = self.config.input_dim;
160 let output_dim = self.config.output_dim;
161 let mut prng = FnvPrng::new(self.config.seed);
162
163 let mut matrix = Vec::with_capacity(output_dim);
165 for _ in 0..output_dim {
166 let mut row = Vec::with_capacity(input_dim);
167 for _ in 0..input_dim {
168 row.push(prng.next_gaussian());
169 }
170 matrix.push(row);
171 }
172
173 self.normalize_columns(&mut matrix);
175
176 self.projection_matrix = Some(matrix);
177 Ok(())
178 }
179
180 fn normalize_columns(&self, matrix: &mut [Vec<f64>]) {
182 if matrix.is_empty() {
183 return;
184 }
185 let input_dim = matrix[0].len();
186 let output_dim = matrix.len();
187
188 for col in 0..input_dim {
189 let mut norm_sq = 0.0f64;
190 for row in matrix.iter().take(output_dim) {
191 norm_sq += row[col] * row[col];
192 }
193 let norm = norm_sq.sqrt();
194 if norm > 1e-15 {
195 for row in matrix.iter_mut().take(output_dim) {
196 row[col] /= norm;
197 }
198 }
199 }
200 }
201
202 fn fit_pca(&mut self, embeddings: &[Vec<f64>]) -> Result<(), String> {
204 if embeddings.is_empty() {
205 return Err("cannot fit PCA with zero embeddings".to_string());
206 }
207
208 let n = embeddings.len();
209 let d = self.config.input_dim;
210 let k = self.config.output_dim;
211
212 let mut mean = vec![0.0f64; d];
214 for emb in embeddings {
215 for (j, val) in emb.iter().enumerate() {
216 mean[j] += val;
217 }
218 }
219 let n_f64 = n as f64;
220 for m in &mut mean {
221 *m /= n_f64;
222 }
223
224 let centered: Vec<Vec<f64>> = embeddings
226 .iter()
227 .map(|emb| emb.iter().zip(mean.iter()).map(|(v, m)| v - m).collect())
228 .collect();
229
230 let mut prng = FnvPrng::new(self.config.seed);
232 let mut components: Vec<Vec<f64>> = Vec::with_capacity(k);
233 let max_iterations = 100;
234
235 for comp_idx in 0..k {
236 let mut v: Vec<f64> = (0..d).map(|_| prng.next_gaussian()).collect();
238 let mut v_norm = vec_norm(&v);
239 if v_norm > 1e-15 {
240 for val in &mut v {
241 *val /= v_norm;
242 }
243 }
244
245 for _iter in 0..max_iterations {
246 let projections: Vec<f64> = centered.iter().map(|row| dot(row, &v)).collect();
249
250 let mut new_v = vec![0.0f64; d];
252 for (i, proj) in projections.iter().enumerate() {
253 for (j, val) in centered[i].iter().enumerate() {
254 new_v[j] += proj * val;
255 }
256 }
257 for val in &mut new_v {
258 *val /= n_f64;
259 }
260
261 for prev in &components {
263 let proj = dot(&new_v, prev);
264 for (j, val) in new_v.iter_mut().enumerate() {
265 *val -= proj * prev[j];
266 }
267 }
268
269 v_norm = vec_norm(&new_v);
271 if v_norm < 1e-15 {
272 for val in new_v.iter_mut() {
274 *val = prng.next_gaussian();
275 }
276 v_norm = vec_norm(&new_v);
277 if v_norm > 1e-15 {
278 for val in &mut new_v {
279 *val /= v_norm;
280 }
281 }
282 } else {
283 for val in &mut new_v {
284 *val /= v_norm;
285 }
286 }
287
288 let diff: f64 = v
290 .iter()
291 .zip(new_v.iter())
292 .map(|(a, b)| (a - b).powi(2))
293 .sum();
294 v = new_v;
295 if diff < 1e-10 {
296 break;
297 }
298 }
299
300 components.push(v);
301 let _ = comp_idx; }
303
304 self.projection_matrix = Some(components);
306 Ok(())
307 }
308
309 pub fn transform(&mut self, embedding: &[f64]) -> Result<Vec<f64>, String> {
311 if !self.fitted {
312 return Err("reducer has not been fitted yet".to_string());
313 }
314
315 if embedding.len() != self.config.input_dim {
316 return Err(format!(
317 "input dimension mismatch: expected {}, got {}",
318 self.config.input_dim,
319 embedding.len()
320 ));
321 }
322
323 let result = match self.config.method {
324 ReductionMethod::Truncation => embedding[..self.config.output_dim].to_vec(),
325 ReductionMethod::RandomProjection | ReductionMethod::PCA => {
326 let matrix = self
327 .projection_matrix
328 .as_ref()
329 .ok_or_else(|| "projection matrix not initialized".to_string())?;
330 let mut out = Vec::with_capacity(self.config.output_dim);
331 for row in matrix {
332 out.push(dot(row, embedding));
333 }
334 out
335 }
336 };
337
338 self.reductions_performed += 1;
339 Ok(result)
340 }
341
342 pub fn fit_transform(&mut self, embeddings: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, String> {
344 self.fit(embeddings)?;
345 let mut results = Vec::with_capacity(embeddings.len());
346 for emb in embeddings {
347 results.push(self.transform(emb)?);
348 }
349 Ok(results)
350 }
351
352 pub fn reconstruction_error(&self, original: &[f64], reduced: &[f64]) -> f64 {
358 let reconstructed = match self.config.method {
359 ReductionMethod::Truncation => {
360 let mut r = reduced.to_vec();
361 r.resize(self.config.input_dim, 0.0);
362 r
363 }
364 ReductionMethod::RandomProjection | ReductionMethod::PCA => {
365 if let Some(matrix) = &self.projection_matrix {
367 let mut r = vec![0.0f64; self.config.input_dim];
368 for (i, row) in matrix.iter().enumerate() {
369 if i < reduced.len() {
370 for (j, &val) in row.iter().enumerate() {
371 r[j] += reduced[i] * val;
372 }
373 }
374 }
375 r
376 } else {
377 vec![0.0f64; self.config.input_dim]
378 }
379 }
380 };
381
382 let n = original.len().min(reconstructed.len());
384 if n == 0 {
385 return 0.0;
386 }
387 let mse: f64 = original
388 .iter()
389 .take(n)
390 .zip(reconstructed.iter().take(n))
391 .map(|(a, b)| (a - b).powi(2))
392 .sum::<f64>()
393 / n as f64;
394 mse
395 }
396
397 pub fn is_fitted(&self) -> bool {
399 self.fitted
400 }
401
402 pub fn reset(&mut self) {
404 self.projection_matrix = None;
405 self.fitted = false;
406 self.reductions_performed = 0;
407 }
408
409 pub fn stats(&self) -> ReducerStats {
411 ReducerStats {
412 input_dim: self.config.input_dim,
413 output_dim: self.config.output_dim,
414 method: self.config.method,
415 fitted: self.fitted,
416 reductions_performed: self.reductions_performed,
417 }
418 }
419}
420
421fn dot(a: &[f64], b: &[f64]) -> f64 {
423 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
424}
425
426fn vec_norm(v: &[f64]) -> f64 {
428 v.iter().map(|x| x * x).sum::<f64>().sqrt()
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 fn make_config(
436 input_dim: usize,
437 output_dim: usize,
438 method: ReductionMethod,
439 seed: u64,
440 ) -> ReducerConfig {
441 ReducerConfig {
442 input_dim,
443 output_dim,
444 method,
445 seed,
446 }
447 }
448
449 #[test]
452 fn test_random_projection_reduces_dim() {
453 let config = make_config(100, 10, ReductionMethod::RandomProjection, 42);
454 let mut reducer = SemanticDimensionReducer::new(config);
455 let embeddings = vec![vec![1.0; 100]; 5];
456 reducer.fit(&embeddings).expect("fit should succeed");
457 let result = reducer
458 .transform(&embeddings[0])
459 .expect("transform should succeed");
460 assert_eq!(result.len(), 10);
461 }
462
463 #[test]
464 fn test_random_projection_deterministic_same_seed() {
465 let config1 = make_config(50, 10, ReductionMethod::RandomProjection, 123);
466 let config2 = make_config(50, 10, ReductionMethod::RandomProjection, 123);
467 let mut r1 = SemanticDimensionReducer::new(config1);
468 let mut r2 = SemanticDimensionReducer::new(config2);
469 let embeddings = vec![vec![0.5; 50]; 3];
470 r1.fit(&embeddings).expect("fit should succeed");
471 r2.fit(&embeddings).expect("fit should succeed");
472 let t1 = r1.transform(&embeddings[0]).expect("transform");
473 let t2 = r2.transform(&embeddings[0]).expect("transform");
474 assert_eq!(t1, t2);
475 }
476
477 #[test]
478 fn test_random_projection_different_seeds_differ() {
479 let config1 = make_config(50, 10, ReductionMethod::RandomProjection, 100);
480 let config2 = make_config(50, 10, ReductionMethod::RandomProjection, 200);
481 let mut r1 = SemanticDimensionReducer::new(config1);
482 let mut r2 = SemanticDimensionReducer::new(config2);
483 let embeddings = vec![vec![0.5; 50]; 3];
484 r1.fit(&embeddings).expect("fit");
485 r2.fit(&embeddings).expect("fit");
486 let t1 = r1.transform(&embeddings[0]).expect("transform");
487 let t2 = r2.transform(&embeddings[0]).expect("transform");
488 assert_ne!(t1, t2);
489 }
490
491 #[test]
492 fn test_random_projection_reconstruction_error() {
493 let config = make_config(20, 15, ReductionMethod::RandomProjection, 42);
494 let mut reducer = SemanticDimensionReducer::new(config);
495 let embedding = (0..20).map(|i| i as f64 * 0.1).collect::<Vec<_>>();
496 let embeddings = vec![embedding.clone()];
497 reducer.fit(&embeddings).expect("fit");
498 let reduced = reducer.transform(&embedding).expect("transform");
499 let error = reducer.reconstruction_error(&embedding, &reduced);
500 assert!(error >= 0.0);
502 assert!(error.is_finite());
503 }
504
505 #[test]
508 fn test_truncation_takes_first_n() {
509 let config = make_config(10, 5, ReductionMethod::Truncation, 0);
510 let mut reducer = SemanticDimensionReducer::new(config);
511 let embedding: Vec<f64> = (0..10).map(|i| i as f64).collect();
512 let embeddings = vec![embedding.clone()];
513 reducer.fit(&embeddings).expect("fit");
514 let result = reducer.transform(&embedding).expect("transform");
515 assert_eq!(result, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
516 }
517
518 #[test]
519 fn test_truncation_reconstruction_error() {
520 let config = make_config(10, 5, ReductionMethod::Truncation, 0);
521 let mut reducer = SemanticDimensionReducer::new(config);
522 let embedding: Vec<f64> = (0..10).map(|i| i as f64).collect();
523 let embeddings = vec![embedding.clone()];
524 reducer.fit(&embeddings).expect("fit");
525 let reduced = reducer.transform(&embedding).expect("transform");
526 let error = reducer.reconstruction_error(&embedding, &reduced);
527 assert!((error - 25.5).abs() < 1e-10);
530 }
531
532 #[test]
535 fn test_pca_reduces_dim() {
536 let config = make_config(10, 3, ReductionMethod::PCA, 42);
537 let mut reducer = SemanticDimensionReducer::new(config);
538 let mut embeddings = Vec::new();
540 for i in 0..20 {
541 let mut emb = vec![0.0; 10];
542 emb[0] = i as f64 * 10.0; emb[1] = i as f64 * 5.0; emb[2] = i as f64 * 1.0; for (j, val) in emb.iter_mut().enumerate().skip(3) {
546 *val = 0.01 * (i as f64 + j as f64);
547 }
548 embeddings.push(emb);
549 }
550 reducer.fit(&embeddings).expect("fit");
551 let result = reducer.transform(&embeddings[0]).expect("transform");
552 assert_eq!(result.len(), 3);
553 }
554
555 #[test]
556 fn test_pca_preserves_variance_direction() {
557 let config = make_config(5, 1, ReductionMethod::PCA, 42);
558 let mut reducer = SemanticDimensionReducer::new(config);
559 let embeddings: Vec<Vec<f64>> = (0..50)
561 .map(|i| {
562 let mut v = vec![0.0; 5];
563 v[0] = i as f64;
564 v
565 })
566 .collect();
567 reducer.fit(&embeddings).expect("fit");
568
569 let t1 = reducer.transform(&embeddings[0]).expect("transform");
571 let t2 = reducer.transform(&embeddings[49]).expect("transform");
572 let separation = (t1[0] - t2[0]).abs();
573 assert!(
574 separation > 1.0,
575 "PCA should preserve main variance direction, got separation {separation}"
576 );
577 }
578
579 #[test]
580 fn test_pca_empty_embeddings_error() {
581 let config = make_config(10, 3, ReductionMethod::PCA, 42);
582 let mut reducer = SemanticDimensionReducer::new(config);
583 let result = reducer.fit(&[]);
584 assert!(result.is_err());
585 }
586
587 #[test]
588 fn test_pca_reconstruction_error() {
589 let config = make_config(10, 5, ReductionMethod::PCA, 42);
590 let mut reducer = SemanticDimensionReducer::new(config);
591 let embeddings: Vec<Vec<f64>> = (0..30)
592 .map(|i| (0..10).map(|j| (i * 10 + j) as f64 * 0.01).collect())
593 .collect();
594 reducer.fit(&embeddings).expect("fit");
595 let reduced = reducer.transform(&embeddings[0]).expect("transform");
596 let error = reducer.reconstruction_error(&embeddings[0], &reduced);
597 assert!(error >= 0.0);
598 assert!(error.is_finite());
599 }
600
601 #[test]
604 fn test_transform_error_if_not_fitted() {
605 let config = make_config(10, 5, ReductionMethod::RandomProjection, 42);
606 let mut reducer = SemanticDimensionReducer::new(config);
607 let result = reducer.transform(&[1.0; 10]);
608 assert!(result.is_err());
609 assert!(result
610 .expect_err("should error")
611 .contains("not been fitted"));
612 }
613
614 #[test]
615 fn test_transform_error_wrong_input_dim() {
616 let config = make_config(10, 5, ReductionMethod::RandomProjection, 42);
617 let mut reducer = SemanticDimensionReducer::new(config);
618 let embeddings = vec![vec![1.0; 10]; 3];
619 reducer.fit(&embeddings).expect("fit");
620 let result = reducer.transform(&[1.0; 7]);
621 assert!(result.is_err());
622 assert!(result
623 .expect_err("should error")
624 .contains("dimension mismatch"));
625 }
626
627 #[test]
628 fn test_fit_error_output_dim_greater_than_input_dim() {
629 let config = make_config(5, 10, ReductionMethod::RandomProjection, 42);
630 let mut reducer = SemanticDimensionReducer::new(config);
631 let result = reducer.fit(&[vec![1.0; 5]]);
632 assert!(result.is_err());
633 }
634
635 #[test]
636 fn test_fit_error_wrong_embedding_dim() {
637 let config = make_config(10, 5, ReductionMethod::RandomProjection, 42);
638 let mut reducer = SemanticDimensionReducer::new(config);
639 let result = reducer.fit(&[vec![1.0; 7]]);
640 assert!(result.is_err());
641 assert!(result.expect_err("should error").contains("dimension"));
642 }
643
644 #[test]
647 fn test_fit_transform_random_projection() {
648 let config = make_config(20, 5, ReductionMethod::RandomProjection, 42);
649 let mut reducer = SemanticDimensionReducer::new(config);
650 let embeddings: Vec<Vec<f64>> = (0..10)
651 .map(|i| (0..20).map(|j| (i * 20 + j) as f64 * 0.01).collect())
652 .collect();
653 let results = reducer.fit_transform(&embeddings).expect("fit_transform");
654 assert_eq!(results.len(), 10);
655 for r in &results {
656 assert_eq!(r.len(), 5);
657 }
658 assert!(reducer.is_fitted());
659 }
660
661 #[test]
662 fn test_fit_transform_truncation() {
663 let config = make_config(8, 3, ReductionMethod::Truncation, 0);
664 let mut reducer = SemanticDimensionReducer::new(config);
665 let embeddings = vec![
666 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
667 vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0],
668 ];
669 let results = reducer.fit_transform(&embeddings).expect("fit_transform");
670 assert_eq!(results[0], vec![1.0, 2.0, 3.0]);
671 assert_eq!(results[1], vec![10.0, 20.0, 30.0]);
672 }
673
674 #[test]
675 fn test_fit_transform_pca() {
676 let config = make_config(6, 2, ReductionMethod::PCA, 99);
677 let mut reducer = SemanticDimensionReducer::new(config);
678 let embeddings: Vec<Vec<f64>> = (0..20)
679 .map(|i| {
680 let mut v = vec![0.0; 6];
681 v[0] = i as f64 * 3.0;
682 v[1] = i as f64 * 2.0;
683 v[2] = i as f64 * 0.1;
684 v
685 })
686 .collect();
687 let results = reducer.fit_transform(&embeddings).expect("fit_transform");
688 assert_eq!(results.len(), 20);
689 for r in &results {
690 assert_eq!(r.len(), 2);
691 }
692 }
693
694 #[test]
697 fn test_is_fitted_initially_false() {
698 let config = make_config(10, 5, ReductionMethod::RandomProjection, 42);
699 let reducer = SemanticDimensionReducer::new(config);
700 assert!(!reducer.is_fitted());
701 }
702
703 #[test]
704 fn test_reset_clears_state() {
705 let config = make_config(10, 5, ReductionMethod::RandomProjection, 42);
706 let mut reducer = SemanticDimensionReducer::new(config);
707 let embeddings = vec![vec![1.0; 10]; 5];
708 reducer.fit(&embeddings).expect("fit");
709 let _ = reducer.transform(&embeddings[0]);
710 assert!(reducer.is_fitted());
711 assert!(reducer.stats().reductions_performed > 0);
712
713 reducer.reset();
714
715 assert!(!reducer.is_fitted());
716 assert_eq!(reducer.stats().reductions_performed, 0);
717 assert!(reducer.transform(&embeddings[0]).is_err());
718 }
719
720 #[test]
721 fn test_stats_accuracy() {
722 let config = make_config(20, 8, ReductionMethod::RandomProjection, 55);
723 let mut reducer = SemanticDimensionReducer::new(config);
724 let stats = reducer.stats();
725 assert_eq!(stats.input_dim, 20);
726 assert_eq!(stats.output_dim, 8);
727 assert_eq!(stats.method, ReductionMethod::RandomProjection);
728 assert!(!stats.fitted);
729 assert_eq!(stats.reductions_performed, 0);
730
731 let embeddings = vec![vec![1.0; 20]; 3];
732 reducer.fit(&embeddings).expect("fit");
733 let _ = reducer.transform(&embeddings[0]);
734 let _ = reducer.transform(&embeddings[1]);
735
736 let stats = reducer.stats();
737 assert!(stats.fitted);
738 assert_eq!(stats.reductions_performed, 2);
739 }
740
741 #[test]
742 fn test_stats_method_truncation() {
743 let config = make_config(10, 5, ReductionMethod::Truncation, 0);
744 let reducer = SemanticDimensionReducer::new(config);
745 assert_eq!(reducer.stats().method, ReductionMethod::Truncation);
746 }
747
748 #[test]
749 fn test_stats_method_pca() {
750 let config = make_config(10, 5, ReductionMethod::PCA, 0);
751 let reducer = SemanticDimensionReducer::new(config);
752 assert_eq!(reducer.stats().method, ReductionMethod::PCA);
753 }
754
755 #[test]
758 fn test_input_dim_equals_output_dim() {
759 let config = make_config(5, 5, ReductionMethod::RandomProjection, 42);
760 let mut reducer = SemanticDimensionReducer::new(config);
761 let embedding = vec![1.0, 2.0, 3.0, 4.0, 5.0];
762 let embeddings = vec![embedding.clone()];
763 reducer.fit(&embeddings).expect("fit");
764 let result = reducer.transform(&embedding).expect("transform");
765 assert_eq!(result.len(), 5);
766 }
767
768 #[test]
769 fn test_input_dim_equals_output_dim_truncation() {
770 let config = make_config(5, 5, ReductionMethod::Truncation, 0);
771 let mut reducer = SemanticDimensionReducer::new(config);
772 let embedding = vec![1.0, 2.0, 3.0, 4.0, 5.0];
773 let embeddings = vec![embedding.clone()];
774 reducer.fit(&embeddings).expect("fit");
775 let result = reducer.transform(&embedding).expect("transform");
776 assert_eq!(result, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
777 }
778
779 #[test]
780 fn test_single_embedding() {
781 let config = make_config(10, 3, ReductionMethod::RandomProjection, 42);
782 let mut reducer = SemanticDimensionReducer::new(config);
783 let embeddings = vec![vec![1.0; 10]];
784 reducer.fit(&embeddings).expect("fit");
785 let result = reducer.transform(&embeddings[0]).expect("transform");
786 assert_eq!(result.len(), 3);
787 }
788
789 #[test]
790 fn test_single_embedding_pca() {
791 let config = make_config(10, 3, ReductionMethod::PCA, 42);
793 let mut reducer = SemanticDimensionReducer::new(config);
794 let embeddings = vec![vec![1.0; 10]];
795 reducer.fit(&embeddings).expect("fit");
796 let result = reducer.transform(&embeddings[0]).expect("transform");
797 assert_eq!(result.len(), 3);
798 }
799
800 #[test]
801 fn test_reduce_to_one_dimension() {
802 let config = make_config(50, 1, ReductionMethod::RandomProjection, 42);
803 let mut reducer = SemanticDimensionReducer::new(config);
804 let embeddings = vec![vec![0.5; 50]; 5];
805 reducer.fit(&embeddings).expect("fit");
806 let result = reducer.transform(&embeddings[0]).expect("transform");
807 assert_eq!(result.len(), 1);
808 }
809
810 #[test]
811 fn test_large_reduction_ratio() {
812 let config = make_config(1000, 2, ReductionMethod::RandomProjection, 42);
813 let mut reducer = SemanticDimensionReducer::new(config);
814 let embeddings = vec![vec![0.1; 1000]; 3];
815 reducer.fit(&embeddings).expect("fit");
816 let result = reducer.transform(&embeddings[0]).expect("transform");
817 assert_eq!(result.len(), 2);
818 }
819
820 #[test]
821 fn test_reductions_counter_increments() {
822 let config = make_config(10, 5, ReductionMethod::Truncation, 0);
823 let mut reducer = SemanticDimensionReducer::new(config);
824 let embeddings = vec![vec![1.0; 10]; 3];
825 reducer.fit(&embeddings).expect("fit");
826 assert_eq!(reducer.stats().reductions_performed, 0);
827 let _ = reducer.transform(&embeddings[0]);
828 assert_eq!(reducer.stats().reductions_performed, 1);
829 let _ = reducer.transform(&embeddings[1]);
830 let _ = reducer.transform(&embeddings[2]);
831 assert_eq!(reducer.stats().reductions_performed, 3);
832 }
833
834 #[test]
835 fn test_reduction_result_struct() {
836 let result = ReductionResult {
837 original_dim: 100,
838 reduced_dim: 10,
839 reconstruction_error: Some(0.05),
840 };
841 assert_eq!(result.original_dim, 100);
842 assert_eq!(result.reduced_dim, 10);
843 assert!((result.reconstruction_error.expect("should have error") - 0.05).abs() < 1e-10);
844 }
845
846 #[test]
847 fn test_reduction_result_no_error() {
848 let result = ReductionResult {
849 original_dim: 100,
850 reduced_dim: 10,
851 reconstruction_error: None,
852 };
853 assert!(result.reconstruction_error.is_none());
854 }
855
856 #[test]
857 fn test_reconstruction_error_zero_for_identity_truncation() {
858 let config = make_config(5, 5, ReductionMethod::Truncation, 0);
860 let mut reducer = SemanticDimensionReducer::new(config);
861 let embedding = vec![1.0, 2.0, 3.0, 4.0, 5.0];
862 reducer.fit(std::slice::from_ref(&embedding)).expect("fit");
863 let reduced = reducer.transform(&embedding).expect("transform");
864 let error = reducer.reconstruction_error(&embedding, &reduced);
865 assert!(
866 error < 1e-10,
867 "identity truncation should have ~0 error, got {error}"
868 );
869 }
870
871 #[test]
872 fn test_fit_transform_counts_reductions() {
873 let config = make_config(10, 3, ReductionMethod::RandomProjection, 42);
874 let mut reducer = SemanticDimensionReducer::new(config);
875 let embeddings = vec![vec![1.0; 10]; 7];
876 let _ = reducer.fit_transform(&embeddings).expect("fit_transform");
877 assert_eq!(reducer.stats().reductions_performed, 7);
878 }
879}