use crate::error::{AlgorithmError, Result};
use oxigdal_core::buffer::RasterBuffer;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, PartialEq)]
pub struct RasterStatistics {
pub count: usize,
pub min: f64,
pub max: f64,
pub mean: f64,
pub median: f64,
pub stddev: f64,
pub variance: f64,
pub sum: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Percentiles {
pub p10: f64,
pub p25: f64,
pub p50: f64,
pub p75: f64,
pub p90: f64,
}
#[derive(Debug, Clone)]
pub struct Histogram {
pub edges: Vec<f64>,
pub counts: Vec<usize>,
pub total: usize,
}
impl Histogram {
fn find_bin(&self, value: f64) -> Option<usize> {
if value < self.edges[0] || value > *self.edges.last()? {
return None;
}
for i in 0..self.counts.len() {
if value >= self.edges[i] && value < self.edges[i + 1] {
return Some(i);
}
}
if (value - self.edges[self.counts.len()]).abs() < f64::EPSILON {
return Some(self.counts.len() - 1);
}
None
}
#[must_use]
pub fn frequencies(&self) -> Vec<f64> {
if self.total == 0 {
return vec![0.0; self.counts.len()];
}
self.counts
.iter()
.map(|&count| count as f64 / self.total as f64)
.collect()
}
}
#[cfg(feature = "parallel")]
pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
compute_statistics_parallel(raster)
}
#[cfg(not(feature = "parallel"))]
pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
compute_statistics_sequential(raster)
}
#[cfg(feature = "parallel")]
fn compute_statistics_parallel(raster: &RasterBuffer) -> Result<RasterStatistics> {
let (count, sum, sum_sq, min, max, median_samples) = (0..raster.height())
.into_par_iter()
.map(|y| {
let mut row_count = 0usize;
let mut row_sum = 0.0f64;
let mut row_sum_sq = 0.0f64;
let mut row_min = f64::INFINITY;
let mut row_max = f64::NEG_INFINITY;
let mut row_samples = Vec::new();
for x in 0..raster.width() {
if let Ok(val) = raster.get_pixel(x, y) {
if !raster.is_nodata(val) && val.is_finite() {
row_count += 1;
row_sum += val;
row_sum_sq += val * val;
row_min = row_min.min(val);
row_max = row_max.max(val);
if row_samples.len() < 10000 {
row_samples.push(val);
} else {
let idx = ((row_count.wrapping_mul(1103515245).wrapping_add(12345))
>> 16)
% row_count;
if idx < 10000 {
row_samples[idx] = val;
}
}
}
}
}
(
row_count,
row_sum,
row_sum_sq,
row_min,
row_max,
row_samples,
)
})
.reduce(
|| (0, 0.0, 0.0, f64::INFINITY, f64::NEG_INFINITY, Vec::new()),
|(c1, s1, sq1, min1, max1, mut samples1), (c2, s2, sq2, min2, max2, samples2)| {
let total = c1 + c2;
if total > 0 {
for val in samples2 {
if samples1.len() < 10000 {
samples1.push(val);
} else {
let idx = ((total.wrapping_mul(1103515245).wrapping_add(12345)) >> 16)
% total;
if idx < 10000 {
let len = samples1.len();
samples1[idx % len] = val;
}
}
}
}
(
c1 + c2,
s1 + s2,
sq1 + sq2,
min1.min(min2),
max1.max(max2),
samples1,
)
},
);
if count == 0 {
return Err(AlgorithmError::InsufficientData {
operation: "compute_statistics",
message: "No valid pixels found".to_string(),
});
}
let mean = sum / count as f64;
let variance = (sum_sq / count as f64) - (mean * mean);
let stddev = variance.sqrt();
let mut samples = median_samples;
samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let median = if samples.is_empty() {
mean } else if samples.len() % 2 == 0 {
let mid = samples.len() / 2;
(samples[mid - 1] + samples[mid]) / 2.0
} else {
samples[samples.len() / 2]
};
Ok(RasterStatistics {
count,
min,
max,
mean,
median,
stddev,
variance,
sum,
})
}
fn compute_statistics_sequential(raster: &RasterBuffer) -> Result<RasterStatistics> {
let mut count = 0usize;
let mut sum = 0.0f64;
let mut sum_sq = 0.0f64;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
let mut median_samples = Vec::new();
for y in 0..raster.height() {
for x in 0..raster.width() {
let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if !raster.is_nodata(val) && val.is_finite() {
count += 1;
sum += val;
sum_sq += val * val;
min = min.min(val);
max = max.max(val);
if median_samples.len() < 10000 {
median_samples.push(val);
} else {
let idx = ((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
if idx < 10000 {
median_samples[idx] = val;
}
}
}
}
}
if count == 0 {
return Err(AlgorithmError::InsufficientData {
operation: "compute_statistics",
message: "No valid pixels found".to_string(),
});
}
let mean = sum / count as f64;
let variance = (sum_sq / count as f64) - (mean * mean);
let stddev = variance.sqrt();
median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let median = if median_samples.is_empty() {
mean
} else if median_samples.len() % 2 == 0 {
let mid = median_samples.len() / 2;
(median_samples[mid - 1] + median_samples[mid]) / 2.0
} else {
median_samples[median_samples.len() / 2]
};
Ok(RasterStatistics {
count,
min,
max,
mean,
median,
stddev,
variance,
sum,
})
}
#[cfg(feature = "parallel")]
pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
compute_percentiles_parallel(raster)
}
#[cfg(not(feature = "parallel"))]
pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
compute_percentiles_sequential(raster)
}
#[cfg(feature = "parallel")]
fn compute_percentiles_parallel(raster: &RasterBuffer) -> Result<Percentiles> {
const SAMPLE_SIZE: usize = 10000;
let (count, samples) = (0..raster.height())
.into_par_iter()
.map(|y| {
let mut row_count = 0usize;
let mut row_samples = Vec::with_capacity(SAMPLE_SIZE.min(raster.width() as usize));
for x in 0..raster.width() {
if let Ok(val) = raster.get_pixel(x, y) {
if !raster.is_nodata(val) && val.is_finite() {
row_count += 1;
if row_samples.len() < SAMPLE_SIZE {
row_samples.push(val);
} else {
let idx = ((row_count.wrapping_mul(1103515245).wrapping_add(12345))
>> 16)
% row_count;
if idx < SAMPLE_SIZE {
row_samples[idx] = val;
}
}
}
}
}
(row_count, row_samples)
})
.reduce(
|| (0, Vec::new()),
|(c1, mut s1), (c2, s2)| {
let total = c1 + c2;
for val in s2 {
if s1.len() < SAMPLE_SIZE {
s1.push(val);
} else if total > 0 {
let idx =
((total.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % total;
if idx < SAMPLE_SIZE {
let len = s1.len();
s1[idx % len] = val;
}
}
}
(total, s1)
},
);
if count == 0 || samples.is_empty() {
return Err(AlgorithmError::InsufficientData {
operation: "compute_percentiles",
message: "No valid pixels found".to_string(),
});
}
let mut sorted_samples = samples;
sorted_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let p10 = percentile(&sorted_samples, 10.0)?;
let p25 = percentile(&sorted_samples, 25.0)?;
let p50 = percentile(&sorted_samples, 50.0)?;
let p75 = percentile(&sorted_samples, 75.0)?;
let p90 = percentile(&sorted_samples, 90.0)?;
Ok(Percentiles {
p10,
p25,
p50,
p75,
p90,
})
}
fn compute_percentiles_sequential(raster: &RasterBuffer) -> Result<Percentiles> {
const SAMPLE_SIZE: usize = 10000;
let mut count = 0usize;
let mut samples = Vec::with_capacity(SAMPLE_SIZE);
for y in 0..raster.height() {
for x in 0..raster.width() {
let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if !raster.is_nodata(val) && val.is_finite() {
count += 1;
if samples.len() < SAMPLE_SIZE {
samples.push(val);
} else {
let idx = ((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
if idx < SAMPLE_SIZE {
samples[idx] = val;
}
}
}
}
}
if count == 0 || samples.is_empty() {
return Err(AlgorithmError::InsufficientData {
operation: "compute_percentiles",
message: "No valid pixels found".to_string(),
});
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let p10 = percentile(&samples, 10.0)?;
let p25 = percentile(&samples, 25.0)?;
let p50 = percentile(&samples, 50.0)?;
let p75 = percentile(&samples, 75.0)?;
let p90 = percentile(&samples, 90.0)?;
Ok(Percentiles {
p10,
p25,
p50,
p75,
p90,
})
}
fn percentile(sorted_values: &[f64], p: f64) -> Result<f64> {
if sorted_values.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "percentile",
});
}
if !(0.0..=100.0).contains(&p) {
return Err(AlgorithmError::InvalidParameter {
parameter: "percentile",
message: format!("Percentile must be between 0 and 100, got {p}"),
});
}
let n = sorted_values.len();
let rank = p / 100.0 * (n - 1) as f64;
let lower = rank.floor() as usize;
let upper = rank.ceil() as usize;
if lower == upper {
Ok(sorted_values[lower])
} else {
let fraction = rank - lower as f64;
Ok(sorted_values[lower] * (1.0 - fraction) + sorted_values[upper] * fraction)
}
}
pub fn compute_histogram(
raster: &RasterBuffer,
bins: usize,
min_val: Option<f64>,
max_val: Option<f64>,
) -> Result<Histogram> {
if bins == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "bins",
message: "Number of bins must be greater than 0".to_string(),
});
}
let mut values = Vec::new();
for y in 0..raster.height() {
for x in 0..raster.width() {
let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if !raster.is_nodata(val) && val.is_finite() {
values.push(val);
}
}
}
if values.is_empty() {
return Err(AlgorithmError::InsufficientData {
operation: "compute_histogram",
message: "No valid pixels found".to_string(),
});
}
let data_min = values.iter().copied().fold(f64::INFINITY, f64::min);
let data_max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let min = min_val.unwrap_or(data_min);
let max = max_val.unwrap_or(data_max);
if max <= min {
return Err(AlgorithmError::InvalidParameter {
parameter: "min/max",
message: format!("max ({max}) must be greater than min ({min})"),
});
}
let bin_width = (max - min) / bins as f64;
let mut edges = Vec::with_capacity(bins + 1);
for i in 0..=bins {
edges.push(min + i as f64 * bin_width);
}
let mut counts = vec![0usize; bins];
for &val in &values {
if val < min || val > max {
continue;
}
let bin_idx = if (val - max).abs() < f64::EPSILON {
bins - 1 } else {
((val - min) / bin_width).floor() as usize
};
if bin_idx < bins {
counts[bin_idx] += 1;
}
}
Ok(Histogram {
edges,
counts,
total: values.len(),
})
}
#[cfg(feature = "parallel")]
#[allow(dead_code)]
fn compute_histogram_parallel(
raster: &RasterBuffer,
bins: usize,
min_val: Option<f64>,
max_val: Option<f64>,
) -> Result<Histogram> {
if bins == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "bins",
message: "Number of bins must be greater than 0".to_string(),
});
}
let (min, max) = if min_val.is_none() || max_val.is_none() {
let (count, data_min, data_max) = (0..raster.height())
.into_par_iter()
.map(|y| {
let mut row_count = 0usize;
let mut row_min = f64::INFINITY;
let mut row_max = f64::NEG_INFINITY;
for x in 0..raster.width() {
if let Ok(val) = raster.get_pixel(x, y) {
if !raster.is_nodata(val) && val.is_finite() {
row_count += 1;
row_min = row_min.min(val);
row_max = row_max.max(val);
}
}
}
(row_count, row_min, row_max)
})
.reduce(
|| (0, f64::INFINITY, f64::NEG_INFINITY),
|(c1, min1, max1), (c2, min2, max2)| (c1 + c2, min1.min(min2), max1.max(max2)),
);
if count == 0 {
return Err(AlgorithmError::InsufficientData {
operation: "compute_histogram",
message: "No valid pixels found".to_string(),
});
}
(min_val.unwrap_or(data_min), max_val.unwrap_or(data_max))
} else {
(min_val.unwrap_or(0.0), max_val.unwrap_or(0.0))
};
if max <= min {
return Err(AlgorithmError::InvalidParameter {
parameter: "min/max",
message: format!("max ({max}) must be greater than min ({min})"),
});
}
let bin_width = (max - min) / bins as f64;
let mut edges = Vec::with_capacity(bins + 1);
for i in 0..=bins {
edges.push(min + i as f64 * bin_width);
}
let (total, counts) = (0..raster.height())
.into_par_iter()
.map(|y| {
let mut row_total = 0usize;
let mut row_counts = vec![0usize; bins];
for x in 0..raster.width() {
if let Ok(val) = raster.get_pixel(x, y) {
if !raster.is_nodata(val) && val.is_finite() {
if val >= min && val <= max {
row_total += 1;
let bin_idx = if (val - max).abs() < f64::EPSILON {
bins - 1
} else {
let idx = ((val - min) / bin_width).floor() as usize;
idx.min(bins - 1)
};
row_counts[bin_idx] += 1;
}
}
}
}
(row_total, row_counts)
})
.reduce(
|| (0, vec![0usize; bins]),
|(t1, mut c1), (t2, c2)| {
for (i, &count) in c2.iter().enumerate() {
c1[i] += count;
}
(t1 + t2, c1)
},
);
Ok(Histogram {
edges,
counts,
total,
})
}
pub fn compute_mode(raster: &RasterBuffer, bins: usize) -> Result<f64> {
let histogram = compute_histogram(raster, bins, None, None)?;
let max_bin = histogram
.counts
.iter()
.enumerate()
.max_by_key(|(_, count)| *count)
.map(|(idx, _)| idx)
.ok_or(AlgorithmError::InsufficientData {
operation: "compute_mode",
message: "No bins found".to_string(),
})?;
let mode = (histogram.edges[max_bin] + histogram.edges[max_bin + 1]) / 2.0;
Ok(mode)
}
#[derive(Debug, Clone)]
pub struct Zone {
pub id: usize,
pub pixels: Vec<(u64, u64)>,
}
#[cfg(feature = "parallel")]
pub fn compute_zonal_statistics(
raster: &RasterBuffer,
zones: &[Zone],
) -> Result<Vec<(usize, RasterStatistics)>> {
compute_zonal_statistics_parallel(raster, zones)
}
#[cfg(not(feature = "parallel"))]
pub fn compute_zonal_statistics(
raster: &RasterBuffer,
zones: &[Zone],
) -> Result<Vec<(usize, RasterStatistics)>> {
compute_zonal_statistics_sequential(raster, zones)
}
#[cfg(feature = "parallel")]
fn compute_zonal_statistics_parallel(
raster: &RasterBuffer,
zones: &[Zone],
) -> Result<Vec<(usize, RasterStatistics)>> {
zones
.par_iter()
.map(|zone| {
let mut count = 0usize;
let mut sum = 0.0f64;
let mut sum_sq = 0.0f64;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
for &(x, y) in &zone.pixels {
if x < raster.width() && y < raster.height() {
if let Ok(val) = raster.get_pixel(x, y) {
if !raster.is_nodata(val) && val.is_finite() {
count += 1;
sum += val;
sum_sq += val * val;
min = min.min(val);
max = max.max(val);
if median_samples.len() < 10000 {
median_samples.push(val);
} else {
let idx = fastrand::usize(0..count);
if idx < 10000 {
median_samples[idx] = val;
}
}
}
}
}
}
if count == 0 {
return Err(AlgorithmError::InsufficientData {
operation: "compute_zonal_statistics",
message: format!("Zone {} has no valid pixels", zone.id),
});
}
let mean = sum / count as f64;
let variance = (sum_sq / count as f64) - (mean * mean);
let stddev = variance.sqrt();
median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let median = if median_samples.is_empty() {
mean
} else if median_samples.len() % 2 == 0 {
let mid = median_samples.len() / 2;
(median_samples[mid - 1] + median_samples[mid]) / 2.0
} else {
median_samples[median_samples.len() / 2]
};
Ok((
zone.id,
RasterStatistics {
count,
min,
max,
mean,
median,
stddev,
variance,
sum,
},
))
})
.collect()
}
fn compute_zonal_statistics_sequential(
raster: &RasterBuffer,
zones: &[Zone],
) -> Result<Vec<(usize, RasterStatistics)>> {
let mut results = Vec::with_capacity(zones.len());
for zone in zones {
let mut count = 0usize;
let mut sum = 0.0f64;
let mut sum_sq = 0.0f64;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
for &(x, y) in &zone.pixels {
if x < raster.width() && y < raster.height() {
let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if !raster.is_nodata(val) && val.is_finite() {
count += 1;
sum += val;
sum_sq += val * val;
min = min.min(val);
max = max.max(val);
if median_samples.len() < 10000 {
median_samples.push(val);
} else {
let idx =
((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
if idx < 10000 {
median_samples[idx] = val;
}
}
}
}
}
if count == 0 {
return Err(AlgorithmError::InsufficientData {
operation: "compute_zonal_statistics",
message: format!("Zone {} has no valid pixels", zone.id),
});
}
let mean = sum / count as f64;
let variance = (sum_sq / count as f64) - (mean * mean);
let stddev = variance.sqrt();
median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let median = if median_samples.is_empty() {
mean
} else if median_samples.len() % 2 == 0 {
let mid = median_samples.len() / 2;
(median_samples[mid - 1] + median_samples[mid]) / 2.0
} else {
median_samples[median_samples.len() / 2]
};
results.push((
zone.id,
RasterStatistics {
count,
min,
max,
mean,
median,
stddev,
variance,
sum,
},
));
}
Ok(results)
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
use oxigdal_core::types::RasterDataType;
#[test]
fn test_basic_statistics() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let stats = compute_statistics(&raster);
assert!(stats.is_ok());
let s = stats.expect("Stats should be ok");
assert_eq!(s.count, 100);
assert!((s.min - 0.0).abs() < f64::EPSILON);
assert!((s.max - 99.0).abs() < f64::EPSILON);
assert!((s.mean - 49.5).abs() < 0.1);
}
#[test]
fn test_percentiles() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let perc = compute_percentiles(&raster);
assert!(perc.is_ok());
let p = perc.expect("Percentiles should be ok");
assert!((p.p50 - 49.5).abs() < 1.0); }
#[test]
fn test_histogram() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let hist = compute_histogram(&raster, 10, None, None);
assert!(hist.is_ok());
let h = hist.expect("Histogram should be ok");
assert_eq!(h.counts.len(), 10);
assert_eq!(h.edges.len(), 11);
assert_eq!(h.total, 100);
}
#[test]
fn test_zonal_stats() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let zone1 = Zone {
id: 1,
pixels: vec![(0, 0), (1, 0), (2, 0)], };
let zone2 = Zone {
id: 2,
pixels: vec![(7, 9), (8, 9), (9, 9)], };
let result = compute_zonal_statistics(&raster, &[zone1, zone2]);
assert!(result.is_ok());
let zones = result.expect("Zonal stats should be ok");
assert_eq!(zones.len(), 2);
assert_eq!(zones[0].0, 1);
assert!((zones[0].1.mean - 1.0).abs() < f64::EPSILON);
assert_eq!(zones[1].0, 2);
assert!((zones[1].1.mean - 98.0).abs() < f64::EPSILON);
}
#[test]
fn test_statistics_single_pixel() {
let mut raster = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
raster.set_pixel(0, 0, 42.0).ok();
let stats = compute_statistics(&raster);
assert!(stats.is_ok());
let s = stats.expect("Should succeed");
assert_eq!(s.count, 1);
assert!((s.min - 42.0).abs() < f64::EPSILON);
assert!((s.max - 42.0).abs() < f64::EPSILON);
assert!((s.mean - 42.0).abs() < f64::EPSILON);
assert!((s.median - 42.0).abs() < f64::EPSILON);
assert!((s.stddev - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_histogram_zero_bins() {
let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
let result = compute_histogram(&raster, 0, None, None);
assert!(result.is_err());
if let Err(AlgorithmError::InvalidParameter { .. }) = result {
} else {
panic!("Expected InvalidParameter error");
}
}
#[test]
fn test_histogram_invalid_range() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
raster.set_pixel(x, y, (x + y) as f64).ok();
}
}
let result = compute_histogram(&raster, 10, Some(100.0), Some(50.0)); assert!(result.is_err());
}
#[test]
fn test_percentile_out_of_range() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = percentile(&values, 150.0); assert!(result.is_err());
}
#[test]
fn test_percentile_negative() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = percentile(&values, -10.0);
assert!(result.is_err());
}
#[test]
fn test_percentile_empty_array() {
let values: Vec<f64> = vec![];
let result = percentile(&values, 50.0);
assert!(result.is_err());
if let Err(AlgorithmError::EmptyInput { .. }) = result {
} else {
panic!("Expected EmptyInput error");
}
}
#[test]
fn test_compute_mode() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
let val = if (x + y) % 3 == 0 {
50.0
} else {
(x * 10) as f64
};
raster.set_pixel(x, y, val).ok();
}
}
let result = compute_mode(&raster, 20);
assert!(result.is_ok());
}
#[test]
fn test_statistics_with_nodata() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
if x == 2 && y == 2 {
raster.set_pixel(x, y, f64::NAN).ok(); } else {
raster.set_pixel(x, y, (x + y) as f64).ok();
}
}
}
let stats = compute_statistics(&raster);
assert!(stats.is_ok());
let s = stats.expect("Should succeed");
assert_eq!(s.count, 24);
}
#[test]
fn test_percentiles_extreme_values() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let perc = compute_percentiles(&raster);
assert!(perc.is_ok());
let p = perc.expect("Should succeed");
assert!(p.p10 < 15.0);
assert!(p.p10 > 5.0);
assert!(p.p90 > 85.0);
assert!(p.p90 < 95.0);
}
#[test]
fn test_histogram_custom_range() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let hist = compute_histogram(&raster, 5, Some(0.0), Some(100.0));
assert!(hist.is_ok());
let h = hist.expect("Should succeed");
assert_eq!(h.counts.len(), 5);
assert_eq!(h.edges.len(), 6);
assert_eq!(h.total, 100);
assert!((h.edges[0] - 0.0).abs() < f64::EPSILON);
assert!((h.edges[5] - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_histogram_frequencies() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let hist = compute_histogram(&raster, 10, None, None);
assert!(hist.is_ok());
let h = hist.expect("Should succeed");
let freqs = h.frequencies();
assert_eq!(freqs.len(), 10);
let sum: f64 = freqs.iter().sum();
assert!((sum - 1.0).abs() < 0.001);
}
#[test]
fn test_zonal_stats_single_zone() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
raster.set_pixel(x, y, 10.0).ok();
}
}
let zone = Zone {
id: 1,
pixels: vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
};
let result = compute_zonal_statistics(&raster, &[zone]);
assert!(result.is_ok());
let zones = result.expect("Should succeed");
assert_eq!(zones.len(), 1);
assert!((zones[0].1.mean - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_zonal_stats_empty_zone() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
raster.set_pixel(x, y, f64::NAN).ok(); }
}
let zone = Zone {
id: 1,
pixels: vec![(0, 0), (1, 1)],
};
let result = compute_zonal_statistics(&raster, &[zone]);
assert!(result.is_err()); }
#[test]
fn test_zonal_stats_out_of_bounds() {
let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
let zone = Zone {
id: 1,
pixels: vec![(10, 10), (20, 20)], };
let result = compute_zonal_statistics(&raster, &[zone]);
assert!(result.is_err()); }
#[test]
fn test_zonal_stats_multiple_zones() {
let mut raster = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
for y in 0..20 {
for x in 0..20 {
raster.set_pixel(x, y, (y * 20 + x) as f64).ok();
}
}
let zones = vec![
Zone {
id: 1,
pixels: (0..10).flat_map(|y| (0..10).map(move |x| (x, y))).collect(),
},
Zone {
id: 2,
pixels: (10..20)
.flat_map(|y| (10..20).map(move |x| (x, y)))
.collect(),
},
Zone {
id: 3,
pixels: (0..10)
.flat_map(|y| (10..20).map(move |x| (x, y)))
.collect(),
},
];
let result = compute_zonal_statistics(&raster, &zones);
assert!(result.is_ok());
let zone_stats = result.expect("Should succeed");
assert_eq!(zone_stats.len(), 3);
}
#[test]
fn test_variance_and_stddev_relationship() {
let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
raster.set_pixel(x, y, (x + y) as f64).ok();
}
}
let stats = compute_statistics(&raster);
assert!(stats.is_ok());
let s = stats.expect("Should succeed");
assert!((s.stddev * s.stddev - s.variance).abs() < 0.001);
}
#[test]
fn test_median_vs_mean() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
let val = if x == 4 && y == 4 {
1000.0 } else {
10.0
};
raster.set_pixel(x, y, val).ok();
}
}
let stats = compute_statistics(&raster);
assert!(stats.is_ok());
let s = stats.expect("Should succeed");
assert!((s.median - 10.0).abs() < 1.0);
assert!(s.mean > s.median);
}
#[test]
fn test_percentile_interpolation() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let p50 = percentile(&values, 50.0);
assert!(p50.is_ok());
assert!((p50.expect("Should succeed") - 3.0).abs() < f64::EPSILON);
let p25 = percentile(&values, 25.0);
assert!(p25.is_ok());
assert!((p25.expect("Should succeed") - 2.0).abs() < 0.1);
let p75 = percentile(&values, 75.0);
assert!(p75.is_ok());
assert!((p75.expect("Should succeed") - 4.0).abs() < 0.1);
}
#[test]
fn test_histogram_edge_case_last_value() {
let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
raster.set_pixel(x, y, (x + y) as f64).ok();
}
}
let hist = compute_histogram(&raster, 8, Some(0.0), Some(8.0));
assert!(hist.is_ok());
let h = hist.expect("Should succeed");
assert_eq!(h.counts.len(), 8);
}
}