geographdb_core/algorithms/
delay_embed.rs1pub fn delay_embedding(series: &[Vec<f32>], tau: usize, embed_dim: usize) -> Vec<Vec<f32>> {
29 let n = series.len();
30 if n == 0 || embed_dim == 0 {
31 return Vec::new();
32 }
33 let n_features = series[0].len();
34 let out_dim = n_features * embed_dim;
35
36 (0..n)
37 .map(|t| {
38 let mut row = vec![0.0f32; out_dim];
39 for lag in 0..embed_dim {
40 let src_t = (t as isize) - (lag * tau) as isize;
41 if src_t >= 0 {
42 let src = &series[src_t as usize];
43 let offset = lag * n_features;
44 row[offset..offset + n_features].copy_from_slice(src);
45 }
46 }
48 row
49 })
50 .collect()
51}
52
53pub fn correlation_dimension(points: &[Vec<f32>], r_min: f32, r_max: f32, n_steps: usize) -> f64 {
65 let n = points.len();
66 if n < 4 || r_min <= 0.0 || r_max <= r_min || n_steps < 2 {
67 return 0.0;
68 }
69
70 let log_rmin = (r_min as f64).ln();
72 let log_rmax = (r_max as f64).ln();
73 let radii: Vec<f64> = (0..n_steps)
74 .map(|i| {
75 let t = i as f64 / (n_steps - 1) as f64;
76 (log_rmin + t * (log_rmax - log_rmin)).exp()
77 })
78 .collect();
79
80 let total_pairs = (n * (n - 1) / 2) as f64;
82 let mut log_r_fit: Vec<f64> = Vec::with_capacity(n_steps);
83 let mut log_c_fit: Vec<f64> = Vec::with_capacity(n_steps);
84
85 for &r in &radii {
86 let mut count = 0usize;
87 for i in 0..n {
88 for j in (i + 1)..n {
89 if euclidean_dist_sq(&points[i], &points[j]) < (r * r) as f32 {
90 count += 1;
91 }
92 }
93 }
94 if count < 2 {
97 continue;
98 }
99 let c = count as f64 / total_pairs;
100 log_r_fit.push(r.ln());
101 log_c_fit.push(c.ln());
102 }
103
104 let m = log_r_fit.len();
106 if m < 2 {
107 return 0.0;
108 }
109
110 let mean_r = log_r_fit.iter().sum::<f64>() / m as f64;
112 let mean_c = log_c_fit.iter().sum::<f64>() / m as f64;
113 let num: f64 = log_r_fit
114 .iter()
115 .zip(log_c_fit.iter())
116 .map(|(r, c)| (r - mean_r) * (c - mean_c))
117 .sum();
118 let den: f64 = log_r_fit.iter().map(|r| (r - mean_r).powi(2)).sum();
119 if den.abs() < 1e-15 {
120 return 0.0;
121 }
122 (num / den).max(0.0)
123}
124
125pub fn min_cache_size_takens(dim_estimate: f64) -> usize {
128 2 * dim_estimate.ceil() as usize + 1
129}
130
131fn euclidean_dist_sq(a: &[f32], b: &[f32]) -> f32 {
134 a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
135}
136
137#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
146 fn test_delay_embedding_shape() {
147 let series: Vec<Vec<f32>> = (0..10).map(|i| vec![i as f32, i as f32 + 1.0]).collect();
148 let embedded = delay_embedding(&series, 1, 3);
149 assert_eq!(embedded.len(), 10);
150 assert_eq!(embedded[0].len(), 6); }
152
153 #[test]
154 fn test_delay_embedding_tau1_lag0_matches_original() {
155 let series: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32]).collect();
156 let embedded = delay_embedding(&series, 1, 2);
157 for (t, row) in embedded.iter().enumerate() {
159 assert_eq!(row[0], t as f32, "lag-0 mismatch at t={t}");
160 }
161 }
162
163 #[test]
164 fn test_delay_embedding_zero_pads_before_start() {
165 let series: Vec<Vec<f32>> = (0..4).map(|i| vec![i as f32]).collect();
166 let embedded = delay_embedding(&series, 1, 3);
167 assert_eq!(embedded[0], vec![0.0, 0.0, 0.0]);
169 assert_eq!(embedded[1], vec![1.0, 0.0, 0.0]);
171 assert_eq!(embedded[2], vec![2.0, 1.0, 0.0]);
173 }
174
175 #[test]
176 fn test_delay_embedding_empty_series() {
177 let embedded = delay_embedding(&[], 1, 3);
178 assert!(embedded.is_empty());
179 }
180
181 #[test]
185 fn test_correlation_dim_line() {
186 let points: Vec<Vec<f32>> = (0..50).map(|i| vec![i as f32 / 49.0]).collect();
188 let d = correlation_dimension(&points, 0.01, 1.0, 20);
189 assert!(
190 (d - 1.0).abs() < 0.3,
191 "line should have corr dim ≈ 1.0, got {d:.3}"
192 );
193 }
194
195 #[test]
197 fn test_correlation_dim_circle() {
198 let points: Vec<Vec<f32>> = (0..60)
199 .map(|i| {
200 let theta = 2.0 * std::f32::consts::PI * i as f32 / 60.0;
201 vec![theta.cos(), theta.sin()]
202 })
203 .collect();
204 let d = correlation_dimension(&points, 0.05, 1.5, 20);
205 assert!(
206 (d - 1.0).abs() < 0.4,
207 "circle should have corr dim ≈ 1.0, got {d:.3}"
208 );
209 }
210
211 #[test]
213 fn test_correlation_dim_plane() {
214 let points: Vec<Vec<f32>> = (0..10)
216 .flat_map(|i| (0..10).map(move |j| vec![i as f32 / 9.0, j as f32 / 9.0]))
217 .collect();
218 let d = correlation_dimension(&points, 0.05, 1.0, 20);
219 assert!(
220 (d - 2.0).abs() < 0.5,
221 "plane should have corr dim ≈ 2.0, got {d:.3}"
222 );
223 }
224
225 #[test]
227 fn test_correlation_dim_degenerate() {
228 let d = correlation_dimension(&[vec![1.0f32], vec![2.0f32]], 0.1, 1.0, 10);
229 assert_eq!(d, 0.0);
230 }
231
232 #[test]
235 fn test_min_cache_size_formula() {
236 assert_eq!(min_cache_size_takens(1.0), 3); assert_eq!(min_cache_size_takens(2.0), 5); assert_eq!(min_cache_size_takens(64.0), 129); assert_eq!(min_cache_size_takens(1.5), 5); }
241
242 #[test]
243 fn test_min_cache_size_fractional_rounds_up() {
244 assert_eq!(min_cache_size_takens(0.7), 3);
246 assert_eq!(min_cache_size_takens(2.1), 7);
248 }
249}