1use crate::error::{AlgorithmError, Result};
35use oxigdal_core::buffer::RasterBuffer;
36
37#[cfg(not(feature = "std"))]
38use alloc::vec::Vec;
39
40#[cfg(feature = "parallel")]
41use rayon::prelude::*;
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct RasterStatistics {
46 pub count: usize,
48 pub min: f64,
50 pub max: f64,
52 pub mean: f64,
54 pub median: f64,
61 pub stddev: f64,
63 pub variance: f64,
65 pub sum: f64,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub struct Percentiles {
72 pub p10: f64,
74 pub p25: f64,
76 pub p50: f64,
78 pub p75: f64,
80 pub p90: f64,
82}
83
84#[derive(Debug, Clone)]
86pub struct Histogram {
87 pub edges: Vec<f64>,
89 pub counts: Vec<usize>,
91 pub total: usize,
93}
94
95impl Histogram {
96 fn find_bin(&self, value: f64) -> Option<usize> {
98 if value < self.edges[0] || value > *self.edges.last()? {
99 return None;
100 }
101
102 for i in 0..self.counts.len() {
104 if value >= self.edges[i] && value < self.edges[i + 1] {
105 return Some(i);
106 }
107 }
108
109 if (value - self.edges[self.counts.len()]).abs() < f64::EPSILON {
111 return Some(self.counts.len() - 1);
112 }
113
114 None
115 }
116
117 #[must_use]
119 pub fn frequencies(&self) -> Vec<f64> {
120 if self.total == 0 {
121 return vec![0.0; self.counts.len()];
122 }
123
124 self.counts
125 .iter()
126 .map(|&count| count as f64 / self.total as f64)
127 .collect()
128 }
129}
130
131fn reservoir_offer(reservoir: &mut Vec<f64>, capacity: usize, seen: usize, value: f64) {
142 if reservoir.len() < capacity {
143 reservoir.push(value);
144 } else if seen > 0 {
145 let idx = fastrand::usize(0..seen);
146 if idx < capacity {
147 reservoir[idx] = value;
148 }
149 }
150}
151
152fn reservoir_merge(
159 accumulator: &mut Vec<f64>,
160 capacity: usize,
161 accumulated_seen: usize,
162 incoming: Vec<f64>,
163) {
164 let mut seen = accumulated_seen;
165 for value in incoming {
166 seen += 1;
167 reservoir_offer(accumulator, capacity, seen, value);
168 }
169}
170
171#[cfg(feature = "parallel")]
181pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
182 compute_statistics_parallel(raster)
183}
184
185#[cfg(not(feature = "parallel"))]
187pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
188 compute_statistics_sequential(raster)
189}
190
191#[cfg(feature = "parallel")]
196fn compute_statistics_parallel(raster: &RasterBuffer) -> Result<RasterStatistics> {
197 let (count, sum, sum_sq, min, max, median_samples) = (0..raster.height())
199 .into_par_iter()
200 .map(|y| {
201 let mut row_count = 0usize;
202 let mut row_sum = 0.0f64;
203 let mut row_sum_sq = 0.0f64;
204 let mut row_min = f64::INFINITY;
205 let mut row_max = f64::NEG_INFINITY;
206 let mut row_samples = Vec::new();
207
208 for x in 0..raster.width() {
209 if let Ok(val) = raster.get_pixel(x, y) {
210 if !raster.is_nodata(val) && val.is_finite() {
211 row_count += 1;
212 row_sum += val;
213 row_sum_sq += val * val;
214 row_min = row_min.min(val);
215 row_max = row_max.max(val);
216
217 reservoir_offer(&mut row_samples, 10_000, row_count, val);
219 }
220 }
221 }
222
223 (
224 row_count,
225 row_sum,
226 row_sum_sq,
227 row_min,
228 row_max,
229 row_samples,
230 )
231 })
232 .reduce(
233 || (0, 0.0, 0.0, f64::INFINITY, f64::NEG_INFINITY, Vec::new()),
234 |(c1, s1, sq1, min1, max1, mut samples1), (c2, s2, sq2, min2, max2, samples2)| {
235 reservoir_merge(&mut samples1, 10_000, c1, samples2);
237
238 (
239 c1 + c2,
240 s1 + s2,
241 sq1 + sq2,
242 min1.min(min2),
243 max1.max(max2),
244 samples1,
245 )
246 },
247 );
248
249 if count == 0 {
250 return Err(AlgorithmError::InsufficientData {
251 operation: "compute_statistics",
252 message: "No valid pixels found".to_string(),
253 });
254 }
255
256 let mean = sum / count as f64;
257
258 let variance = (sum_sq / count as f64) - (mean * mean);
260 let stddev = variance.sqrt();
261
262 let mut samples = median_samples;
264 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
265
266 let median = if samples.is_empty() {
267 mean } else if samples.len() % 2 == 0 {
269 let mid = samples.len() / 2;
270 (samples[mid - 1] + samples[mid]) / 2.0
271 } else {
272 samples[samples.len() / 2]
273 };
274
275 Ok(RasterStatistics {
276 count,
277 min,
278 max,
279 mean,
280 median,
281 stddev,
282 variance,
283 sum,
284 })
285}
286
287fn compute_statistics_sequential(raster: &RasterBuffer) -> Result<RasterStatistics> {
295 let mut count = 0usize;
296 let mut sum = 0.0f64;
297 let mut sum_sq = 0.0f64;
298 let mut min = f64::INFINITY;
299 let mut max = f64::NEG_INFINITY;
300 let mut median_samples = Vec::new();
301
302 for y in 0..raster.height() {
304 for x in 0..raster.width() {
305 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
306 if !raster.is_nodata(val) && val.is_finite() {
307 count += 1;
308 sum += val;
309 sum_sq += val * val;
310 min = min.min(val);
311 max = max.max(val);
312
313 reservoir_offer(&mut median_samples, 10_000, count, val);
315 }
316 }
317 }
318
319 if count == 0 {
320 return Err(AlgorithmError::InsufficientData {
321 operation: "compute_statistics",
322 message: "No valid pixels found".to_string(),
323 });
324 }
325
326 let mean = sum / count as f64;
327 let variance = (sum_sq / count as f64) - (mean * mean);
328 let stddev = variance.sqrt();
329
330 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
332
333 let median = if median_samples.is_empty() {
334 mean
335 } else if median_samples.len() % 2 == 0 {
336 let mid = median_samples.len() / 2;
337 (median_samples[mid - 1] + median_samples[mid]) / 2.0
338 } else {
339 median_samples[median_samples.len() / 2]
340 };
341
342 Ok(RasterStatistics {
343 count,
344 min,
345 max,
346 mean,
347 median,
348 stddev,
349 variance,
350 sum,
351 })
352}
353
354pub fn compute_exact_median(raster: &RasterBuffer) -> Result<f64> {
369 let mut values = Vec::new();
370
371 for y in 0..raster.height() {
372 for x in 0..raster.width() {
373 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
374 if !raster.is_nodata(val) && val.is_finite() {
375 values.push(val);
376 }
377 }
378 }
379
380 exact_median_of(values, "compute_exact_median")
381}
382
383fn exact_median_of(mut values: Vec<f64>, operation: &'static str) -> Result<f64> {
388 if values.is_empty() {
389 return Err(AlgorithmError::InsufficientData {
390 operation,
391 message: "No valid pixels found".to_string(),
392 });
393 }
394
395 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal);
396 let len = values.len();
397 let mid = len / 2;
398
399 if len % 2 == 1 {
400 let (_, &mut median, _) = values.select_nth_unstable_by(mid, cmp);
401 Ok(median)
402 } else {
403 let (lower, &mut upper, _) = values.select_nth_unstable_by(mid, cmp);
408 let lower_max = lower.iter().copied().fold(f64::NEG_INFINITY, f64::max);
409 Ok((lower_max + upper) / 2.0)
410 }
411}
412
413#[cfg(feature = "parallel")]
422pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
423 compute_percentiles_parallel(raster)
424}
425
426#[cfg(not(feature = "parallel"))]
428pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
429 compute_percentiles_sequential(raster)
430}
431
432#[cfg(feature = "parallel")]
434fn compute_percentiles_parallel(raster: &RasterBuffer) -> Result<Percentiles> {
435 const SAMPLE_SIZE: usize = 10000;
436
437 let (count, samples) = (0..raster.height())
439 .into_par_iter()
440 .map(|y| {
441 let mut row_count = 0usize;
442 let mut row_samples = Vec::with_capacity(SAMPLE_SIZE.min(raster.width() as usize));
443
444 for x in 0..raster.width() {
445 if let Ok(val) = raster.get_pixel(x, y) {
446 if !raster.is_nodata(val) && val.is_finite() {
447 row_count += 1;
448 reservoir_offer(&mut row_samples, SAMPLE_SIZE, row_count, val);
449 }
450 }
451 }
452
453 (row_count, row_samples)
454 })
455 .reduce(
456 || (0, Vec::new()),
457 |(c1, mut s1), (c2, s2)| {
458 reservoir_merge(&mut s1, SAMPLE_SIZE, c1, s2);
460
461 (c1 + c2, s1)
462 },
463 );
464
465 if count == 0 || samples.is_empty() {
466 return Err(AlgorithmError::InsufficientData {
467 operation: "compute_percentiles",
468 message: "No valid pixels found".to_string(),
469 });
470 }
471
472 let mut sorted_samples = samples;
474 sorted_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
475
476 let p10 = percentile(&sorted_samples, 10.0)?;
477 let p25 = percentile(&sorted_samples, 25.0)?;
478 let p50 = percentile(&sorted_samples, 50.0)?;
479 let p75 = percentile(&sorted_samples, 75.0)?;
480 let p90 = percentile(&sorted_samples, 90.0)?;
481
482 Ok(Percentiles {
483 p10,
484 p25,
485 p50,
486 p75,
487 p90,
488 })
489}
490
491fn compute_percentiles_sequential(raster: &RasterBuffer) -> Result<Percentiles> {
493 const SAMPLE_SIZE: usize = 10000;
494
495 let mut count = 0usize;
496 let mut samples = Vec::with_capacity(SAMPLE_SIZE);
497
498 for y in 0..raster.height() {
500 for x in 0..raster.width() {
501 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
502 if !raster.is_nodata(val) && val.is_finite() {
503 count += 1;
504 reservoir_offer(&mut samples, SAMPLE_SIZE, count, val);
505 }
506 }
507 }
508
509 if count == 0 || samples.is_empty() {
510 return Err(AlgorithmError::InsufficientData {
511 operation: "compute_percentiles",
512 message: "No valid pixels found".to_string(),
513 });
514 }
515
516 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
518
519 let p10 = percentile(&samples, 10.0)?;
520 let p25 = percentile(&samples, 25.0)?;
521 let p50 = percentile(&samples, 50.0)?;
522 let p75 = percentile(&samples, 75.0)?;
523 let p90 = percentile(&samples, 90.0)?;
524
525 Ok(Percentiles {
526 p10,
527 p25,
528 p50,
529 p75,
530 p90,
531 })
532}
533
534fn percentile(sorted_values: &[f64], p: f64) -> Result<f64> {
536 if sorted_values.is_empty() {
537 return Err(AlgorithmError::EmptyInput {
538 operation: "percentile",
539 });
540 }
541
542 if !(0.0..=100.0).contains(&p) {
543 return Err(AlgorithmError::InvalidParameter {
544 parameter: "percentile",
545 message: format!("Percentile must be between 0 and 100, got {p}"),
546 });
547 }
548
549 let n = sorted_values.len();
550 let rank = p / 100.0 * (n - 1) as f64;
551 let lower = rank.floor() as usize;
552 let upper = rank.ceil() as usize;
553
554 if lower == upper {
555 Ok(sorted_values[lower])
556 } else {
557 let fraction = rank - lower as f64;
558 Ok(sorted_values[lower] * (1.0 - fraction) + sorted_values[upper] * fraction)
559 }
560}
561
562pub fn compute_histogram(
575 raster: &RasterBuffer,
576 bins: usize,
577 min_val: Option<f64>,
578 max_val: Option<f64>,
579) -> Result<Histogram> {
580 if bins == 0 {
581 return Err(AlgorithmError::InvalidParameter {
582 parameter: "bins",
583 message: "Number of bins must be greater than 0".to_string(),
584 });
585 }
586
587 let mut values = Vec::new();
588
589 for y in 0..raster.height() {
591 for x in 0..raster.width() {
592 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
593 if !raster.is_nodata(val) && val.is_finite() {
594 values.push(val);
595 }
596 }
597 }
598
599 if values.is_empty() {
600 return Err(AlgorithmError::InsufficientData {
601 operation: "compute_histogram",
602 message: "No valid pixels found".to_string(),
603 });
604 }
605
606 let data_min = values.iter().copied().fold(f64::INFINITY, f64::min);
608 let data_max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
609
610 let min = min_val.unwrap_or(data_min);
611 let max = max_val.unwrap_or(data_max);
612
613 if max <= min {
614 return Err(AlgorithmError::InvalidParameter {
615 parameter: "min/max",
616 message: format!("max ({max}) must be greater than min ({min})"),
617 });
618 }
619
620 let bin_width = (max - min) / bins as f64;
622 let mut edges = Vec::with_capacity(bins + 1);
623 for i in 0..=bins {
624 edges.push(min + i as f64 * bin_width);
625 }
626
627 let mut counts = vec![0usize; bins];
629 for &val in &values {
630 if val < min || val > max {
631 continue;
632 }
633
634 let bin_idx = if (val - max).abs() < f64::EPSILON {
635 bins - 1 } else {
637 ((val - min) / bin_width).floor() as usize
638 };
639
640 if bin_idx < bins {
641 counts[bin_idx] += 1;
642 }
643 }
644
645 Ok(Histogram {
646 edges,
647 counts,
648 total: values.len(),
649 })
650}
651
652#[cfg(feature = "parallel")]
654#[allow(dead_code)]
655fn compute_histogram_parallel(
656 raster: &RasterBuffer,
657 bins: usize,
658 min_val: Option<f64>,
659 max_val: Option<f64>,
660) -> Result<Histogram> {
661 if bins == 0 {
662 return Err(AlgorithmError::InvalidParameter {
663 parameter: "bins",
664 message: "Number of bins must be greater than 0".to_string(),
665 });
666 }
667
668 let (min, max) = if min_val.is_none() || max_val.is_none() {
670 let (count, data_min, data_max) = (0..raster.height())
671 .into_par_iter()
672 .map(|y| {
673 let mut row_count = 0usize;
674 let mut row_min = f64::INFINITY;
675 let mut row_max = f64::NEG_INFINITY;
676
677 for x in 0..raster.width() {
678 if let Ok(val) = raster.get_pixel(x, y) {
679 if !raster.is_nodata(val) && val.is_finite() {
680 row_count += 1;
681 row_min = row_min.min(val);
682 row_max = row_max.max(val);
683 }
684 }
685 }
686
687 (row_count, row_min, row_max)
688 })
689 .reduce(
690 || (0, f64::INFINITY, f64::NEG_INFINITY),
691 |(c1, min1, max1), (c2, min2, max2)| (c1 + c2, min1.min(min2), max1.max(max2)),
692 );
693
694 if count == 0 {
695 return Err(AlgorithmError::InsufficientData {
696 operation: "compute_histogram",
697 message: "No valid pixels found".to_string(),
698 });
699 }
700
701 (min_val.unwrap_or(data_min), max_val.unwrap_or(data_max))
702 } else {
703 (min_val.unwrap_or(0.0), max_val.unwrap_or(0.0))
704 };
705
706 if max <= min {
707 return Err(AlgorithmError::InvalidParameter {
708 parameter: "min/max",
709 message: format!("max ({max}) must be greater than min ({min})"),
710 });
711 }
712
713 let bin_width = (max - min) / bins as f64;
715 let mut edges = Vec::with_capacity(bins + 1);
716 for i in 0..=bins {
717 edges.push(min + i as f64 * bin_width);
718 }
719
720 let (total, counts) = (0..raster.height())
722 .into_par_iter()
723 .map(|y| {
724 let mut row_total = 0usize;
725 let mut row_counts = vec![0usize; bins];
726
727 for x in 0..raster.width() {
728 if let Ok(val) = raster.get_pixel(x, y) {
729 if !raster.is_nodata(val) && val.is_finite() {
730 if val >= min && val <= max {
731 row_total += 1;
732
733 let bin_idx = if (val - max).abs() < f64::EPSILON {
734 bins - 1
735 } else {
736 let idx = ((val - min) / bin_width).floor() as usize;
737 idx.min(bins - 1)
738 };
739
740 row_counts[bin_idx] += 1;
741 }
742 }
743 }
744 }
745
746 (row_total, row_counts)
747 })
748 .reduce(
749 || (0, vec![0usize; bins]),
750 |(t1, mut c1), (t2, c2)| {
751 for (i, &count) in c2.iter().enumerate() {
752 c1[i] += count;
753 }
754 (t1 + t2, c1)
755 },
756 );
757
758 Ok(Histogram {
759 edges,
760 counts,
761 total,
762 })
763}
764
765pub fn compute_mode(raster: &RasterBuffer, bins: usize) -> Result<f64> {
774 let histogram = compute_histogram(raster, bins, None, None)?;
775
776 let max_bin = histogram
778 .counts
779 .iter()
780 .enumerate()
781 .max_by_key(|(_, count)| *count)
782 .map(|(idx, _)| idx)
783 .ok_or(AlgorithmError::InsufficientData {
784 operation: "compute_mode",
785 message: "No bins found".to_string(),
786 })?;
787
788 let mode = (histogram.edges[max_bin] + histogram.edges[max_bin + 1]) / 2.0;
790 Ok(mode)
791}
792
793#[derive(Debug, Clone)]
795pub struct Zone {
796 pub id: usize,
798 pub pixels: Vec<(u64, u64)>,
800}
801
802#[cfg(feature = "parallel")]
816pub fn compute_zonal_statistics(
817 raster: &RasterBuffer,
818 zones: &[Zone],
819) -> Result<Vec<(usize, RasterStatistics)>> {
820 compute_zonal_statistics_parallel(raster, zones)
821}
822
823#[cfg(not(feature = "parallel"))]
825pub fn compute_zonal_statistics(
826 raster: &RasterBuffer,
827 zones: &[Zone],
828) -> Result<Vec<(usize, RasterStatistics)>> {
829 compute_zonal_statistics_sequential(raster, zones)
830}
831
832#[cfg(feature = "parallel")]
834fn compute_zonal_statistics_parallel(
835 raster: &RasterBuffer,
836 zones: &[Zone],
837) -> Result<Vec<(usize, RasterStatistics)>> {
838 zones
839 .par_iter()
840 .map(|zone| {
841 let mut count = 0usize;
843 let mut sum = 0.0f64;
844 let mut sum_sq = 0.0f64;
845 let mut min = f64::INFINITY;
846 let mut max = f64::NEG_INFINITY;
847 let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
848
849 for &(x, y) in &zone.pixels {
850 if x < raster.width() && y < raster.height() {
851 if let Ok(val) = raster.get_pixel(x, y) {
852 if !raster.is_nodata(val) && val.is_finite() {
853 count += 1;
854 sum += val;
855 sum_sq += val * val;
856 min = min.min(val);
857 max = max.max(val);
858
859 reservoir_offer(&mut median_samples, 10_000, count, val);
861 }
862 }
863 }
864 }
865
866 if count == 0 {
867 return Err(AlgorithmError::InsufficientData {
868 operation: "compute_zonal_statistics",
869 message: format!("Zone {} has no valid pixels", zone.id),
870 });
871 }
872
873 let mean = sum / count as f64;
874 let variance = (sum_sq / count as f64) - (mean * mean);
875 let stddev = variance.sqrt();
876
877 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
879
880 let median = if median_samples.is_empty() {
881 mean
882 } else if median_samples.len() % 2 == 0 {
883 let mid = median_samples.len() / 2;
884 (median_samples[mid - 1] + median_samples[mid]) / 2.0
885 } else {
886 median_samples[median_samples.len() / 2]
887 };
888
889 Ok((
890 zone.id,
891 RasterStatistics {
892 count,
893 min,
894 max,
895 mean,
896 median,
897 stddev,
898 variance,
899 sum,
900 },
901 ))
902 })
903 .collect()
904}
905
906fn compute_zonal_statistics_sequential(
908 raster: &RasterBuffer,
909 zones: &[Zone],
910) -> Result<Vec<(usize, RasterStatistics)>> {
911 let mut results = Vec::with_capacity(zones.len());
912
913 for zone in zones {
914 let mut count = 0usize;
916 let mut sum = 0.0f64;
917 let mut sum_sq = 0.0f64;
918 let mut min = f64::INFINITY;
919 let mut max = f64::NEG_INFINITY;
920 let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
921
922 for &(x, y) in &zone.pixels {
923 if x < raster.width() && y < raster.height() {
924 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
925 if !raster.is_nodata(val) && val.is_finite() {
926 count += 1;
927 sum += val;
928 sum_sq += val * val;
929 min = min.min(val);
930 max = max.max(val);
931
932 reservoir_offer(&mut median_samples, 10_000, count, val);
934 }
935 }
936 }
937
938 if count == 0 {
939 return Err(AlgorithmError::InsufficientData {
940 operation: "compute_zonal_statistics",
941 message: format!("Zone {} has no valid pixels", zone.id),
942 });
943 }
944
945 let mean = sum / count as f64;
946 let variance = (sum_sq / count as f64) - (mean * mean);
947 let stddev = variance.sqrt();
948
949 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
951
952 let median = if median_samples.is_empty() {
953 mean
954 } else if median_samples.len() % 2 == 0 {
955 let mid = median_samples.len() / 2;
956 (median_samples[mid - 1] + median_samples[mid]) / 2.0
957 } else {
958 median_samples[median_samples.len() / 2]
959 };
960
961 results.push((
962 zone.id,
963 RasterStatistics {
964 count,
965 min,
966 max,
967 mean,
968 median,
969 stddev,
970 variance,
971 sum,
972 },
973 ));
974 }
975
976 Ok(results)
977}
978
979#[cfg(test)]
980#[allow(clippy::panic)]
981mod tests {
982 use super::*;
983 use oxigdal_core::types::RasterDataType;
984
985 #[test]
986 fn test_basic_statistics() {
987 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
988
989 for y in 0..10 {
991 for x in 0..10 {
992 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
993 }
994 }
995
996 let stats = compute_statistics(&raster);
997 assert!(stats.is_ok());
998 let s = stats.expect("Stats should be ok");
999
1000 assert_eq!(s.count, 100);
1001 assert!((s.min - 0.0).abs() < f64::EPSILON);
1002 assert!((s.max - 99.0).abs() < f64::EPSILON);
1003 assert!((s.mean - 49.5).abs() < 0.1);
1004 }
1005
1006 #[test]
1007 fn test_percentiles() {
1008 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1009
1010 for y in 0..10 {
1011 for x in 0..10 {
1012 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1013 }
1014 }
1015
1016 let perc = compute_percentiles(&raster);
1017 assert!(perc.is_ok());
1018 let p = perc.expect("Percentiles should be ok");
1019
1020 assert!((p.p50 - 49.5).abs() < 1.0); }
1022
1023 #[test]
1024 fn test_histogram() {
1025 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1026
1027 for y in 0..10 {
1028 for x in 0..10 {
1029 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1030 }
1031 }
1032
1033 let hist = compute_histogram(&raster, 10, None, None);
1034 assert!(hist.is_ok());
1035 let h = hist.expect("Histogram should be ok");
1036
1037 assert_eq!(h.counts.len(), 10);
1038 assert_eq!(h.edges.len(), 11);
1039 assert_eq!(h.total, 100);
1040 }
1041
1042 #[test]
1043 fn test_zonal_stats() {
1044 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1045
1046 for y in 0..10 {
1047 for x in 0..10 {
1048 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1049 }
1050 }
1051
1052 let zone1 = Zone {
1054 id: 1,
1055 pixels: vec![(0, 0), (1, 0), (2, 0)], };
1057
1058 let zone2 = Zone {
1059 id: 2,
1060 pixels: vec![(7, 9), (8, 9), (9, 9)], };
1062
1063 let result = compute_zonal_statistics(&raster, &[zone1, zone2]);
1064 assert!(result.is_ok());
1065 let zones = result.expect("Zonal stats should be ok");
1066
1067 assert_eq!(zones.len(), 2);
1068 assert_eq!(zones[0].0, 1);
1069 assert!((zones[0].1.mean - 1.0).abs() < f64::EPSILON);
1070
1071 assert_eq!(zones[1].0, 2);
1072 assert!((zones[1].1.mean - 98.0).abs() < f64::EPSILON);
1073 }
1074
1075 #[test]
1078 fn test_statistics_single_pixel() {
1079 let mut raster = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
1080 raster.set_pixel(0, 0, 42.0).ok();
1081
1082 let stats = compute_statistics(&raster);
1083 assert!(stats.is_ok());
1084 let s = stats.expect("Should succeed");
1085
1086 assert_eq!(s.count, 1);
1087 assert!((s.min - 42.0).abs() < f64::EPSILON);
1088 assert!((s.max - 42.0).abs() < f64::EPSILON);
1089 assert!((s.mean - 42.0).abs() < f64::EPSILON);
1090 assert!((s.median - 42.0).abs() < f64::EPSILON);
1091 assert!((s.stddev - 0.0).abs() < f64::EPSILON);
1092 }
1093
1094 #[test]
1095 fn test_histogram_zero_bins() {
1096 let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1097
1098 let result = compute_histogram(&raster, 0, None, None);
1099 assert!(result.is_err());
1100 if let Err(AlgorithmError::InvalidParameter { .. }) = result {
1101 } else {
1103 panic!("Expected InvalidParameter error");
1104 }
1105 }
1106
1107 #[test]
1108 fn test_histogram_invalid_range() {
1109 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1110 for y in 0..5 {
1111 for x in 0..5 {
1112 raster.set_pixel(x, y, (x + y) as f64).ok();
1113 }
1114 }
1115
1116 let result = compute_histogram(&raster, 10, Some(100.0), Some(50.0)); assert!(result.is_err());
1118 }
1119
1120 #[test]
1121 fn test_percentile_out_of_range() {
1122 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1123
1124 let result = percentile(&values, 150.0); assert!(result.is_err());
1126 }
1127
1128 #[test]
1129 fn test_percentile_negative() {
1130 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1131
1132 let result = percentile(&values, -10.0);
1133 assert!(result.is_err());
1134 }
1135
1136 #[test]
1137 fn test_percentile_empty_array() {
1138 let values: Vec<f64> = vec![];
1139
1140 let result = percentile(&values, 50.0);
1141 assert!(result.is_err());
1142 if let Err(AlgorithmError::EmptyInput { .. }) = result {
1143 } else {
1145 panic!("Expected EmptyInput error");
1146 }
1147 }
1148
1149 #[test]
1152 fn test_compute_mode() {
1153 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1154
1155 for y in 0..10 {
1157 for x in 0..10 {
1158 let val = if (x + y) % 3 == 0 {
1159 50.0
1160 } else {
1161 (x * 10) as f64
1162 };
1163 raster.set_pixel(x, y, val).ok();
1164 }
1165 }
1166
1167 let result = compute_mode(&raster, 20);
1168 assert!(result.is_ok());
1169 }
1170
1171 #[test]
1174 fn test_statistics_with_nodata() {
1175 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1176
1177 for y in 0..5 {
1178 for x in 0..5 {
1179 if x == 2 && y == 2 {
1180 raster.set_pixel(x, y, f64::NAN).ok(); } else {
1182 raster.set_pixel(x, y, (x + y) as f64).ok();
1183 }
1184 }
1185 }
1186
1187 let stats = compute_statistics(&raster);
1188 assert!(stats.is_ok());
1189 let s = stats.expect("Should succeed");
1190
1191 assert_eq!(s.count, 24);
1193 }
1194
1195 #[test]
1196 fn test_percentiles_extreme_values() {
1197 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1198
1199 for y in 0..10 {
1200 for x in 0..10 {
1201 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1202 }
1203 }
1204
1205 let perc = compute_percentiles(&raster);
1206 assert!(perc.is_ok());
1207 let p = perc.expect("Should succeed");
1208
1209 assert!(p.p10 < 15.0);
1211 assert!(p.p10 > 5.0);
1212
1213 assert!(p.p90 > 85.0);
1215 assert!(p.p90 < 95.0);
1216 }
1217
1218 #[test]
1219 fn test_histogram_custom_range() {
1220 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1221
1222 for y in 0..10 {
1223 for x in 0..10 {
1224 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1225 }
1226 }
1227
1228 let hist = compute_histogram(&raster, 5, Some(0.0), Some(100.0));
1229 assert!(hist.is_ok());
1230 let h = hist.expect("Should succeed");
1231
1232 assert_eq!(h.counts.len(), 5);
1233 assert_eq!(h.edges.len(), 6);
1234 assert_eq!(h.total, 100);
1235
1236 assert!((h.edges[0] - 0.0).abs() < f64::EPSILON);
1238 assert!((h.edges[5] - 100.0).abs() < f64::EPSILON);
1239 }
1240
1241 #[test]
1242 fn test_histogram_frequencies() {
1243 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1244
1245 for y in 0..10 {
1246 for x in 0..10 {
1247 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1248 }
1249 }
1250
1251 let hist = compute_histogram(&raster, 10, None, None);
1252 assert!(hist.is_ok());
1253 let h = hist.expect("Should succeed");
1254
1255 let freqs = h.frequencies();
1256 assert_eq!(freqs.len(), 10);
1257
1258 let sum: f64 = freqs.iter().sum();
1260 assert!((sum - 1.0).abs() < 0.001);
1261 }
1262
1263 #[test]
1266 fn test_zonal_stats_single_zone() {
1267 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1268
1269 for y in 0..5 {
1270 for x in 0..5 {
1271 raster.set_pixel(x, y, 10.0).ok();
1272 }
1273 }
1274
1275 let zone = Zone {
1276 id: 1,
1277 pixels: vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
1278 };
1279
1280 let result = compute_zonal_statistics(&raster, &[zone]);
1281 assert!(result.is_ok());
1282 let zones = result.expect("Should succeed");
1283
1284 assert_eq!(zones.len(), 1);
1285 assert!((zones[0].1.mean - 10.0).abs() < f64::EPSILON);
1286 }
1287
1288 #[test]
1289 fn test_zonal_stats_empty_zone() {
1290 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1291
1292 for y in 0..5 {
1293 for x in 0..5 {
1294 raster.set_pixel(x, y, f64::NAN).ok(); }
1296 }
1297
1298 let zone = Zone {
1299 id: 1,
1300 pixels: vec![(0, 0), (1, 1)],
1301 };
1302
1303 let result = compute_zonal_statistics(&raster, &[zone]);
1304 assert!(result.is_err()); }
1306
1307 #[test]
1308 fn test_zonal_stats_out_of_bounds() {
1309 let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1310
1311 let zone = Zone {
1312 id: 1,
1313 pixels: vec![(10, 10), (20, 20)], };
1315
1316 let result = compute_zonal_statistics(&raster, &[zone]);
1317 assert!(result.is_err()); }
1319
1320 #[test]
1321 fn test_zonal_stats_multiple_zones() {
1322 let mut raster = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
1323
1324 for y in 0..20 {
1325 for x in 0..20 {
1326 raster.set_pixel(x, y, (y * 20 + x) as f64).ok();
1327 }
1328 }
1329
1330 let zones = vec![
1331 Zone {
1332 id: 1,
1333 pixels: (0..10).flat_map(|y| (0..10).map(move |x| (x, y))).collect(),
1334 },
1335 Zone {
1336 id: 2,
1337 pixels: (10..20)
1338 .flat_map(|y| (10..20).map(move |x| (x, y)))
1339 .collect(),
1340 },
1341 Zone {
1342 id: 3,
1343 pixels: (0..10)
1344 .flat_map(|y| (10..20).map(move |x| (x, y)))
1345 .collect(),
1346 },
1347 ];
1348
1349 let result = compute_zonal_statistics(&raster, &zones);
1350 assert!(result.is_ok());
1351 let zone_stats = result.expect("Should succeed");
1352
1353 assert_eq!(zone_stats.len(), 3);
1354 }
1355
1356 #[test]
1359 fn test_variance_and_stddev_relationship() {
1360 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1361
1362 for y in 0..10 {
1363 for x in 0..10 {
1364 raster.set_pixel(x, y, (x + y) as f64).ok();
1365 }
1366 }
1367
1368 let stats = compute_statistics(&raster);
1369 assert!(stats.is_ok());
1370 let s = stats.expect("Should succeed");
1371
1372 assert!((s.stddev * s.stddev - s.variance).abs() < 0.001);
1374 }
1375
1376 #[test]
1377 fn test_median_vs_mean() {
1378 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1379
1380 for y in 0..5 {
1382 for x in 0..5 {
1383 let val = if x == 4 && y == 4 {
1384 1000.0 } else {
1386 10.0
1387 };
1388 raster.set_pixel(x, y, val).ok();
1389 }
1390 }
1391
1392 let stats = compute_statistics(&raster);
1393 assert!(stats.is_ok());
1394 let s = stats.expect("Should succeed");
1395
1396 assert!((s.median - 10.0).abs() < 1.0);
1398 assert!(s.mean > s.median);
1400 }
1401
1402 #[test]
1403 fn test_percentile_interpolation() {
1404 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1405
1406 let p50 = percentile(&values, 50.0);
1408 assert!(p50.is_ok());
1409 assert!((p50.expect("Should succeed") - 3.0).abs() < f64::EPSILON);
1410
1411 let p25 = percentile(&values, 25.0);
1413 assert!(p25.is_ok());
1414 assert!((p25.expect("Should succeed") - 2.0).abs() < 0.1);
1415
1416 let p75 = percentile(&values, 75.0);
1418 assert!(p75.is_ok());
1419 assert!((p75.expect("Should succeed") - 4.0).abs() < 0.1);
1420 }
1421
1422 #[test]
1423 fn test_histogram_edge_case_last_value() {
1424 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1425
1426 for y in 0..5 {
1427 for x in 0..5 {
1428 raster.set_pixel(x, y, (x + y) as f64).ok();
1429 }
1430 }
1431
1432 let hist = compute_histogram(&raster, 8, Some(0.0), Some(8.0));
1433 assert!(hist.is_ok());
1434 let h = hist.expect("Should succeed");
1435
1436 assert_eq!(h.counts.len(), 8);
1438 }
1439
1440 #[test]
1441 fn test_exact_median_odd_count() {
1442 let mut raster = RasterBuffer::zeros(5, 1, RasterDataType::Float32);
1443 let values = [9.0, 1.0, 5.0, 3.0, 7.0];
1444 for (x, &v) in values.iter().enumerate() {
1445 raster.set_pixel(x as u64, 0, v).ok();
1446 }
1447
1448 let median = compute_exact_median(&raster).expect("median should succeed");
1450 assert!((median - 5.0).abs() < f64::EPSILON);
1451 }
1452
1453 #[test]
1454 fn test_exact_median_even_count() {
1455 let mut raster = RasterBuffer::zeros(3, 2, RasterDataType::Float32);
1456 let values = [5.0, 1.0, 3.0, 2.0, 4.0, 100.0];
1457 let mut idx = 0usize;
1458 for y in 0..2 {
1459 for x in 0..3 {
1460 raster.set_pixel(x, y, values[idx]).ok();
1461 idx += 1;
1462 }
1463 }
1464
1465 let median = compute_exact_median(&raster).expect("median should succeed");
1469 assert!((median - 3.5).abs() < f64::EPSILON);
1470 }
1471
1472 #[test]
1473 fn test_exact_median_no_valid_pixels_errors() {
1474 let mut raster = RasterBuffer::zeros(4, 4, RasterDataType::Float32);
1475 for y in 0..4 {
1476 for x in 0..4 {
1477 raster.set_pixel(x, y, f64::NAN).ok(); }
1479 }
1480
1481 let result = compute_exact_median(&raster);
1482 assert!(result.is_err());
1483 }
1484
1485 #[test]
1491 fn test_exact_median_matches_full_sort_for_large_raster() {
1492 let width = 120u64;
1495 let height = 130u64;
1496 let mut raster = RasterBuffer::zeros(width, height, RasterDataType::Float32);
1497
1498 let mut all_values = Vec::with_capacity((width * height) as usize);
1499 let mut n = 0i64;
1500 for y in 0..height {
1501 for x in 0..width {
1502 let val = ((n * 2654435761_i64).rem_euclid(1_000_003)) as f64;
1504 raster.set_pixel(x, y, val).ok();
1505 all_values.push(val);
1506 n += 1;
1507 }
1508 }
1509
1510 all_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
1511 let len = all_values.len();
1512 let expected = if len % 2 == 0 {
1513 (all_values[len / 2 - 1] + all_values[len / 2]) / 2.0
1514 } else {
1515 all_values[len / 2]
1516 };
1517
1518 let median = compute_exact_median(&raster).expect("median should succeed");
1519 assert!(
1520 (median - expected).abs() < f64::EPSILON,
1521 "exact median {median} did not match full-sort median {expected}"
1522 );
1523 }
1524
1525 #[test]
1532 fn test_reservoir_sampled_median_close_to_exact_for_large_raster() {
1533 let width = 150u64;
1534 let height = 100u64; let mut raster = RasterBuffer::zeros(width, height, RasterDataType::Float32);
1536
1537 let mut n = 0.0f64;
1538 for y in 0..height {
1539 for x in 0..width {
1540 raster.set_pixel(x, y, n).ok(); n += 1.0;
1542 }
1543 }
1544
1545 let exact = compute_exact_median(&raster).expect("exact median should succeed");
1546 let stats = compute_statistics(&raster).expect("statistics should succeed");
1547
1548 let tolerance = (width * height) as f64 * 0.02;
1553 assert!(
1554 (stats.median - exact).abs() < tolerance,
1555 "reservoir-sampled median {} too far from exact median {} (tolerance {})",
1556 stats.median,
1557 exact,
1558 tolerance
1559 );
1560 }
1561
1562 #[test]
1567 fn test_reservoir_offer_retains_all_items_within_capacity() {
1568 let mut reservoir: Vec<f64> = Vec::new();
1569 for i in 0..50usize {
1570 reservoir_offer(&mut reservoir, 100, i + 1, i as f64);
1571 }
1572
1573 assert_eq!(reservoir.len(), 50);
1574 let mut sorted = reservoir.clone();
1575 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
1576 let expected: Vec<f64> = (0..50).map(|i| i as f64).collect();
1577 assert_eq!(sorted, expected);
1578 }
1579
1580 #[test]
1584 fn test_reservoir_offer_stays_at_capacity_and_only_holds_stream_values() {
1585 let mut reservoir: Vec<f64> = Vec::new();
1586 let capacity = 20usize;
1587 let stream_len = 500usize;
1588
1589 for i in 0..stream_len {
1590 reservoir_offer(&mut reservoir, capacity, i + 1, i as f64);
1591 }
1592
1593 assert_eq!(reservoir.len(), capacity);
1594 for &val in &reservoir {
1595 assert!((0.0..stream_len as f64).contains(&val));
1596 }
1597 }
1598}