use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::Float;
use crate::error::{SpatialError, SpatialResult};
#[allow(dead_code)]
pub fn morans_i<T: Float>(values: &ArrayView1<T>, weights: &ArrayView2<T>) -> SpatialResult<T> {
let n = values.len();
if weights.shape()[0] != n || weights.shape()[1] != n {
return Err(SpatialError::DimensionError(
"Weights matrix dimensions must match number of values".to_string(),
));
}
let mean = values.sum() / T::from(n).expect("Operation failed");
let deviations: Array1<T> = values.map(|&x| x - mean);
let w_sum = weights.sum();
if w_sum.is_zero() {
return Err(SpatialError::ValueError(
"Sum of weights cannot be zero".to_string(),
));
}
let mut numerator = T::zero();
for i in 0..n {
for j in 0..n {
if i != j {
numerator = numerator + weights[[i, j]] * deviations[i] * deviations[j];
}
}
}
let denominator: T = deviations.map(|&x| x * x).sum();
if denominator.is_zero() {
return Err(SpatialError::ValueError(
"Variance cannot be zero".to_string(),
));
}
let morans_i = (T::from(n).expect("Operation failed") / w_sum) * (numerator / denominator);
Ok(morans_i)
}
#[allow(dead_code)]
pub fn gearys_c<T: Float>(values: &ArrayView1<T>, weights: &ArrayView2<T>) -> SpatialResult<T> {
let n = values.len();
if weights.shape()[0] != n || weights.shape()[1] != n {
return Err(SpatialError::DimensionError(
"Weights matrix dimensions must match number of values".to_string(),
));
}
let mean = values.sum() / T::from(n).expect("Operation failed");
let w_sum = weights.sum();
if w_sum.is_zero() {
return Err(SpatialError::ValueError(
"Sum of weights cannot be zero".to_string(),
));
}
let mut numerator = T::zero();
for i in 0..n {
for j in 0..n {
if i != j {
let diff = values[i] - values[j];
numerator = numerator + weights[[i, j]] * diff * diff;
}
}
}
let variance_sum: T = values
.map(|&x| {
let diff = x - mean;
diff * diff
})
.sum();
if variance_sum.is_zero() {
return Err(SpatialError::ValueError(
"Variance cannot be zero".to_string(),
));
}
let denominator = (T::one() + T::one()) * w_sum * variance_sum;
let gearys_c = (T::from((n - 1) as i32).expect("Operation failed") / denominator) * numerator;
Ok(gearys_c)
}
#[allow(dead_code)]
pub fn local_morans_i<T: Float>(
values: &ArrayView1<T>,
weights: &ArrayView2<T>,
) -> SpatialResult<Array1<T>> {
let n = values.len();
if weights.shape()[0] != n || weights.shape()[1] != n {
return Err(SpatialError::DimensionError(
"Weights matrix dimensions must match number of values".to_string(),
));
}
let mean = values.sum() / T::from(n).expect("Operation failed");
let variance: T = values
.map(|&x| {
let diff = x - mean;
diff * diff
})
.sum()
/ T::from(n).expect("Operation failed");
if variance.is_zero() {
return Err(SpatialError::ValueError(
"Variance cannot be zero".to_string(),
));
}
let mut local_i = Array1::zeros(n);
for i in 0..n {
let zi = (values[i] - mean) / variance.sqrt();
let mut weighted_sum = T::zero();
for j in 0..n {
if i != j && weights[[i, j]] > T::zero() {
let zj = (values[j] - mean) / variance.sqrt();
weighted_sum = weighted_sum + weights[[i, j]] * zj;
}
}
local_i[i] = zi * weighted_sum;
}
Ok(local_i)
}
#[allow(dead_code)]
pub fn getis_ord_gi<T: Float>(
values: &ArrayView1<T>,
weights: &ArrayView2<T>,
include_self: bool,
) -> SpatialResult<Array1<T>> {
let n = values.len();
if weights.shape()[0] != n || weights.shape()[1] != n {
return Err(SpatialError::DimensionError(
"Weights matrix dimensions must match number of values".to_string(),
));
}
let mean = values.sum() / T::from(n).expect("Operation failed");
let variance: T = values
.map(|&x| {
let diff = x - mean;
diff * diff
})
.sum()
/ T::from(n).expect("Operation failed");
if variance.is_zero() {
return Err(SpatialError::ValueError(
"Variance cannot be zero".to_string(),
));
}
let _std_dev = variance.sqrt();
let mut gi_stats = Array1::zeros(n);
for i in 0..n {
let mut weighted_sum = T::zero();
let mut weight_sum = T::zero();
let mut weight_sum_squared = T::zero();
for j in 0..n {
let use_weight = if include_self {
weights[[i, j]]
} else if i == j {
T::zero()
} else {
weights[[i, j]]
};
if use_weight > T::zero() {
weighted_sum = weighted_sum + use_weight * values[j];
weight_sum = weight_sum + use_weight;
weight_sum_squared = weight_sum_squared + use_weight * use_weight;
}
}
if weight_sum > T::zero() {
let n_f = T::from(n).expect("Operation failed");
let expected = weight_sum * mean;
let variance_of_sum =
(n_f * weight_sum_squared - weight_sum * weight_sum) * variance / (n_f - T::one());
if variance_of_sum > T::zero() {
gi_stats[i] = (weighted_sum - expected) / variance_of_sum.sqrt();
}
}
}
Ok(gi_stats)
}
#[allow(dead_code)]
pub fn distance_weights_matrix<T: Float>(
coordinates: &ArrayView2<T>,
max_distance: T,
decay_function: &str,
bandwidth: T,
) -> SpatialResult<Array2<T>> {
let n = coordinates.shape()[0];
if coordinates.shape()[1] != 2 {
return Err(SpatialError::DimensionError(
"Coordinates must be 2D (x, y)".to_string(),
));
}
let mut weights = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
if i != j {
let dx = coordinates[[i, 0]] - coordinates[[j, 0]];
let dy = coordinates[[i, 1]] - coordinates[[j, 1]];
let _distance = (dx * dx + dy * dy).sqrt();
if _distance <= max_distance {
let weight = match decay_function {
"inverse" => {
if _distance > T::zero() {
T::one() / (T::one() + _distance / bandwidth)
} else {
T::zero()
}
}
"exponential" => (-_distance / bandwidth).exp(),
"gaussian" => {
let exponent = -(_distance * _distance) / (bandwidth * bandwidth);
exponent.exp()
}
_ => {
return Err(SpatialError::ValueError(
"Unknown decay function. Use 'inverse', 'exponential', or 'gaussian'".to_string(),
));
}
};
weights[[i, j]] = weight;
}
}
}
}
Ok(weights)
}
#[allow(dead_code)]
pub fn clark_evans_index<T: Float>(coordinates: &ArrayView2<T>, study_area: T) -> SpatialResult<T> {
let n = coordinates.shape()[0];
if coordinates.shape()[1] != 2 {
return Err(SpatialError::DimensionError(
"Coordinates must be 2D (x, y)".to_string(),
));
}
if n < 2 {
return Err(SpatialError::ValueError(
"Need at least 2 points to calculate nearest neighbor distances".to_string(),
));
}
let mut nn_distances = Vec::with_capacity(n);
for i in 0..n {
let mut min_distance = T::infinity();
for j in 0..n {
if i != j {
let dx = coordinates[[i, 0]] - coordinates[[j, 0]];
let dy = coordinates[[i, 1]] - coordinates[[j, 1]];
let distance = (dx * dx + dy * dy).sqrt();
if distance < min_distance {
min_distance = distance;
}
}
}
nn_distances.push(min_distance);
}
let observed_mean = nn_distances.iter().fold(T::zero(), |acc, &d| acc + d)
/ T::from(n).expect("Operation failed");
let density = T::from(n).expect("Operation failed") / study_area;
let expected_mean = T::one() / (T::from(2.0).expect("Operation failed") * density.sqrt());
let clark_evans = observed_mean / expected_mean;
Ok(clark_evans)
}
pub fn contiguity_weights_matrix(
polygons: &[Vec<usize>],
n: usize,
min_shared_vertices: usize,
) -> SpatialResult<Array2<f64>> {
if polygons.len() != n {
return Err(SpatialError::DimensionError(format!(
"Number of polygons ({}) must match n ({})",
polygons.len(),
n
)));
}
if min_shared_vertices == 0 {
return Err(SpatialError::ValueError(
"min_shared_vertices must be at least 1".to_string(),
));
}
let mut weights = Array2::zeros((n, n));
for i in 0..n {
for j in (i + 1)..n {
let shared = polygons[i]
.iter()
.filter(|v| polygons[j].contains(v))
.count();
if shared >= min_shared_vertices {
weights[[i, j]] = 1.0;
weights[[j, i]] = 1.0;
}
}
}
Ok(weights)
}
pub fn knn_weights_matrix(coordinates: &ArrayView2<f64>, k: usize) -> SpatialResult<Array2<f64>> {
let n = coordinates.shape()[0];
let ndim = coordinates.shape()[1];
if k == 0 {
return Err(SpatialError::ValueError("k must be at least 1".to_string()));
}
if k >= n {
return Err(SpatialError::ValueError(format!(
"k ({}) must be less than number of points ({})",
k, n
)));
}
let mut weights = Array2::zeros((n, n));
for i in 0..n {
let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n - 1);
for j in 0..n {
if i != j {
let mut dist_sq = 0.0;
for d in 0..ndim {
let diff = coordinates[[i, d]] - coordinates[[j, d]];
dist_sq += diff * diff;
}
distances.push((j, dist_sq));
}
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
for &(j, _) in distances.iter().take(k) {
weights[[i, j]] = 1.0;
}
}
Ok(weights)
}
pub fn ripleys_k(
coordinates: &ArrayView2<f64>,
study_area: f64,
distances: &ArrayView1<f64>,
) -> SpatialResult<Array1<f64>> {
let n = coordinates.shape()[0];
if coordinates.shape()[1] != 2 {
return Err(SpatialError::DimensionError(
"Coordinates must be 2D (x, y)".to_string(),
));
}
if n < 2 {
return Err(SpatialError::ValueError(
"Need at least 2 points for Ripley's K".to_string(),
));
}
if study_area <= 0.0 {
return Err(SpatialError::ValueError(
"Study area must be positive".to_string(),
));
}
let n_dists = distances.len();
let mut k_values = Array1::zeros(n_dists);
let n_f = n as f64;
let intensity = n_f / study_area;
let mut pairwise_dists: Vec<f64> = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n {
for j in (i + 1)..n {
let dx = coordinates[[i, 0]] - coordinates[[j, 0]];
let dy = coordinates[[i, 1]] - coordinates[[j, 1]];
pairwise_dists.push((dx * dx + dy * dy).sqrt());
}
}
for (t_idx, &t) in distances.iter().enumerate() {
let count: usize = pairwise_dists.iter().filter(|&&d| d <= t).count();
k_values[t_idx] = (study_area / (n_f * n_f)) * (2.0 * count as f64);
}
Ok(k_values)
}
pub fn ripleys_l(
coordinates: &ArrayView2<f64>,
study_area: f64,
distances: &ArrayView1<f64>,
) -> SpatialResult<Array1<f64>> {
let k_values = ripleys_k(coordinates, study_area, distances)?;
let l_values = k_values.mapv(|k| {
if k >= 0.0 {
(k / std::f64::consts::PI).sqrt()
} else {
0.0
}
});
Ok(l_values)
}
pub fn average_nearest_neighbor(
coordinates: &ArrayView2<f64>,
study_area: f64,
) -> SpatialResult<AnnResult> {
let n = coordinates.shape()[0];
let ndim = coordinates.shape()[1];
if n < 2 {
return Err(SpatialError::ValueError(
"Need at least 2 points for average nearest neighbor".to_string(),
));
}
if study_area <= 0.0 {
return Err(SpatialError::ValueError(
"Study area must be positive".to_string(),
));
}
let mut nn_distances = Vec::with_capacity(n);
for i in 0..n {
let mut min_distance = f64::INFINITY;
for j in 0..n {
if i != j {
let mut dist_sq = 0.0;
for d in 0..ndim {
let diff = coordinates[[i, d]] - coordinates[[j, d]];
dist_sq += diff * diff;
}
let dist = dist_sq.sqrt();
if dist < min_distance {
min_distance = dist;
}
}
}
nn_distances.push(min_distance);
}
let observed_mean: f64 = nn_distances.iter().sum::<f64>() / n as f64;
let density = n as f64 / study_area;
let expected_mean = 1.0 / (2.0 * density.sqrt());
let se = 0.26136 / (n as f64 * density).sqrt();
let z_score = if se > 0.0 {
(observed_mean - expected_mean) / se
} else {
0.0
};
let r_index = if expected_mean > 0.0 {
observed_mean / expected_mean
} else {
0.0
};
Ok(AnnResult {
observed_mean,
expected_mean,
z_score,
r_index,
nn_distances,
})
}
#[derive(Debug, Clone)]
pub struct AnnResult {
pub observed_mean: f64,
pub expected_mean: f64,
pub z_score: f64,
pub r_index: f64,
pub nn_distances: Vec<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_morans_i() {
let values = array![1.0, 1.0, 3.0, 3.0, 3.0];
let weights = array![
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
];
let moran = morans_i(&values.view(), &weights.view()).expect("Operation failed");
assert!(moran > 0.0);
}
#[test]
fn test_gearys_c() {
let values = array![1.0, 1.0, 3.0, 3.0, 3.0];
let weights = array![
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
];
let geary = gearys_c(&values.view(), &weights.view()).expect("Operation failed");
assert!(geary < 1.0);
}
#[test]
fn test_local_morans_i() {
let values = array![1.0, 1.0, 3.0, 3.0, 3.0];
let weights = array![
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
];
let local_i = local_morans_i(&values.view(), &weights.view()).expect("Operation failed");
assert_eq!(local_i.len(), 5);
}
#[test]
fn test_distance_weights_matrix() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [2.0, 2.0],];
let weights =
distance_weights_matrix(&coords.view(), 1.5, "inverse", 1.0).expect("Operation failed");
assert_eq!(weights.shape(), &[4, 4]);
for i in 0..4 {
assert_relative_eq!(weights[[i, i]], 0.0, epsilon = 1e-10);
}
assert!(weights[[0, 1]] > 0.0);
assert!(weights[[1, 0]] > 0.0);
assert_relative_eq!(weights[[0, 3]], 0.0, epsilon = 1e-10);
}
#[test]
fn test_clark_evans_index() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0],];
let ce_index = clark_evans_index(&coords.view(), 4.0).expect("Operation failed");
assert!(ce_index > 1.0);
}
#[test]
fn test_contiguity_weights_matrix_queen() {
let polygons = vec![
vec![0, 1, 2, 3],
vec![2, 3, 4, 5],
vec![3, 5, 6, 7],
vec![8, 9, 10, 11],
];
let weights = contiguity_weights_matrix(&polygons, 4, 1).expect("Operation failed");
assert_eq!(weights.shape(), &[4, 4]);
assert_relative_eq!(weights[[0, 1]], 1.0);
assert_relative_eq!(weights[[1, 0]], 1.0);
assert_relative_eq!(weights[[0, 2]], 1.0);
assert_relative_eq!(weights[[1, 2]], 1.0);
assert_relative_eq!(weights[[0, 3]], 0.0);
assert_relative_eq!(weights[[1, 3]], 0.0);
assert_relative_eq!(weights[[2, 3]], 0.0);
}
#[test]
fn test_contiguity_weights_matrix_rook() {
let polygons = vec![
vec![0, 1, 2, 3],
vec![2, 3, 4, 5],
vec![3, 5, 6, 7], vec![8, 9, 10, 11],
];
let weights = contiguity_weights_matrix(&polygons, 4, 2).expect("Operation failed");
assert_relative_eq!(weights[[0, 1]], 1.0);
assert_relative_eq!(weights[[0, 2]], 0.0);
assert_relative_eq!(weights[[1, 2]], 1.0);
}
#[test]
fn test_knn_weights_matrix() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [5.0, 5.0],];
let weights = knn_weights_matrix(&coords.view(), 2).expect("Operation failed");
assert_eq!(weights.shape(), &[5, 5]);
for i in 0..5 {
let row_sum: f64 = (0..5).map(|j| weights[[i, j]]).sum();
assert_relative_eq!(row_sum, 2.0, epsilon = 1e-10);
}
for i in 0..5 {
assert_relative_eq!(weights[[i, i]], 0.0);
}
assert_relative_eq!(weights[[0, 1]], 1.0);
assert_relative_eq!(weights[[0, 2]], 1.0);
}
#[test]
fn test_knn_weights_errors() {
let coords = array![[0.0, 0.0], [1.0, 0.0]];
assert!(knn_weights_matrix(&coords.view(), 0).is_err());
assert!(knn_weights_matrix(&coords.view(), 2).is_err());
}
#[test]
fn test_ripleys_k() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5],];
let distances = array![0.5, 1.0, 1.5, 2.0];
let k_values = ripleys_k(&coords.view(), 4.0, &distances.view()).expect("Operation failed");
assert_eq!(k_values.len(), 4);
for i in 1..k_values.len() {
assert!(
k_values[i] >= k_values[i - 1],
"K should be non-decreasing: K[{}] = {} < K[{}] = {}",
i,
k_values[i],
i - 1,
k_values[i - 1]
);
}
assert!(k_values[3] > k_values[0]);
}
#[test]
fn test_ripleys_l() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5],];
let distances = array![0.5, 1.0, 1.5];
let l_values = ripleys_l(&coords.view(), 4.0, &distances.view()).expect("Operation failed");
assert_eq!(l_values.len(), 3);
for &l in l_values.iter() {
assert!(l >= 0.0);
}
}
#[test]
fn test_ripleys_k_errors() {
let coords = array![[0.0, 0.0]]; let distances = array![1.0];
assert!(ripleys_k(&coords.view(), 1.0, &distances.view()).is_err());
let coords2 = array![[0.0, 0.0], [1.0, 1.0]];
assert!(ripleys_k(&coords2.view(), -1.0, &distances.view()).is_err());
}
#[test]
fn test_average_nearest_neighbor() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0],];
let result = average_nearest_neighbor(&coords.view(), 4.0).expect("Operation failed");
assert!(result.observed_mean > 0.0);
assert!(result.expected_mean > 0.0);
assert_eq!(result.nn_distances.len(), 4);
for &d in &result.nn_distances {
assert_relative_eq!(d, 1.0, epsilon = 1e-10);
}
assert!(result.r_index > 1.0);
}
#[test]
fn test_average_nearest_neighbor_clustered() {
let coords = array![[0.0, 0.0], [0.01, 0.0], [0.0, 0.01], [0.01, 0.01],];
let result = average_nearest_neighbor(&coords.view(), 100.0).expect("Operation failed");
assert!(
result.r_index < 1.0,
"Expected R < 1 for clustered pattern, got {}",
result.r_index
);
}
#[test]
fn test_average_nearest_neighbor_errors() {
let coords = array![[0.0, 0.0]];
assert!(average_nearest_neighbor(&coords.view(), 1.0).is_err());
let coords2 = array![[0.0, 0.0], [1.0, 1.0]];
assert!(average_nearest_neighbor(&coords2.view(), -1.0).is_err());
}
}