1#[derive(Debug, Clone)]
31pub struct SignatureConfig {
32 pub depth: usize,
34 pub time_augmentation: bool,
37}
38
39impl Default for SignatureConfig {
40 fn default() -> Self {
41 Self {
42 depth: 2,
43 time_augmentation: false,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct PathSignatureResult {
51 pub depth: usize,
53 pub input_dim: usize,
55 pub signature: Vec<f64>,
57 pub output_dim: usize,
59}
60
61pub fn compute_signature(
82 trajectory: &[(i64, &[f32])],
83 config: &SignatureConfig,
84) -> Result<PathSignatureResult, SignatureError> {
85 if trajectory.len() < 2 {
86 return Err(SignatureError::InsufficientData {
87 got: trajectory.len(),
88 need: 2,
89 });
90 }
91
92 let k = trajectory[0].1.len();
93 if k == 0 {
94 return Err(SignatureError::ZeroDimension);
95 }
96
97 let (points, dim) = if config.time_augmentation {
99 let t0 = trajectory[0].0 as f64;
100 let t_range = (trajectory.last().unwrap().0 - trajectory[0].0).max(1) as f64;
101 let augmented: Vec<Vec<f64>> = trajectory
102 .iter()
103 .map(|(ts, vec)| {
104 let mut p = Vec::with_capacity(k + 1);
105 p.push((*ts as f64 - t0) / t_range); p.extend(vec.iter().map(|&v| v as f64));
107 p
108 })
109 .collect();
110 (augmented, k + 1)
111 } else {
112 let points: Vec<Vec<f64>> = trajectory
113 .iter()
114 .map(|(_, vec)| vec.iter().map(|&v| v as f64).collect())
115 .collect();
116 (points, k)
117 };
118
119 let n = points.len();
121 let increments: Vec<Vec<f64>> = (0..n - 1)
122 .map(|i| (0..dim).map(|d| points[i + 1][d] - points[i][d]).collect())
123 .collect();
124
125 let mut signature = Vec::new();
126 let mut output_dim = 0;
127
128 if config.depth >= 1 {
130 let mut level1 = vec![0.0f64; dim];
131 for dx in &increments {
132 for d in 0..dim {
133 level1[d] += dx[d];
134 }
135 }
136 output_dim += dim;
137 signature.extend_from_slice(&level1);
138 }
139
140 if config.depth >= 2 {
142 let mut level2 = vec![0.0f64; dim * dim];
143 let mut cumsum = vec![0.0f64; dim];
146 for dx in &increments {
147 for i in 0..dim {
149 for j in 0..dim {
150 level2[i * dim + j] += cumsum[i] * dx[j];
151 }
152 }
153 for d in 0..dim {
155 cumsum[d] += dx[d];
156 }
157 }
158 output_dim += dim * dim;
159 signature.extend_from_slice(&level2);
160 }
161
162 if config.depth >= 3 {
164 let mut level3 = vec![0.0f64; dim * dim * dim];
165 let mut cumsum1 = vec![0.0f64; dim];
168 let mut cumsum2 = vec![0.0f64; dim * dim];
169 for dx in &increments {
170 for i in 0..dim {
172 for j in 0..dim {
173 for kk in 0..dim {
174 level3[i * dim * dim + j * dim + kk] += cumsum2[i * dim + j] * dx[kk];
175 }
176 }
177 }
178 for i in 0..dim {
180 for j in 0..dim {
181 cumsum2[i * dim + j] += cumsum1[i] * dx[j];
182 }
183 }
184 for d in 0..dim {
186 cumsum1[d] += dx[d];
187 }
188 }
189 output_dim += dim * dim * dim;
190 signature.extend_from_slice(&level3);
191 }
192
193 Ok(PathSignatureResult {
194 depth: config.depth,
195 input_dim: dim,
196 signature,
197 output_dim,
198 })
199}
200
201pub fn compute_log_signature(
209 trajectory: &[(i64, &[f32])],
210 config: &SignatureConfig,
211) -> Result<PathSignatureResult, SignatureError> {
212 let full = compute_signature(trajectory, config)?;
214 let dim = full.input_dim;
215
216 let mut log_sig = Vec::new();
217
218 log_sig.extend_from_slice(&full.signature[..dim]);
220
221 if config.depth >= 2 {
223 let level2_start = dim;
224 for i in 0..dim {
225 for j in (i + 1)..dim {
226 let s_ij = full.signature[level2_start + i * dim + j];
227 let s_ji = full.signature[level2_start + j * dim + i];
228 log_sig.push((s_ij - s_ji) / 2.0);
229 }
230 }
231 }
232
233 if config.depth >= 3 {
236 let level3_start = dim + dim * dim;
237 let level3_len = dim * dim * dim;
238 if full.signature.len() >= level3_start + level3_len {
239 log_sig.extend_from_slice(&full.signature[level3_start..level3_start + level3_len]);
240 }
241 }
242
243 let output_dim = log_sig.len();
244 Ok(PathSignatureResult {
245 depth: config.depth,
246 input_dim: dim,
247 signature: log_sig,
248 output_dim,
249 })
250}
251
252pub fn update_signature_incremental(
265 existing: &mut PathSignatureResult,
266 last_point: &[f32],
267 new_point: &[f32],
268) -> Result<(), SignatureError> {
269 let dim = existing.input_dim;
270 if last_point.len() != dim || new_point.len() != dim {
271 return Err(SignatureError::DimensionMismatch {
272 expected: dim,
273 got: new_point.len(),
274 });
275 }
276
277 let dx: Vec<f64> = (0..dim)
279 .map(|d| new_point[d] as f64 - last_point[d] as f64)
280 .collect();
281
282 for (sig, &delta) in existing.signature.iter_mut().zip(dx.iter()).take(dim) {
289 *sig += delta;
290 }
291
292 if existing.depth >= 2 {
294 let level2_start = dim;
295 for i in 0..dim {
299 let old_s1_i = existing.signature[i] - dx[i];
300 for (j, &dxj) in dx.iter().enumerate().take(dim) {
301 existing.signature[level2_start + i * dim + j] += old_s1_i * dxj;
302 }
303 }
304 }
305
306 Ok(())
307}
308
309pub fn signature_distance(a: &PathSignatureResult, b: &PathSignatureResult) -> f64 {
314 if a.signature.len() != b.signature.len() {
315 return f64::INFINITY;
316 }
317 a.signature
318 .iter()
319 .zip(b.signature.iter())
320 .map(|(x, y)| (x - y) * (x - y))
321 .sum::<f64>()
322 .sqrt()
323}
324
325#[derive(Debug, thiserror::Error)]
327pub enum SignatureError {
328 #[error("insufficient data: got {got} points, need at least {need}")]
330 InsufficientData {
331 got: usize,
333 need: usize,
335 },
336 #[error("zero-dimensional input vectors")]
338 ZeroDimension,
339 #[error("dimension mismatch: expected {expected}, got {got}")]
341 DimensionMismatch {
342 expected: usize,
344 got: usize,
346 },
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn make_trajectory<'a>(points: &'a [&'a [f32]]) -> Vec<(i64, &'a [f32])> {
354 points
355 .iter()
356 .enumerate()
357 .map(|(i, p)| (i as i64, *p))
358 .collect()
359 }
360
361 #[test]
362 fn depth1_is_displacement() {
363 let traj = make_trajectory(&[&[0.0, 0.0], &[1.0, 0.0], &[1.0, 2.0]]);
364 let config = SignatureConfig {
365 depth: 1,
366 time_augmentation: false,
367 };
368 let result = compute_signature(&traj, &config).unwrap();
369
370 assert_eq!(result.output_dim, 2);
371 assert!((result.signature[0] - 1.0).abs() < 1e-10);
373 assert!((result.signature[1] - 2.0).abs() < 1e-10);
374 }
375
376 #[test]
377 fn depth2_captures_rotation() {
378 let traj_a = make_trajectory(&[&[0.0, 0.0], &[1.0, 0.0], &[1.0, 1.0]]);
380 let traj_b = make_trajectory(&[&[0.0, 0.0], &[0.0, 1.0], &[1.0, 1.0]]);
382
383 let config = SignatureConfig {
384 depth: 2,
385 time_augmentation: false,
386 };
387 let sig_a = compute_signature(&traj_a, &config).unwrap();
388 let sig_b = compute_signature(&traj_b, &config).unwrap();
389
390 assert!((sig_a.signature[0] - sig_b.signature[0]).abs() < 1e-10);
392 assert!((sig_a.signature[1] - sig_b.signature[1]).abs() < 1e-10);
393
394 let area_a = sig_a.signature[2 + 1]; let area_b = sig_b.signature[2 + 1]; assert!(
399 (area_a - area_b).abs() > 0.1,
400 "Depth 2 should distinguish rotation: area_a={area_a}, area_b={area_b}"
401 );
402 }
403
404 #[test]
405 fn incremental_matches_full() {
406 let points: Vec<[f32; 3]> = vec![
407 [0.0, 0.0, 0.0],
408 [1.0, 0.5, -0.3],
409 [1.5, 1.0, 0.2],
410 [2.0, 0.8, 0.5],
411 ];
412
413 let config = SignatureConfig {
414 depth: 2,
415 time_augmentation: false,
416 };
417
418 let traj_full: Vec<(i64, &[f32])> = points
420 .iter()
421 .enumerate()
422 .map(|(i, p)| (i as i64, p.as_slice()))
423 .collect();
424 let full = compute_signature(&traj_full, &config).unwrap();
425
426 let traj_3: Vec<(i64, &[f32])> = points[..3]
428 .iter()
429 .enumerate()
430 .map(|(i, p)| (i as i64, p.as_slice()))
431 .collect();
432 let mut incremental = compute_signature(&traj_3, &config).unwrap();
433 update_signature_incremental(&mut incremental, &points[2], &points[3]).unwrap();
434
435 for (i, (a, b)) in full
437 .signature
438 .iter()
439 .zip(incremental.signature.iter())
440 .enumerate()
441 {
442 assert!(
443 (a - b).abs() < 1e-8,
444 "Mismatch at index {i}: full={a}, incremental={b}"
445 );
446 }
447 }
448
449 #[test]
450 fn log_signature_is_smaller() {
451 let traj = make_trajectory(&[&[0.0, 0.0, 0.0], &[1.0, 0.5, -0.3], &[1.5, 1.0, 0.2]]);
452 let config = SignatureConfig {
453 depth: 2,
454 time_augmentation: false,
455 };
456
457 let full = compute_signature(&traj, &config).unwrap();
458 let log = compute_log_signature(&traj, &config).unwrap();
459
460 assert_eq!(full.output_dim, 12);
462 assert_eq!(log.output_dim, 6);
463 }
464
465 #[test]
466 fn signature_distance_works() {
467 let traj_a = make_trajectory(&[&[0.0, 0.0], &[1.0, 0.0], &[1.0, 1.0]]);
468 let traj_b = make_trajectory(&[&[0.0, 0.0], &[0.0, 1.0], &[1.0, 1.0]]);
469 let traj_c = make_trajectory(&[&[0.0, 0.0], &[1.0, 0.0], &[1.0, 1.0]]); let config = SignatureConfig {
472 depth: 2,
473 time_augmentation: false,
474 };
475 let sig_a = compute_signature(&traj_a, &config).unwrap();
476 let sig_b = compute_signature(&traj_b, &config).unwrap();
477 let sig_c = compute_signature(&traj_c, &config).unwrap();
478
479 assert!(signature_distance(&sig_a, &sig_c) < 1e-10); assert!(signature_distance(&sig_a, &sig_b) > 0.1); }
482
483 #[test]
484 fn insufficient_data_error() {
485 let traj = make_trajectory(&[&[1.0, 2.0]]);
486 let config = SignatureConfig::default();
487 assert!(compute_signature(&traj, &config).is_err());
488 }
489}