1use flow_utils::KernelDensity2D;
6use ndarray::Array2;
7
8#[derive(Clone, Debug)]
10pub struct ContourData {
11 pub contours: Vec<ContourPath>,
13 pub outliers: Vec<(f64, f64)>,
15}
16
17pub type ContourPath = Vec<(f64, f64)>;
19
20pub fn calculate_contours(
30 data: &[(f32, f32)],
31 level_count: u32,
32 smoothing: f64,
33 draw_outliers: bool,
34 _x_range: (f32, f32),
35 _y_range: (f32, f32),
36) -> Result<ContourData, flow_utils::KdeError> {
37 if data.len() < 3 {
38 return Ok(ContourData {
39 contours: Vec::new(),
40 outliers: data
41 .iter()
42 .map(|&(x, y)| (x as f64, y as f64))
43 .collect(),
44 });
45 }
46
47 let data_x: Vec<f64> = data.iter().map(|(x, _)| *x as f64).collect();
48 let data_y: Vec<f64> = data.iter().map(|(_, y)| *y as f64).collect();
49
50 let n_grid = 128usize;
52 let kde = KernelDensity2D::estimate(&data_x, &data_y, smoothing, n_grid)?;
53
54 let max_density = kde
55 .z
56 .iter()
57 .cloned()
58 .fold(f64::NEG_INFINITY, f64::max)
59 .max(1e-300);
60
61 let levels: Vec<f64> = if level_count <= 1 {
64 vec![0.1 * max_density]
65 } else {
66 (1..=level_count as usize)
67 .map(|i| {
68 let t = i as f64 / (level_count as f64 + 1.0);
69 let frac = 0.01 + 0.98 * t;
71 frac * max_density
72 })
73 .collect()
74 };
75
76 let mut contours = Vec::with_capacity(levels.len());
77 for &threshold in &levels {
78 let paths = marching_squares_contours(&kde.x, &kde.y, &kde.z, threshold);
79 contours.extend(paths);
80 }
81
82 let outliers = if draw_outliers {
83 let min_threshold = levels.first().copied().unwrap_or(0.01 * max_density);
84 data.iter()
85 .filter(|&&(x, y)| {
86 let d = kde.density_at(x as f64, y as f64);
87 d < min_threshold
88 })
89 .map(|&(x, y)| (x as f64, y as f64))
90 .collect()
91 } else {
92 Vec::new()
93 };
94
95 Ok(ContourData {
96 contours,
97 outliers,
98 })
99}
100
101fn marching_squares_contours(
105 x: &[f64],
106 y: &[f64],
107 z: &Array2<f64>,
108 threshold: f64,
109) -> Vec<ContourPath> {
110 let nx = x.len();
111 let ny = y.len();
112 if nx < 2 || ny < 2 {
113 return Vec::new();
114 }
115
116 let mut segments = Vec::new();
118
119 for i in 0..nx - 1 {
120 for j in 0..ny - 1 {
121 let v00 = z[[i, j]];
122 let v10 = z[[i + 1, j]];
123 let v01 = z[[i, j + 1]];
124 let v11 = z[[i + 1, j + 1]];
125
126 let c00 = v00 >= threshold;
127 let c10 = v10 >= threshold;
128 let c01 = v01 >= threshold;
129 let c11 = v11 >= threshold;
130
131 let case = (c00 as u8) | ((c10 as u8) << 1) | ((c01 as u8) << 2) | ((c11 as u8) << 3);
132 if case == 0 || case == 15 {
133 continue; }
135
136 let lerp = |a: f64, b: f64, va: f64, vb: f64| -> f64 {
138 if (va >= threshold) == (vb >= threshold) {
139 return a; }
141 let t = (threshold - va) / (vb - va).max(1e-300);
142 a + t * (b - a)
143 };
144
145 let x0 = x[i];
146 let x1 = x[i + 1];
147 let y0 = y[j];
148 let y1 = y[j + 1];
149
150 let bottom = (lerp(x0, x1, v00, v10), y0);
152 let right = (x1, lerp(y0, y1, v10, v11));
153 let top = (lerp(x1, x0, v11, v01), y1);
154 let left = (x0, lerp(y1, y0, v01, v00));
155
156 match case {
157 1 => segments.push((left, bottom)),
158 2 => segments.push((bottom, right)),
159 3 => segments.push((left, right)),
160 4 => segments.push((top, right)),
161 5 => {
162 segments.push((left, bottom));
163 segments.push((top, right));
164 }
165 6 => segments.push((bottom, top)),
166 7 => segments.push((left, top)),
167 8 => segments.push((top, left)),
168 9 => segments.push((bottom, top)),
169 10 => {
170 segments.push((bottom, left));
171 segments.push((top, right));
172 }
173 11 => segments.push((bottom, top)),
174 12 => segments.push((right, left)),
175 13 => segments.push((right, bottom)),
176 14 => segments.push((top, left)),
177 _ => {}
178 }
179 }
180 }
181
182 chain_segments_into_paths(segments)
184}
185
186fn chain_segments_into_paths(segments: Vec<((f64, f64), (f64, f64))>) -> Vec<ContourPath> {
188 const EQ_EPS: f64 = 1e-10;
189
190 fn points_eq(a: (f64, f64), b: (f64, f64)) -> bool {
191 (a.0 - b.0).abs() < EQ_EPS && (a.1 - b.1).abs() < EQ_EPS
192 }
193
194 let mut used = vec![false; segments.len()];
195 let mut paths = Vec::new();
196
197 for start_idx in 0..segments.len() {
198 if used[start_idx] {
199 continue;
200 }
201
202 let (p0, p1) = segments[start_idx];
203 used[start_idx] = true;
204 let mut path = vec![p0, p1];
205
206 loop {
207 let mut found = false;
208 for (idx, &(a, b)) in segments.iter().enumerate() {
209 if used[idx] {
210 continue;
211 }
212 let last = *path.last().unwrap();
213 if points_eq(last, a) {
214 path.push(b);
215 used[idx] = true;
216 found = true;
217 break;
218 } else if points_eq(last, b) {
219 path.push(a);
220 used[idx] = true;
221 found = true;
222 break;
223 }
224 }
225 if !found {
226 break;
227 }
228 if path.len() >= 3 && points_eq(path.first().copied().unwrap(), *path.last().unwrap()) {
230 path.pop(); break;
232 }
233 }
234
235 if path.len() >= 2 {
236 if path.len() >= 3 && points_eq(path[0], path[path.len() - 1]) {
238 path.pop();
239 }
240 paths.push(path);
241 }
242 }
243
244 paths
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_contour_empty_data() {
253 let data: Vec<(f32, f32)> = vec![];
254 let result = calculate_contours(&data, 5, 1.0, false, (0.0, 100.0), (0.0, 100.0));
255 assert!(result.is_ok());
256 let contour_data = result.unwrap();
257 assert!(contour_data.contours.is_empty());
258 assert!(contour_data.outliers.is_empty());
259 }
260
261 #[test]
262 fn test_contour_small_data() {
263 let data = vec![(1.0f32, 2.0f32), (2.0, 3.0), (3.0, 4.0)];
264 let result = calculate_contours(&data, 3, 1.0, false, (0.0, 10.0), (0.0, 10.0));
265 assert!(result.is_ok());
266 }
267
268 #[test]
269 fn test_contour_clustered_data() {
270 let mut data: Vec<(f32, f32)> = Vec::new();
272 for xi in 0..20 {
273 for yi in 0..20 {
274 data.push((100.0 + xi as f32 * 2.0, 100.0 + yi as f32 * 2.0));
275 }
276 }
277 let result = calculate_contours(&data, 5, 0.8, false, (0.0, 200.0), (0.0, 200.0));
278 assert!(result.is_ok());
279 let contour_data = result.unwrap();
280 assert!(
282 !contour_data.contours.is_empty(),
283 "dense cluster should produce at least one contour path"
284 );
285 }
286}