#[derive(Debug, Clone)]
pub struct SignatureConfig {
pub dim: usize,
pub max_depth: usize,
}
pub fn signature_size(dim: usize, max_depth: usize) -> usize {
if dim == 0 || max_depth == 0 {
return 0;
}
if dim == 1 {
return max_depth;
}
let mut total: usize = 0;
let mut power: usize = 1;
for _ in 0..max_depth {
power = power.saturating_mul(dim);
total = total.saturating_add(power);
}
total
}
#[derive(Debug, Clone)]
pub struct SignatureExtractor {
dim: usize,
max_depth: usize,
terms: Vec<f64>,
offsets: Vec<usize>,
count: usize,
last: Option<Vec<f64>>,
}
impl SignatureExtractor {
pub fn new(dim: usize, max_depth: usize) -> Self {
let total = signature_size(dim, max_depth);
let mut offsets = Vec::with_capacity(max_depth + 1);
offsets.push(0);
let mut power = 1;
for _ in 0..max_depth {
power *= dim;
offsets.push(offsets.last().unwrap() + power);
}
Self {
dim,
max_depth,
terms: vec![0.0; total],
offsets,
count: 0,
last: None,
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn max_depth(&self) -> usize {
self.max_depth
}
pub fn count(&self) -> usize {
self.count
}
pub fn num_terms(&self) -> usize {
self.terms.len()
}
pub fn observe(&mut self, point: &[f64]) {
assert_eq!(
point.len(),
self.dim,
"point dimension {} != extractor dimension {}",
point.len(),
self.dim
);
self.count += 1;
if let Some(ref last) = self.last {
let dx: Vec<f64> = point.iter().zip(last.iter()).map(|(a, b)| a - b).collect();
self.extend_signature(&dx);
}
self.last = Some(point.to_vec());
}
fn extend_signature(&mut self, dx: &[f64]) {
for depth in (1..=self.max_depth).rev() {
if depth == 1 {
for (term, &dx_val) in self.terms[..self.dim].iter_mut().zip(dx.iter()) {
*term += dx_val;
}
} else {
let parent_start = self.offsets[depth - 2];
let parent_count = self.offsets[depth - 1] - parent_start;
let child_start = self.offsets[depth - 1];
let parents: Vec<f64> =
self.terms[parent_start..parent_start + parent_count].to_vec();
for (p_idx, &parent_val) in parents.iter().enumerate() {
for (d_idx, &dx_val) in dx.iter().enumerate() {
let child_idx = child_start + p_idx * self.dim + d_idx;
self.terms[child_idx] += parent_val * dx_val;
}
}
}
}
}
pub fn signature(&self) -> &[f64] {
&self.terms
}
pub fn signature_at_depth(&self, depth: usize) -> Option<&[f64]> {
if depth == 0 || depth > self.max_depth {
return None;
}
let start = self.offsets[depth - 1];
let end = self.offsets[depth];
Some(&self.terms[start..end])
}
pub fn norm(&self) -> f64 {
self.terms.iter().map(|x| x * x).sum::<f64>().sqrt()
}
pub fn reset(&mut self) {
self.terms.fill(0.0);
self.count = 0;
self.last = None;
}
}
pub fn signature_distance(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "signature length mismatch");
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y) * (x - y))
.sum::<f64>()
.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_size_basic() {
assert_eq!(signature_size(0, 3), 0);
assert_eq!(signature_size(3, 0), 0);
assert_eq!(signature_size(1, 3), 3); assert_eq!(signature_size(2, 1), 2); assert_eq!(signature_size(2, 2), 6); assert_eq!(signature_size(2, 3), 14); assert_eq!(signature_size(3, 2), 12); }
#[test]
fn extractor_initial_state() {
let ext = SignatureExtractor::new(2, 3);
assert_eq!(ext.dim(), 2);
assert_eq!(ext.max_depth(), 3);
assert_eq!(ext.count(), 0);
assert_eq!(ext.num_terms(), 14); assert!(ext.signature().iter().all(|&x| x == 0.0));
}
#[test]
fn single_observation_no_signature() {
let mut ext = SignatureExtractor::new(2, 2);
ext.observe(&[1.0, 2.0]);
assert!(ext.signature().iter().all(|&x| x == 0.0));
assert_eq!(ext.count(), 1);
}
#[test]
fn depth1_is_total_increment() {
let mut ext = SignatureExtractor::new(2, 2);
ext.observe(&[0.0, 0.0]);
ext.observe(&[1.0, 3.0]);
ext.observe(&[4.0, 5.0]);
let d1 = ext.signature_at_depth(1).unwrap();
assert!((d1[0] - 4.0).abs() < 1e-10);
assert!((d1[1] - 5.0).abs() < 1e-10);
}
#[test]
fn depth2_iterated_integrals() {
let mut ext = SignatureExtractor::new(1, 2);
ext.observe(&[0.0]);
ext.observe(&[1.0]);
ext.observe(&[3.0]);
ext.observe(&[6.0]);
let d1 = ext.signature_at_depth(1).unwrap();
assert!((d1[0] - 6.0).abs() < 1e-10);
let d2 = ext.signature_at_depth(2).unwrap();
assert!((d2[0] - 11.0).abs() < 1e-10);
}
#[test]
fn signature_depth_out_of_range() {
let ext = SignatureExtractor::new(2, 3);
assert!(ext.signature_at_depth(0).is_none());
assert!(ext.signature_at_depth(4).is_none());
assert!(ext.signature_at_depth(1).is_some());
assert!(ext.signature_at_depth(3).is_some());
}
#[test]
fn norm_computation() {
let mut ext = SignatureExtractor::new(1, 1);
ext.observe(&[0.0]);
ext.observe(&[3.0]);
assert!((ext.norm() - 3.0).abs() < 1e-10);
}
#[test]
fn distance_computation() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 6.0, 3.0];
assert!((signature_distance(&a, &b) - 5.0).abs() < 1e-10);
}
#[test]
fn reset() {
let mut ext = SignatureExtractor::new(2, 2);
ext.observe(&[0.0, 0.0]);
ext.observe(&[1.0, 1.0]);
assert!(ext.norm() > 0.0);
ext.reset();
assert_eq!(ext.count(), 0);
assert!(ext.signature().iter().all(|&x| x == 0.0));
}
#[test]
fn two_dim_depth3() {
let mut ext = SignatureExtractor::new(2, 3);
ext.observe(&[0.0, 0.0]);
ext.observe(&[1.0, 0.0]);
ext.observe(&[1.0, 1.0]);
let d1 = ext.signature_at_depth(1).unwrap();
assert!((d1[0] - 1.0).abs() < 1e-10);
assert!((d1[1] - 1.0).abs() < 1e-10);
let d2 = ext.signature_at_depth(2).unwrap();
assert!((d2[0] - 0.0).abs() < 1e-10); assert!((d2[1] - 1.0).abs() < 1e-10); assert!((d2[2] - 0.0).abs() < 1e-10); assert!((d2[3] - 0.0).abs() < 1e-10); }
#[test]
fn translation_invariance() {
let mut ext1 = SignatureExtractor::new(2, 2);
ext1.observe(&[0.0, 0.0]);
ext1.observe(&[1.0, 2.0]);
ext1.observe(&[3.0, 1.0]);
let mut ext2 = SignatureExtractor::new(2, 2);
ext2.observe(&[100.0, 200.0]);
ext2.observe(&[101.0, 202.0]);
ext2.observe(&[103.0, 201.0]);
let s1 = ext1.signature();
let s2 = ext2.signature();
for (a, b) in s1.iter().zip(s2.iter()) {
assert!(
(a - b).abs() < 1e-10,
"Translation invariance violated: {} != {}",
a,
b
);
}
}
#[test]
fn constant_path_zero_signature() {
let mut ext = SignatureExtractor::new(3, 3);
for _ in 0..10 {
ext.observe(&[5.0, 5.0, 5.0]);
}
assert!(ext.signature().iter().all(|&x| x == 0.0));
}
#[test]
fn signature_distance_zero_for_same() {
let a = vec![1.0, 2.0, 3.0];
assert!((signature_distance(&a, &a)).abs() < 1e-15);
}
#[test]
#[should_panic(expected = "point dimension")]
fn wrong_dimension_panics() {
let mut ext = SignatureExtractor::new(2, 2);
ext.observe(&[1.0]); }
#[test]
fn frame_timing_anomaly_detection() {
let mut normal = SignatureExtractor::new(1, 3);
for &t in &[16.0, 16.1, 15.9, 16.0, 16.2, 15.8, 16.0, 16.1] {
normal.observe(&[t]);
}
let mut anomalous = SignatureExtractor::new(1, 3);
for &t in &[16.0, 16.1, 15.9, 64.0, 16.0, 16.0, 16.0, 16.0] {
anomalous.observe(&[t]);
}
let dist = signature_distance(normal.signature(), anomalous.signature());
assert!(
dist > 1.0,
"Anomalous path should be far from normal (dist={})",
dist
);
}
}