1#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub enum FeatureKind {
9 Mean,
11 Variance,
13 Skewness,
15 Min,
17 Max,
19 Range,
21 L1Norm,
23 L2Norm,
25 Histogram {
28 bins: usize,
30 },
31}
32
33#[derive(Clone, Debug, PartialEq)]
37pub struct ExtractedFeature {
38 pub kind: FeatureKind,
40 pub values: Vec<f64>,
42}
43
44#[derive(Clone, Debug)]
48pub struct ExtractorConfig {
49 pub features: Vec<FeatureKind>,
51 pub histogram_bins: usize,
54}
55
56impl Default for ExtractorConfig {
57 fn default() -> Self {
58 Self {
59 features: vec![
60 FeatureKind::Mean,
61 FeatureKind::Variance,
62 FeatureKind::Min,
63 FeatureKind::Max,
64 FeatureKind::L2Norm,
65 ],
66 histogram_bins: 10,
67 }
68 }
69}
70
71#[derive(Clone, Debug)]
75pub struct ExtractionResult {
76 pub tensor_id: u64,
78 pub features: Vec<ExtractedFeature>,
80}
81
82impl ExtractionResult {
83 pub fn feature_vector(&self) -> Vec<f64> {
85 self.features
86 .iter()
87 .flat_map(|f| f.values.iter().copied())
88 .collect()
89 }
90}
91
92#[derive(Clone, Debug, Default)]
96pub struct ExtractorStats {
97 pub total_extractions: u64,
99 pub total_tensors_processed: u64,
101 pub avg_feature_vector_len: f64,
103}
104
105pub struct TensorFeatureExtractor {
109 pub config: ExtractorConfig,
111 pub stats: ExtractorStats,
113}
114
115impl TensorFeatureExtractor {
116 pub fn new(config: ExtractorConfig) -> Self {
118 Self {
119 config,
120 stats: ExtractorStats::default(),
121 }
122 }
123
124 pub fn extract(&mut self, tensor_id: u64, data: &[f64]) -> ExtractionResult {
128 let n = data.len();
129
130 let precomputed = PrecomputedStats::compute(data);
132
133 let features: Vec<ExtractedFeature> = self
134 .config
135 .features
136 .iter()
137 .map(|kind| {
138 let values = extract_one(kind, data, n, &precomputed);
139 ExtractedFeature {
140 kind: kind.clone(),
141 values,
142 }
143 })
144 .collect();
145
146 let fv_len: usize = features.iter().map(|f| f.values.len()).sum();
148 let prev_total = self.stats.total_tensors_processed;
149 let prev_avg = self.stats.avg_feature_vector_len;
150
151 self.stats.total_tensors_processed += 1;
152 self.stats.total_extractions += features.len() as u64;
153
154 let new_total = prev_total + 1;
156 self.stats.avg_feature_vector_len =
157 (prev_avg * prev_total as f64 + fv_len as f64) / new_total as f64;
158
159 ExtractionResult {
160 tensor_id,
161 features,
162 }
163 }
164
165 pub fn extract_batch(&mut self, tensors: Vec<(u64, Vec<f64>)>) -> Vec<ExtractionResult> {
168 tensors
169 .into_iter()
170 .map(|(id, data)| self.extract(id, &data))
171 .collect()
172 }
173
174 pub fn stats(&self) -> &ExtractorStats {
176 &self.stats
177 }
178}
179
180struct PrecomputedStats {
186 mean: f64,
187 variance: f64,
188 std_dev: f64,
190 min: f64,
191 max: f64,
192}
193
194impl PrecomputedStats {
195 fn compute(data: &[f64]) -> Self {
196 if data.is_empty() {
197 return Self {
198 mean: 0.0,
199 variance: 0.0,
200 std_dev: 0.0,
201 min: 0.0,
202 max: 0.0,
203 };
204 }
205
206 let n = data.len() as f64;
207
208 let mut sum = 0.0_f64;
210 let mut sum_sq = 0.0_f64;
211 let mut min = data[0];
212 let mut max = data[0];
213
214 for &x in data {
215 sum += x;
216 sum_sq += x * x;
217 if x < min {
218 min = x;
219 }
220 if x > max {
221 max = x;
222 }
223 }
224
225 let mean = sum / n;
226 let variance = (sum_sq / n) - (mean * mean);
228 let variance = if variance < 0.0 { 0.0 } else { variance };
230 let std_dev = variance.sqrt();
231
232 Self {
233 mean,
234 variance,
235 std_dev,
236 min,
237 max,
238 }
239 }
240}
241
242fn extract_one(kind: &FeatureKind, data: &[f64], n: usize, pre: &PrecomputedStats) -> Vec<f64> {
244 match kind {
245 FeatureKind::Mean => vec![pre.mean],
246
247 FeatureKind::Variance => vec![pre.variance],
248
249 FeatureKind::Skewness => {
250 if n == 0 || pre.std_dev == 0.0 {
251 return vec![0.0];
252 }
253 let mu = pre.mean;
255 let sigma3 = pre.std_dev * pre.std_dev * pre.std_dev;
256 let third_moment: f64 = data
257 .iter()
258 .map(|&x| {
259 let d = x - mu;
260 d * d * d
261 })
262 .sum::<f64>()
263 / n as f64;
264 vec![third_moment / sigma3]
265 }
266
267 FeatureKind::Min => vec![pre.min],
268
269 FeatureKind::Max => vec![pre.max],
270
271 FeatureKind::Range => vec![pre.max - pre.min],
272
273 FeatureKind::L1Norm => {
274 let l1: f64 = data.iter().map(|x| x.abs()).sum();
275 vec![l1]
276 }
277
278 FeatureKind::L2Norm => {
279 let l2: f64 = data.iter().map(|x| x * x).sum::<f64>().sqrt();
280 vec![l2]
281 }
282
283 FeatureKind::Histogram { bins } => {
284 let bins = *bins;
285 if bins == 0 {
286 return Vec::new();
287 }
288
289 let mut counts = vec![0.0_f64; bins];
290
291 if n == 0 {
292 return counts;
293 }
294
295 let lo = pre.min;
296 let hi = pre.max;
297
298 if (hi - lo).abs() < f64::EPSILON {
299 counts[0] = n as f64;
301 } else {
302 let width = hi - lo;
303 for &x in data {
304 let t = (x - lo) / width;
306 let idx = ((t * bins as f64) as usize).min(bins - 1);
308 counts[idx] += 1.0;
309 }
310 }
311
312 counts
313 }
314 }
315}
316
317#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn extractor_for(kinds: Vec<FeatureKind>) -> TensorFeatureExtractor {
328 TensorFeatureExtractor::new(ExtractorConfig {
329 features: kinds,
330 histogram_bins: 10,
331 })
332 }
333
334 fn single(kind: FeatureKind, data: &[f64]) -> f64 {
335 let mut ex = extractor_for(vec![kind]);
336 let res = ex.extract(0, data);
337 res.features[0].values[0]
338 }
339
340 #[test]
343 fn test_mean_basic() {
344 let v = single(FeatureKind::Mean, &[1.0, 2.0, 3.0, 4.0, 5.0]);
345 assert!((v - 3.0).abs() < 1e-12, "mean={v}");
346 }
347
348 #[test]
349 fn test_mean_single_element() {
350 let v = single(FeatureKind::Mean, &[7.5]);
351 assert!((v - 7.5).abs() < 1e-12, "mean={v}");
352 }
353
354 #[test]
355 fn test_mean_empty() {
356 let v = single(FeatureKind::Mean, &[]);
357 assert_eq!(v, 0.0);
358 }
359
360 #[test]
361 fn test_mean_negative() {
362 let v = single(FeatureKind::Mean, &[-2.0, -4.0]);
363 assert!((v - (-3.0)).abs() < 1e-12, "mean={v}");
364 }
365
366 #[test]
369 fn test_variance_basic() {
370 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
372 let v = single(FeatureKind::Variance, &data);
373 assert!((v - 4.0).abs() < 1e-10, "var={v}");
374 }
375
376 #[test]
377 fn test_variance_empty() {
378 assert_eq!(single(FeatureKind::Variance, &[]), 0.0);
379 }
380
381 #[test]
382 fn test_variance_constant_data() {
383 let data = [3.0_f64; 100];
385 let v = single(FeatureKind::Variance, &data);
386 assert!(v.abs() < 1e-10, "var={v}");
387 }
388
389 #[test]
390 fn test_variance_population_formula() {
391 let v = single(FeatureKind::Variance, &[1.0, 3.0]);
393 assert!((v - 1.0).abs() < 1e-12, "var={v}");
394 }
395
396 #[test]
399 fn test_skewness_empty() {
400 assert_eq!(single(FeatureKind::Skewness, &[]), 0.0);
401 }
402
403 #[test]
404 fn test_skewness_zero_when_sigma_zero() {
405 let data = [5.0_f64; 10];
407 assert_eq!(single(FeatureKind::Skewness, &data), 0.0);
408 }
409
410 #[test]
411 fn test_skewness_symmetric_distribution() {
412 let data: Vec<f64> = (-50..=50).map(|i| i as f64).collect();
414 let v = single(FeatureKind::Skewness, &data);
415 assert!(v.abs() < 1e-10, "skewness={v}");
416 }
417
418 #[test]
419 fn test_skewness_right_skewed() {
420 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 100.0];
422 let v = single(FeatureKind::Skewness, &data);
423 assert!(v > 0.0, "expected positive skewness, got {v}");
424 }
425
426 #[test]
429 fn test_min_basic() {
430 assert!((single(FeatureKind::Min, &[3.0, 1.0, 4.0, 1.0, 5.0]) - 1.0).abs() < 1e-12);
431 }
432
433 #[test]
434 fn test_max_basic() {
435 assert!((single(FeatureKind::Max, &[3.0, 1.0, 4.0, 1.0, 5.0]) - 5.0).abs() < 1e-12);
436 }
437
438 #[test]
439 fn test_min_empty() {
440 assert_eq!(single(FeatureKind::Min, &[]), 0.0);
441 }
442
443 #[test]
444 fn test_max_empty() {
445 assert_eq!(single(FeatureKind::Max, &[]), 0.0);
446 }
447
448 #[test]
451 fn test_range_basic() {
452 let v = single(FeatureKind::Range, &[1.0, 5.0, 3.0, -2.0]);
453 assert!((v - 7.0).abs() < 1e-12, "range={v}");
454 }
455
456 #[test]
457 fn test_range_empty() {
458 assert_eq!(single(FeatureKind::Range, &[]), 0.0);
459 }
460
461 #[test]
462 fn test_range_single() {
463 assert_eq!(single(FeatureKind::Range, &[42.0]), 0.0);
464 }
465
466 #[test]
469 fn test_l1norm_basic() {
470 let v = single(FeatureKind::L1Norm, &[1.0, -2.0, 3.0, -4.0]);
472 assert!((v - 10.0).abs() < 1e-12, "l1={v}");
473 }
474
475 #[test]
476 fn test_l1norm_empty() {
477 assert_eq!(single(FeatureKind::L1Norm, &[]), 0.0);
478 }
479
480 #[test]
483 fn test_l2norm_basic() {
484 let v = single(FeatureKind::L2Norm, &[3.0, 4.0]);
486 assert!((v - 5.0).abs() < 1e-12, "l2={v}");
487 }
488
489 #[test]
490 fn test_l2norm_empty() {
491 assert_eq!(single(FeatureKind::L2Norm, &[]), 0.0);
492 }
493
494 #[test]
497 fn test_histogram_uniform_distribution() {
498 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
500 let mut ex = extractor_for(vec![FeatureKind::Histogram { bins: 10 }]);
501 let res = ex.extract(0, &data);
502 let counts = &res.features[0].values;
503 assert_eq!(counts.len(), 10);
504 for &c in counts {
505 assert!((c - 10.0).abs() < 1.0, "bucket count unexpected: {c}");
507 }
508 }
509
510 #[test]
511 fn test_histogram_min_max_all_in_bucket_zero() {
512 let data = [7.0_f64; 20];
514 let mut ex = extractor_for(vec![FeatureKind::Histogram { bins: 5 }]);
515 let res = ex.extract(0, &data);
516 let counts = &res.features[0].values;
517 assert_eq!(counts.len(), 5);
518 assert!((counts[0] - 20.0).abs() < 1e-12, "bin0={}", counts[0]);
519 for &c in &counts[1..] {
520 assert_eq!(c, 0.0);
521 }
522 }
523
524 #[test]
525 fn test_histogram_empty_data() {
526 let mut ex = extractor_for(vec![FeatureKind::Histogram { bins: 4 }]);
527 let res = ex.extract(0, &[]);
528 let counts = &res.features[0].values;
529 assert_eq!(counts.len(), 4);
530 for &c in counts {
531 assert_eq!(c, 0.0);
532 }
533 }
534
535 #[test]
536 fn test_histogram_known_distribution() {
537 let data = [0.0, 1.0, 2.0];
539 let mut ex = extractor_for(vec![FeatureKind::Histogram { bins: 3 }]);
540 let res = ex.extract(0, &data);
541 let counts = &res.features[0].values;
542 assert_eq!(counts.len(), 3);
543 let total: f64 = counts.iter().sum();
545 assert!((total - 3.0).abs() < 1e-12, "total={total}");
546 }
547
548 #[test]
551 fn test_feature_vector_flatten() {
552 let mut ex = extractor_for(vec![
553 FeatureKind::Mean,
554 FeatureKind::Histogram { bins: 3 },
555 FeatureKind::L2Norm,
556 ]);
557 let res = ex.extract(42, &[1.0, 2.0, 3.0]);
558 let fv = res.feature_vector();
559 assert_eq!(fv.len(), 5, "fv={fv:?}");
561 }
562
563 #[test]
564 fn test_feature_vector_all_scalar() {
565 let mut ex = extractor_for(vec![
566 FeatureKind::Mean,
567 FeatureKind::Variance,
568 FeatureKind::Min,
569 FeatureKind::Max,
570 FeatureKind::L2Norm,
571 ]);
572 let res = ex.extract(0, &[1.0, 2.0, 3.0]);
573 assert_eq!(res.feature_vector().len(), 5);
574 }
575
576 #[test]
579 fn test_extract_batch_multiple_tensors() {
580 let mut ex = extractor_for(vec![FeatureKind::Mean]);
581 let results = ex.extract_batch(vec![
582 (1, vec![1.0, 2.0, 3.0]),
583 (2, vec![4.0, 5.0, 6.0]),
584 (3, vec![7.0, 8.0, 9.0]),
585 ]);
586 assert_eq!(results.len(), 3);
587 assert!((results[0].features[0].values[0] - 2.0).abs() < 1e-12);
588 assert!((results[1].features[0].values[0] - 5.0).abs() < 1e-12);
589 assert!((results[2].features[0].values[0] - 8.0).abs() < 1e-12);
590 }
591
592 #[test]
593 fn test_extract_batch_tensor_ids_preserved() {
594 let mut ex = extractor_for(vec![FeatureKind::Max]);
595 let results = ex.extract_batch(vec![(99, vec![1.0]), (1000, vec![2.0])]);
596 assert_eq!(results[0].tensor_id, 99);
597 assert_eq!(results[1].tensor_id, 1000);
598 }
599
600 #[test]
603 fn test_stats_accumulate_tensors_processed() {
604 let mut ex = extractor_for(vec![FeatureKind::Mean, FeatureKind::Variance]);
605 ex.extract(0, &[1.0, 2.0]);
606 ex.extract(1, &[3.0, 4.0]);
607 ex.extract(2, &[5.0, 6.0]);
608 assert_eq!(ex.stats().total_tensors_processed, 3);
609 }
610
611 #[test]
612 fn test_stats_total_extractions() {
613 let mut ex = extractor_for(vec![FeatureKind::Mean, FeatureKind::Min, FeatureKind::Max]);
615 ex.extract(0, &[1.0]);
616 ex.extract(1, &[2.0]);
617 assert_eq!(ex.stats().total_extractions, 6);
618 }
619
620 #[test]
621 fn test_stats_avg_feature_vector_len() {
622 let mut ex = extractor_for(vec![FeatureKind::Mean, FeatureKind::L2Norm]);
624 ex.extract(0, &[1.0]);
625 ex.extract(1, &[2.0]);
626 assert!((ex.stats().avg_feature_vector_len - 2.0).abs() < 1e-12);
628 }
629
630 #[test]
631 fn test_stats_via_accessor() {
632 let mut ex = extractor_for(vec![FeatureKind::Mean]);
633 ex.extract(0, &[1.0]);
634 let s = ex.stats();
635 assert_eq!(s.total_tensors_processed, 1);
636 }
637
638 #[test]
641 fn test_default_config_features() {
642 let cfg = ExtractorConfig::default();
643 assert!(cfg.features.contains(&FeatureKind::Mean));
644 assert!(cfg.features.contains(&FeatureKind::Variance));
645 assert!(cfg.features.contains(&FeatureKind::Min));
646 assert!(cfg.features.contains(&FeatureKind::Max));
647 assert!(cfg.features.contains(&FeatureKind::L2Norm));
648 assert_eq!(cfg.histogram_bins, 10);
649 }
650
651 #[test]
654 fn test_all_features_empty_data() {
655 let kinds = vec![
656 FeatureKind::Mean,
657 FeatureKind::Variance,
658 FeatureKind::Skewness,
659 FeatureKind::Min,
660 FeatureKind::Max,
661 FeatureKind::Range,
662 FeatureKind::L1Norm,
663 FeatureKind::L2Norm,
664 FeatureKind::Histogram { bins: 4 },
665 ];
666 let mut ex = extractor_for(kinds);
667 let res = ex.extract(0, &[]);
668 for feat in &res.features {
669 match &feat.kind {
670 FeatureKind::Histogram { bins } => {
671 assert_eq!(feat.values.len(), *bins);
672 for &v in &feat.values {
673 assert_eq!(v, 0.0);
674 }
675 }
676 _ => {
677 assert_eq!(feat.values.len(), 1);
678 assert_eq!(feat.values[0], 0.0, "kind={:?}", feat.kind);
679 }
680 }
681 }
682 }
683
684 #[test]
685 fn test_histogram_bin_total_equals_n() {
686 let data: Vec<f64> = (0..137).map(|i| i as f64 * 0.7 - 10.0).collect();
688 let mut ex = extractor_for(vec![FeatureKind::Histogram { bins: 7 }]);
689 let res = ex.extract(0, &data);
690 let total: f64 = res.features[0].values.iter().sum();
691 assert!((total - data.len() as f64).abs() < 1e-10, "total={total}");
692 }
693}