use core::f64::consts::PI;
use ndarray::Array2;
const KDE_EPSILON: f64 = 2.220446049250313e-16;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GridDimensions {
pub width: usize,
pub height: usize,
}
pub struct KDEGrid {
width: usize,
height: usize,
bin_width: usize,
bandwidth: f64,
data: Array2<f64>,
kde_matrix: Array2<f64>,
}
#[allow(unused)]
pub struct KDEModel {
pub width: usize,
pub height: usize,
bin_width: usize,
bandwidth: f64,
pub data: Array2<f64>,
}
impl KDEModel {
#[allow(unused)]
pub fn sum(&self) -> f64 {
self.data.sum()
}
#[inline(always)]
pub fn try_query_bin(&self, query: (i64, i64)) -> Option<f64> {
let (bins_x, bins_y) = self.data.dim();
let (x, y) = query;
if x >= 0 && y >= 0 && (x as usize) < bins_x && (y as usize) < bins_y {
Some(self.data[[x as usize, y as usize]])
} else {
None
}
}
}
impl std::ops::Index<(usize, usize)> for KDEModel {
type Output = f64;
#[inline(always)]
fn index(&self, index: (usize, usize)) -> &Self::Output {
let blx = index.0 / self.bin_width;
let bly = index.1 / self.bin_width;
let (bins_x, bins_y) = self.data.dim();
if blx >= bins_x || bly >= bins_y {
&KDE_EPSILON
} else {
&self.data[[blx, bly]]
}
}
}
impl KDEModel {
#[allow(dead_code)]
#[inline(always)]
pub fn query_interp(&self, index: (usize, usize)) -> f64 {
let blx = index.0 / self.bin_width;
let bly = index.1 / self.bin_width;
let (bins_x, bins_y) = self.data.dim();
if blx >= bins_x || bly >= bins_y {
KDE_EPSILON
} else {
let bandwidth = self.bandwidth;
let bw_sq = bandwidth * bandwidth;
let bwf = self.bin_width as f64;
let half_bin_width = (bwf) / 2.;
let kernel_norm = 1.0 / (2.0 * PI * bw_sq);
let sq_distance = |x: f64, y: f64, nx: i64, ny: i64| {
let px = (nx as f64 * bwf) + half_bin_width;
let py = (ny as f64 * bwf) + half_bin_width;
let dx = x - px;
let dy = y - py;
dx * dx + dy * dy
};
let blx = blx as i64;
let bly = bly as i64;
let left = blx - 1;
let right = blx + 1;
let down = bly - 1;
let up = bly + 1;
let x = index.0 as f64;
let y = index.1 as f64;
let mut weighted_density = 0_f64;
let mut denom = 0_f64;
for (nx, ny) in [
(blx, bly),
(left, bly),
(right, bly),
(blx, down),
(blx, up),
(left, down),
(right, down),
(left, up),
(right, up),
] {
if let Some(sample_d) = self.try_query_bin((nx, ny)) {
let weight = ((sq_distance(x, y, nx, ny) / bw_sq) * 0.5).exp() * kernel_norm;
denom += weight;
weighted_density += sample_d * weight;
}
}
if denom > 0. {
weighted_density / denom
} else {
KDE_EPSILON
}
}
}
}
impl KDEGrid {
pub fn new(grid_dim: GridDimensions, bin_width: usize, bandwidth: Option<f64>) -> Self {
let calc_num_bins = |extent: usize, bw: usize| -> usize {
if extent % bw == 0 {
extent / bw
} else {
(extent / bw) + 1
}
};
let num_x_bins = calc_num_bins(grid_dim.width, bin_width) + 1;
let num_y_bins = calc_num_bins(grid_dim.height, bin_width) + 1;
KDEGrid {
width: grid_dim.width,
height: grid_dim.height,
bin_width,
bandwidth: bandwidth.unwrap_or(1.0),
data: Array2::zeros((num_x_bins, num_y_bins)),
kde_matrix: Array2::zeros((num_x_bins, num_y_bins)),
}
}
#[allow(unused)]
pub fn from_data_with_binwidth(
data: &[u64],
weights: &[f64],
grid_dim: Option<GridDimensions>,
bin_width: usize,
bandwidth: Option<f64>,
) -> Self {
let n = data.len() / 2;
let data = Array2::from_shape_vec(((data.len() / 2), 2), data.to_vec()).unwrap();
let (mut max_x, mut max_y) = (0_u64, 0_u64);
for i in 0..n {
max_x = data[[i, 0]].max(max_x);
max_y = data[[i, 1]].max(max_y);
}
let gd = match grid_dim {
Some(GridDimensions { width, height }) => grid_dim.unwrap(),
None => GridDimensions {
width: (max_x + 1) as usize,
height: (max_y + 1) as usize,
},
};
let mut grid = Self::new(gd, bin_width, bandwidth);
for i in 0..n {
grid.add_observation(data[[i, 0]] as usize, data[[i, 1]] as usize, weights[i]);
}
grid
}
#[inline(always)]
pub fn add_observation(&mut self, x: usize, y: usize, w: f64) {
let bx = x as i64 / self.bin_width as i64;
let by = y as i64 / self.bin_width as i64;
let (x_cells, y_cells) = self.data.dim();
let x_cells = x_cells as i64;
let y_cells = y_cells as i64;
if bx >= x_cells || by >= y_cells {
return;
}
let bandwidth = self.bandwidth;
let bwf = self.bin_width as f64;
let half_bin_width = bwf / 2.0;
let dist_thresh = 10. * bandwidth; let bw_sq = bandwidth * bandwidth;
let kernel_norm = 1.0 / (2.0 * PI * bandwidth.powi(2));
let half_width = (dist_thresh / (self.bin_width as f64) + 1.) as i64;
for i1 in (0.max(bx - half_width))..(x_cells.min(bx + half_width)) {
let k1 = bwf * i1 as f64 + half_bin_width;
for j1 in (0.max(by - half_width))..(y_cells.min(by + half_width)) {
let k2 = bwf * j1 as f64 + half_bin_width;
let dx = k1 - x as f64;
let dy = k2 - y as f64;
let distance_sq = dx * dx + dy * dy;
let distance = distance_sq.sqrt();
let dweight = w;
let py_dist_sq = distance_sq / bw_sq;
if distance <= dist_thresh {
let contrib = dweight * (-py_dist_sq / 2.0).exp() * kernel_norm;
self.kde_matrix[[i1 as usize, j1 as usize]] += contrib;
}
}
}
}
pub fn get_kde(&mut self) -> anyhow::Result<KDEModel> {
let mut new_kde_matrix = self.kde_matrix.clone() + KDE_EPSILON;
new_kde_matrix /= new_kde_matrix.sum();
Ok(KDEModel {
width: self.width,
height: self.height,
bin_width: self.bin_width,
bandwidth: self.bandwidth,
data: new_kde_matrix,
})
}
}