1use crate::calculus;
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum AnchorMetric {
39 Cosine,
41 L2,
43}
44
45pub fn project_to_anchors(
53 trajectory: &[(i64, &[f32])],
54 anchors: &[&[f32]],
55 metric: AnchorMetric,
56) -> Vec<(i64, Vec<f32>)> {
57 trajectory
58 .iter()
59 .map(|&(ts, vec)| {
60 let distances: Vec<f32> = anchors
61 .iter()
62 .map(|anchor| match metric {
63 AnchorMetric::Cosine => calculus::drift_magnitude_cosine(vec, anchor),
64 AnchorMetric::L2 => calculus::drift_magnitude_l2(vec, anchor),
65 })
66 .collect();
67 (ts, distances)
68 })
69 .collect()
70}
71
72#[derive(Debug, Clone)]
74pub struct AnchorSummary {
75 pub mean: Vec<f32>,
77 pub min: Vec<f32>,
79 pub trend: Vec<f32>,
82 pub last: Vec<f32>,
84}
85
86pub fn anchor_summary(projected: &[(i64, Vec<f32>)]) -> AnchorSummary {
88 if projected.is_empty() {
89 return AnchorSummary {
90 mean: vec![],
91 min: vec![],
92 trend: vec![],
93 last: vec![],
94 };
95 }
96
97 let k = projected[0].1.len();
98 let n = projected.len();
99
100 let mut mean = vec![0.0f32; k];
101 let mut min = vec![f32::INFINITY; k];
102 let mut last = vec![0.0f32; k];
103
104 for (_, dists) in projected {
105 for (j, &d) in dists.iter().enumerate() {
106 mean[j] += d;
107 if d < min[j] {
108 min[j] = d;
109 }
110 }
111 }
112
113 for item in mean.iter_mut().take(k) {
114 *item /= n as f32;
115 }
116
117 if let Some((_, d)) = projected.last() {
118 last = d.clone();
119 }
120
121 let x_mean = (n as f64 - 1.0) / 2.0;
123 let x_var: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
124
125 let trend = if x_var > 0.0 {
126 (0..k)
127 .map(|j| {
128 let y_mean = mean[j] as f64;
129 let covar: f64 = projected
130 .iter()
131 .enumerate()
132 .map(|(i, (_, dists))| (i as f64 - x_mean) * (dists[j] as f64 - y_mean))
133 .sum();
134 (covar / x_var) as f32
135 })
136 .collect()
137 } else {
138 vec![0.0; k]
139 };
140
141 AnchorSummary {
142 mean,
143 min,
144 trend,
145 last,
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn project_cosine_identity() {
155 let traj = vec![(0i64, [1.0f32, 0.0, 0.0].as_slice())];
157 let anchors = vec![[1.0f32, 0.0, 0.0].as_slice()];
158 let result = project_to_anchors(&traj, &anchors, AnchorMetric::Cosine);
159 assert!(result[0].1[0] < 1e-6, "self-distance should be ~0");
160 }
161
162 #[test]
163 fn project_cosine_orthogonal() {
164 let traj = vec![(0i64, [1.0f32, 0.0, 0.0].as_slice())];
165 let anchors = vec![[0.0f32, 1.0, 0.0].as_slice()];
166 let result = project_to_anchors(&traj, &anchors, AnchorMetric::Cosine);
167 assert!(
168 (result[0].1[0] - 1.0).abs() < 1e-6,
169 "orthogonal should be 1.0"
170 );
171 }
172
173 #[test]
174 fn project_multiple_anchors() {
175 let traj = vec![(0i64, [1.0f32, 0.0].as_slice()), (1, [0.0, 1.0].as_slice())];
176 let anchors = vec![[1.0f32, 0.0].as_slice(), [0.0, 1.0].as_slice()];
177 let result = project_to_anchors(&traj, &anchors, AnchorMetric::Cosine);
178 assert!(result[0].1[0] < 0.1);
180 assert!(result[0].1[1] > 0.9);
181 assert!(result[1].1[0] > 0.9);
183 assert!(result[1].1[1] < 0.1);
184 }
185
186 #[test]
187 fn summary_trend_approaching() {
188 let projected = vec![
190 (0i64, vec![1.0f32]),
191 (1, vec![0.8]),
192 (2, vec![0.6]),
193 (3, vec![0.4]),
194 (4, vec![0.2]),
195 ];
196 let summary = anchor_summary(&projected);
197 assert!(
198 summary.trend[0] < 0.0,
199 "should have negative trend (approaching)"
200 );
201 assert!((summary.mean[0] - 0.6).abs() < 0.01);
202 assert!((summary.min[0] - 0.2).abs() < 0.01);
203 }
204
205 #[test]
206 fn summary_empty() {
207 let summary = anchor_summary(&[]);
208 assert!(summary.mean.is_empty());
209 }
210}