use crate::GeostatError;
use serde::{Deserialize, Serialize};
use rayon::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KFunctionResult {
pub distances: Vec<f64>,
pub k_values: Vec<f64>,
pub l_values: Vec<f64>,
pub n_points: usize,
pub area: f64,
pub intensity: f64,
}
#[derive(Debug)]
pub struct KFunction {
points: Vec<(f64, f64)>,
bounds: (f64, f64, f64, f64), }
impl KFunction {
pub fn new(points: Vec<(f64, f64)>) -> Result<Self, GeostatError> {
if points.len() < 3 {
return Err(GeostatError::InsufficientData(
"at least 3 points required for K function".to_string(),
));
}
let min_x = points.iter().map(|(x, _)| x).copied().fold(f64::INFINITY, f64::min);
let max_x = points.iter().map(|(x, _)| x).copied().fold(f64::NEG_INFINITY, f64::max);
let min_y = points.iter().map(|(_, y)| y).copied().fold(f64::INFINITY, f64::min);
let max_y = points.iter().map(|(_, y)| y).copied().fold(f64::NEG_INFINITY, f64::max);
Ok(KFunction {
points,
bounds: (min_x, min_y, max_x, max_y),
})
}
pub fn compute(&self, distances: &[f64]) -> Result<KFunctionResult, GeostatError> {
if distances.is_empty() {
return Err(GeostatError::InvalidParameters("no distances specified".to_string()));
}
let n = self.points.len() as f64;
let area = (self.bounds.2 - self.bounds.0) * (self.bounds.3 - self.bounds.1);
let intensity = n / area;
let k_values: Vec<f64> = distances
.par_iter()
.map(|&t| {
let mut count = 0.0;
for i in 0..self.points.len() {
for j in 0..self.points.len() {
if i != j {
let dx = self.points[i].0 - self.points[j].0;
let dy = self.points[i].1 - self.points[j].1;
let dist = (dx * dx + dy * dy).sqrt();
if dist <= t {
count += 1.0;
}
}
}
}
(area / (n * n)) * count
})
.collect();
let l_values: Vec<f64> = distances
.iter()
.zip(k_values.iter())
.map(|(t, k)| {
let k_norm = k / std::f64::consts::PI;
k_norm.sqrt() - t
})
.collect();
Ok(KFunctionResult {
distances: distances.to_vec(),
k_values,
l_values,
n_points: self.points.len(),
area,
intensity,
})
}
pub fn recommended_max_distance(&self) -> f64 {
let width = self.bounds.2 - self.bounds.0;
let height = self.bounds.3 - self.bounds.1;
width.min(height) / 4.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_k_function_csr_homogeneous() {
use rand::Rng;
let mut rng = rand::thread_rng();
let points: Vec<(f64, f64)> = (0..100)
.map(|_| (rng.gen_range(0.0..1.0), rng.gen_range(0.0..1.0)))
.collect();
let kf = KFunction::new(points).unwrap();
let distances = vec![0.05, 0.1, 0.15, 0.2];
let result = kf.compute(&distances).unwrap();
for l in &result.l_values {
assert!(l.abs() < 0.1); }
}
#[test]
fn test_k_function_clustered() {
let mut points = vec![];
for cx in &[0.25, 0.75] {
for cy in &[0.25, 0.75] {
for _ in 0..10 {
points.push((cx + 0.02, cy + 0.02));
}
}
}
let kf = KFunction::new(points).unwrap();
let distances = vec![0.05, 0.1, 0.15];
let result = kf.compute(&distances).unwrap();
assert!(result.l_values[0] > 0.0);
}
#[test]
fn test_k_function_minimum_points() {
let result = KFunction::new(vec![(0.0, 0.0), (1.0, 1.0)]);
assert!(result.is_err()); }
#[test]
fn test_k_function_computation() {
let points = vec![
(0.0, 0.0),
(1.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
];
let kf = KFunction::new(points).unwrap();
let distances = vec![0.5, 1.0, 1.5];
let result = kf.compute(&distances).unwrap();
assert_eq!(result.n_points, 4);
assert_eq!(result.distances.len(), 3);
assert_eq!(result.k_values.len(), 3);
assert_eq!(result.l_values.len(), 3);
for i in 0..2 {
assert!(result.k_values[i] <= result.k_values[i + 1]);
}
}
}