flow_density/kde/
kde2d.rs1use crate::common::{gaussian_kernel, interquartile_range, standard_deviation};
6use crate::kde::{KdeError, KdeResult};
7use ndarray::Array2;
8use realfft::RealFftPlanner;
9use realfft::num_complex::Complex;
10
11#[derive(Debug)]
13pub struct KernelDensity2D {
14 pub x: Vec<f64>,
16 pub y: Vec<f64>,
18 pub z: Array2<f64>,
20}
21
22impl KernelDensity2D {
23 pub fn estimate(
34 data_x: &[f64],
35 data_y: &[f64],
36 adjust: f64,
37 n_points: usize,
38 ) -> KdeResult<Self> {
39 if data_x.len() != data_y.len() {
40 return Err(KdeError::StatsError(
41 "X and Y data must have the same length".to_string(),
42 ));
43 }
44
45 if data_x.is_empty() {
46 return Err(KdeError::EmptyData);
47 }
48
49 let mut clean_x = Vec::new();
51 let mut clean_y = Vec::new();
52 for i in 0..data_x.len() {
53 if data_x[i].is_finite() && data_y[i].is_finite() {
54 clean_x.push(data_x[i]);
55 clean_y.push(data_y[i]);
56 }
57 }
58
59 if clean_x.len() < 3 {
60 return Err(KdeError::InsufficientData {
61 min: 3,
62 actual: clean_x.len(),
63 });
64 }
65
66 let n = clean_x.len() as f64;
68 let std_dev_x = standard_deviation(&clean_x).map_err(|e| KdeError::StatsError(e))?;
69 let iqr_x = interquartile_range(&clean_x).map_err(|e| KdeError::StatsError(e))?;
70 let bw_factor_x = 0.9 * std_dev_x.min(iqr_x / 1.34) * n.powf(-0.2);
71 let bandwidth_x = bw_factor_x * adjust;
72
73 let std_dev_y = standard_deviation(&clean_y).map_err(|e| KdeError::StatsError(e))?;
74 let iqr_y = interquartile_range(&clean_y).map_err(|e| KdeError::StatsError(e))?;
75 let bw_factor_y = 0.9 * std_dev_y.min(iqr_y / 1.34) * n.powf(-0.2);
76 let bandwidth_y = bw_factor_y * adjust;
77
78 let x_min = clean_x.iter().cloned().fold(f64::INFINITY, f64::min);
80 let x_max = clean_x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
81 let y_min = clean_y.iter().cloned().fold(f64::INFINITY, f64::min);
82 let y_max = clean_y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
83
84 let x_grid_min = x_min - 3.0 * bandwidth_x;
85 let x_grid_max = x_max + 3.0 * bandwidth_x;
86 let y_grid_min = y_min - 3.0 * bandwidth_y;
87 let y_grid_max = y_max + 3.0 * bandwidth_y;
88
89 let x: Vec<f64> = (0..n_points)
90 .map(|i| x_grid_min + (x_grid_max - x_grid_min) * (i as f64) / (n_points - 1) as f64)
91 .collect();
92 let y: Vec<f64> = (0..n_points)
93 .map(|i| y_grid_min + (y_grid_max - y_grid_min) * (i as f64) / (n_points - 1) as f64)
94 .collect();
95
96 let z = kde2d_fft(&clean_x, &clean_y, &x, &y, bandwidth_x, bandwidth_y, n)?;
98
99 Ok(KernelDensity2D { x, y, z })
100 }
101
102 pub fn find_contour(&self, threshold: f64) -> Vec<(f64, f64)> {
110 let max_density = self.z.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
111 let density_threshold = threshold * max_density;
112
113 let mut contour_points = Vec::new();
116
117 for i in 0..self.x.len() {
118 for j in 0..self.y.len() {
119 if self.z[[i, j]] >= density_threshold {
120 let is_boundary = (i > 0 && self.z[[i - 1, j]] < density_threshold)
122 || (i < self.x.len() - 1 && self.z[[i + 1, j]] < density_threshold)
123 || (j > 0 && self.z[[i, j - 1]] < density_threshold)
124 || (j < self.y.len() - 1 && self.z[[i, j + 1]] < density_threshold);
125
126 if is_boundary {
127 contour_points.push((self.x[i], self.y[j]));
128 }
129 }
130 }
131 }
132
133 contour_points
134 }
135
136 pub fn density_at(&self, x: f64, y: f64) -> f64 {
145 let x_idx = self.find_grid_index(&self.x, x);
147 let y_idx = self.find_grid_index(&self.y, y);
148
149 if x_idx >= self.x.len() || y_idx >= self.y.len() {
150 return 0.0;
151 }
152
153 let x0 = if x_idx > 0 { x_idx - 1 } else { 0 };
155 let x1 = x_idx.min(self.x.len() - 1);
156 let y0 = if y_idx > 0 { y_idx - 1 } else { 0 };
157 let y1 = y_idx.min(self.y.len() - 1);
158
159 let z00 = self.z[[x0, y0]];
160 let z01 = self.z[[x0, y1]];
161 let z10 = self.z[[x1, y0]];
162 let z11 = self.z[[x1, y1]];
163
164 let dx = if x1 > x0 {
166 (x - self.x[x0]) / (self.x[x1] - self.x[x0])
167 } else {
168 0.0
169 };
170 let dy = if y1 > y0 {
171 (y - self.y[y0]) / (self.y[y1] - self.y[y0])
172 } else {
173 0.0
174 };
175
176 z00 * (1.0 - dx) * (1.0 - dy)
177 + z10 * dx * (1.0 - dy)
178 + z01 * (1.0 - dx) * dy
179 + z11 * dx * dy
180 }
181
182 fn find_grid_index(&self, grid: &[f64], value: f64) -> usize {
183 if value <= grid[0] {
184 return 0;
185 }
186 if value >= grid[grid.len() - 1] {
187 return grid.len() - 1;
188 }
189
190 let mut left = 0;
192 let mut right = grid.len() - 1;
193 while right - left > 1 {
194 let mid = (left + right) / 2;
195 if grid[mid] <= value {
196 left = mid;
197 } else {
198 right = mid;
199 }
200 }
201 left
202 }
203}
204
205fn kde2d_fft(
209 data_x: &[f64],
210 data_y: &[f64],
211 x_grid: &[f64],
212 y_grid: &[f64],
213 bandwidth_x: f64,
214 bandwidth_y: f64,
215 n: f64,
216) -> KdeResult<Array2<f64>> {
217 let nx = x_grid.len();
218 let ny = y_grid.len();
219
220 if nx < 2 || ny < 2 {
221 return Err(KdeError::StatsError(
222 "Grid must have at least 2 points in each dimension".to_string(),
223 ));
224 }
225
226 let x_spacing = (x_grid[nx - 1] - x_grid[0]) / (nx - 1) as f64;
227 let y_spacing = (y_grid[ny - 1] - y_grid[0]) / (ny - 1) as f64;
228
229 let mut binned = Array2::<f64>::zeros((nx, ny));
231 for (&x, &y) in data_x.iter().zip(data_y.iter()) {
232 let x_idx = ((x - x_grid[0]) / x_spacing).floor() as isize;
233 let y_idx = ((y - y_grid[0]) / y_spacing).floor() as isize;
234 if x_idx >= 0 && (x_idx as usize) < nx && y_idx >= 0 && (y_idx as usize) < ny {
235 binned[[x_idx as usize, y_idx as usize]] += 1.0;
236 }
237 }
238
239 let kernel_center_x = (nx - 1) as f64 / 2.0;
241 let kernel_center_y = (ny - 1) as f64 / 2.0;
242 let mut kernel = Array2::<f64>::zeros((nx, ny));
243
244 for i in 0..nx {
245 for j in 0..ny {
246 let grid_x = (i as f64 - kernel_center_x) * x_spacing;
247 let grid_y = (j as f64 - kernel_center_y) * y_spacing;
248 let u_x = grid_x / bandwidth_x;
249 let u_y = grid_y / bandwidth_y;
250 kernel[[i, j]] = gaussian_kernel(u_x) * gaussian_kernel(u_y);
251 }
252 }
253
254 let _fft_size_x = (2 * nx).next_power_of_two();
257 let _fft_size_y = (2 * ny).next_power_of_two();
258
259 let mut density = Array2::<f64>::zeros((nx, ny));
266
267 for j in 0..ny {
269 let mut row_binned = vec![0.0; nx];
270 let mut row_kernel = vec![0.0; nx];
271 for i in 0..nx {
272 row_binned[i] = binned[[i, j]];
273 row_kernel[i] = kernel[[i, j]];
274 }
275
276 let row_density = kde1d_row(&row_binned, &row_kernel, bandwidth_x, n)?;
278 for i in 0..nx {
279 density[[i, j]] = row_density[i];
280 }
281 }
282
283 for i in 0..nx {
286 let mut col_density = vec![0.0; ny];
287 for j in 0..ny {
288 col_density[j] = density[[i, j]];
289 }
290 for j in 0..ny {
292 let mut sum = 0.0;
293 let mut weight_sum = 0.0;
294 for k in 0..ny {
295 let dist = (j as f64 - k as f64) * y_spacing;
296 let weight = gaussian_kernel(dist / bandwidth_y);
297 sum += col_density[k] * weight;
298 weight_sum += weight;
299 }
300 if weight_sum > 0.0 {
301 density[[i, j]] = sum / weight_sum;
302 }
303 }
304 }
305
306 let total: f64 = density.iter().sum();
308 if total > 0.0 {
309 for val in density.iter_mut() {
310 *val /= total * x_spacing * y_spacing;
311 }
312 }
313
314 Ok(density)
315}
316
317fn kde1d_row(binned: &[f64], kernel: &[f64], bandwidth: f64, n: f64) -> KdeResult<Vec<f64>> {
319 let m = binned.len();
320 let fft_size = (2 * m).next_power_of_two();
321
322 let mut planner = RealFftPlanner::<f64>::new();
323 let r2c = planner.plan_fft_forward(fft_size);
324 let c2r = planner.plan_fft_inverse(fft_size);
325
326 let mut binned_padded = vec![0.0; fft_size];
328 binned_padded[..m].copy_from_slice(binned);
329
330 let mut kernel_padded = vec![0.0; fft_size];
331 let kernel_start = (fft_size - m) / 2;
332 let first_half = (m + 1) / 2;
333 kernel_padded[kernel_start..kernel_start + first_half].copy_from_slice(&kernel[m / 2..]);
334 let second_half = m / 2;
335 if second_half > 0 {
336 kernel_padded[..second_half].copy_from_slice(&kernel[..second_half]);
337 }
338
339 let mut binned_spectrum = r2c.make_output_vec();
341 r2c.process(&mut binned_padded, &mut binned_spectrum)
342 .map_err(|e| KdeError::FftError(format!("FFT forward failed: {}", e)))?;
343
344 let mut kernel_spectrum = r2c.make_output_vec();
345 r2c.process(&mut kernel_padded, &mut kernel_spectrum)
346 .map_err(|e| KdeError::FftError(format!("FFT forward failed: {}", e)))?;
347
348 let mut conv_spectrum: Vec<Complex<f64>> = binned_spectrum
350 .iter()
351 .zip(kernel_spectrum.iter())
352 .map(|(a, b)| a * b)
353 .collect();
354
355 let mut conv_result = c2r.make_output_vec();
357 c2r.process(&mut conv_spectrum, &mut conv_result)
358 .map_err(|e| KdeError::FftError(format!("FFT inverse failed: {}", e)))?;
359
360 let kernel_start = (fft_size - m) / 2;
362 let mut density = Vec::with_capacity(m);
363 for i in 0..m {
364 let idx = (kernel_start + i) % fft_size;
365 density.push(conv_result[idx] / (fft_size as f64 * n * bandwidth));
366 }
367
368 Ok(density)
369}