use super::config::{Config, FeatureFamily};
use super::dataset::{Class, Dataset, Trace};
use super::preprocessing::{median, percentile, preprocess};
#[derive(Debug, Clone)]
pub struct TraceFeatures {
pub features: Vec<f64>,
pub class: Class,
pub trace_id: u64,
}
#[derive(Debug, Clone)]
pub struct ExtractedFeatures {
pub dimension: usize,
pub fixed: Vec<TraceFeatures>,
pub random: Vec<TraceFeatures>,
}
impl ExtractedFeatures {
pub fn fixed_count(&self) -> usize {
self.fixed.len()
}
pub fn random_count(&self) -> usize {
self.random.len()
}
pub fn total_count(&self) -> usize {
self.fixed.len() + self.random.len()
}
}
pub fn partition_samples(samples: &[f32], num_partitions: usize) -> Vec<&[f32]> {
if samples.is_empty() || num_partitions == 0 {
return vec![];
}
let n = samples.len();
let base_size = n / num_partitions;
let remainder = n % num_partitions;
let mut partitions = Vec::with_capacity(num_partitions);
let mut start = 0;
for i in 0..num_partitions {
let extra = if i < remainder { 1 } else { 0 };
let end = start + base_size + extra;
partitions.push(&samples[start..end]);
start = end;
}
partitions
}
fn extract_mean_features(partitions: &[&[f32]]) -> Vec<f64> {
partitions
.iter()
.map(|p| {
if p.is_empty() {
0.0
} else {
p.iter().map(|v| *v as f64).sum::<f64>() / p.len() as f64
}
})
.collect()
}
fn extract_robust3_features(partitions: &[&[f32]]) -> Vec<f64> {
let mut features = Vec::with_capacity(partitions.len() * 3);
for p in partitions {
if p.is_empty() {
features.extend([0.0, 0.0, 0.0]);
} else {
let med = median(p) as f64;
let p10 = percentile(p, 10.0) as f64;
let p90 = percentile(p, 90.0) as f64;
features.push(med);
features.push(p10);
features.push(p90);
}
}
features
}
fn extract_centered_square_features(partitions: &[&[f32]]) -> Vec<f64> {
partitions
.iter()
.map(|p| {
if p.len() < 2 {
return 0.0;
}
let n = p.len() as f64;
let mean: f64 = p.iter().map(|v| *v as f64).sum::<f64>() / n;
let variance: f64 =
p.iter().map(|v| (*v as f64 - mean).powi(2)).sum::<f64>() / (n - 1.0);
variance
})
.collect()
}
pub fn extract_trace_features(trace: &Trace, config: &Config) -> TraceFeatures {
let mut samples: Vec<f32> = trace.samples.clone();
preprocess(&mut samples, &config.preprocessing);
let partitions = partition_samples(&samples, config.partition.num_partitions);
let features = match config.feature_family {
FeatureFamily::Mean => extract_mean_features(&partitions),
FeatureFamily::Robust3 => extract_robust3_features(&partitions),
FeatureFamily::CenteredSquare => extract_centered_square_features(&partitions),
};
TraceFeatures {
features,
class: trace.class,
trace_id: trace.id,
}
}
pub fn extract_features(dataset: &Dataset, config: &Config) -> ExtractedFeatures {
let dimension = config.feature_dimension();
let mut fixed = Vec::with_capacity(dataset.fixed_count());
let mut random = Vec::with_capacity(dataset.random_count());
for trace in &dataset.traces {
let features = extract_trace_features(trace, config);
match features.class {
Class::Fixed => fixed.push(features),
Class::Random => random.push(features),
}
}
ExtractedFeatures {
dimension,
fixed,
random,
}
}
pub fn compute_class_difference(features: &ExtractedFeatures) -> Vec<f64> {
if features.fixed.is_empty() || features.random.is_empty() {
return vec![0.0; features.dimension];
}
let d = features.dimension;
let n_fixed = features.fixed.len() as f64;
let n_random = features.random.len() as f64;
let mut mean_fixed = vec![0.0; d];
for tf in &features.fixed {
for (i, v) in tf.features.iter().enumerate() {
mean_fixed[i] += v;
}
}
for v in &mut mean_fixed {
*v /= n_fixed;
}
let mut mean_random = vec![0.0; d];
for tf in &features.random {
for (i, v) in tf.features.iter().enumerate() {
mean_random[i] += v;
}
}
for v in &mut mean_random {
*v /= n_random;
}
mean_fixed
.iter()
.zip(mean_random.iter())
.map(|(f, r)| f - r)
.collect()
}
pub fn compute_pooled_covariance(features: &ExtractedFeatures) -> Vec<f64> {
let d = features.dimension;
let n_total = features.total_count();
if n_total < 2 {
return vec![0.0; d * d];
}
let mut mean = vec![0.0; d];
for tf in features.fixed.iter().chain(features.random.iter()) {
for (i, v) in tf.features.iter().enumerate() {
mean[i] += v;
}
}
for v in &mut mean {
*v /= n_total as f64;
}
let mut cov = vec![0.0; d * d];
for tf in features.fixed.iter().chain(features.random.iter()) {
for i in 0..d {
let dev_i = tf.features[i] - mean[i];
for j in 0..d {
let dev_j = tf.features[j] - mean[j];
cov[i * d + j] += dev_i * dev_j;
}
}
}
let scale = 1.0 / (n_total - 1) as f64;
for v in &mut cov {
*v *= scale;
}
cov
}
#[cfg(test)]
mod tests {
use super::super::config::PreprocessingConfig;
use super::*;
#[test]
fn test_partition_samples() {
let samples: Vec<f32> = (0..10).map(|i| i as f32).collect();
let partitions = partition_samples(&samples, 3);
assert_eq!(partitions.len(), 3);
assert_eq!(partitions[0].len(), 4);
assert_eq!(partitions[1].len(), 3);
assert_eq!(partitions[2].len(), 3);
}
#[test]
fn test_mean_features() {
let samples: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let partitions = partition_samples(&samples, 2);
let features = extract_mean_features(&partitions);
assert_eq!(features.len(), 2);
assert!((features[0] - 2.0).abs() < 1e-6); assert!((features[1] - 5.0).abs() < 1e-6); }
#[test]
fn test_robust3_features() {
let samples: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let partitions = partition_samples(&samples, 2);
let features = extract_robust3_features(&partitions);
assert_eq!(features.len(), 6);
}
#[test]
fn test_centered_square_features() {
let samples: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let partitions = partition_samples(&samples, 2);
let features = extract_centered_square_features(&partitions);
assert_eq!(features.len(), 2);
assert!((features[0] - features[1]).abs() < 1e-6);
}
#[test]
fn test_extract_features() {
let traces = vec![
Trace::new(Class::Fixed, vec![1.0, 2.0, 3.0, 4.0]),
Trace::new(Class::Random, vec![2.0, 3.0, 4.0, 5.0]),
];
let dataset = Dataset::new(traces);
let config = Config::new().with_partitions(2);
let features = extract_features(&dataset, &config);
assert_eq!(features.dimension, 2);
assert_eq!(features.fixed_count(), 1);
assert_eq!(features.random_count(), 1);
}
#[test]
fn test_class_difference() {
let traces = vec![
Trace::new(Class::Fixed, vec![10.0, 20.0, 30.0, 40.0]),
Trace::new(Class::Fixed, vec![12.0, 22.0, 32.0, 42.0]),
Trace::new(Class::Random, vec![5.0, 15.0, 25.0, 35.0]),
Trace::new(Class::Random, vec![7.0, 17.0, 27.0, 37.0]),
];
let dataset = Dataset::new(traces);
let mut config = Config::new().with_partitions(2);
config.preprocessing = PreprocessingConfig::none();
let features = extract_features(&dataset, &config);
let diff = compute_class_difference(&features);
assert!(diff[0] > 0.0);
assert!(diff[1] > 0.0);
}
#[test]
fn test_pooled_covariance() {
let traces = vec![
Trace::new(Class::Fixed, vec![1.0, 2.0, 3.0, 4.0]),
Trace::new(Class::Fixed, vec![2.0, 3.0, 4.0, 5.0]),
Trace::new(Class::Random, vec![1.5, 2.5, 3.5, 4.5]),
Trace::new(Class::Random, vec![2.5, 3.5, 4.5, 5.5]),
];
let dataset = Dataset::new(traces);
let mut config = Config::new().with_partitions(2);
config.preprocessing = PreprocessingConfig::none();
let features = extract_features(&dataset, &config);
let cov = compute_pooled_covariance(&features);
assert_eq!(cov.len(), 4);
assert!(cov[0] > 0.0);
assert!(cov[3] > 0.0);
}
}