1use crate::error::{AlgorithmError, Result};
22use oxigdal_core::buffer::RasterBuffer;
23
24#[cfg(not(feature = "std"))]
25use alloc::vec::Vec;
26
27#[cfg(feature = "parallel")]
28use rayon::prelude::*;
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct RasterStatistics {
33 pub count: usize,
35 pub min: f64,
37 pub max: f64,
39 pub mean: f64,
41 pub median: f64,
43 pub stddev: f64,
45 pub variance: f64,
47 pub sum: f64,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct Percentiles {
54 pub p10: f64,
56 pub p25: f64,
58 pub p50: f64,
60 pub p75: f64,
62 pub p90: f64,
64}
65
66#[derive(Debug, Clone)]
68pub struct Histogram {
69 pub edges: Vec<f64>,
71 pub counts: Vec<usize>,
73 pub total: usize,
75}
76
77impl Histogram {
78 fn find_bin(&self, value: f64) -> Option<usize> {
80 if value < self.edges[0] || value > *self.edges.last()? {
81 return None;
82 }
83
84 for i in 0..self.counts.len() {
86 if value >= self.edges[i] && value < self.edges[i + 1] {
87 return Some(i);
88 }
89 }
90
91 if (value - self.edges[self.counts.len()]).abs() < f64::EPSILON {
93 return Some(self.counts.len() - 1);
94 }
95
96 None
97 }
98
99 #[must_use]
101 pub fn frequencies(&self) -> Vec<f64> {
102 if self.total == 0 {
103 return vec![0.0; self.counts.len()];
104 }
105
106 self.counts
107 .iter()
108 .map(|&count| count as f64 / self.total as f64)
109 .collect()
110 }
111}
112
113#[cfg(feature = "parallel")]
123pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
124 compute_statistics_parallel(raster)
125}
126
127#[cfg(not(feature = "parallel"))]
129pub fn compute_statistics(raster: &RasterBuffer) -> Result<RasterStatistics> {
130 compute_statistics_sequential(raster)
131}
132
133#[cfg(feature = "parallel")]
138fn compute_statistics_parallel(raster: &RasterBuffer) -> Result<RasterStatistics> {
139 let (count, sum, sum_sq, min, max, median_samples) = (0..raster.height())
141 .into_par_iter()
142 .map(|y| {
143 let mut row_count = 0usize;
144 let mut row_sum = 0.0f64;
145 let mut row_sum_sq = 0.0f64;
146 let mut row_min = f64::INFINITY;
147 let mut row_max = f64::NEG_INFINITY;
148 let mut row_samples = Vec::new();
149
150 for x in 0..raster.width() {
151 if let Ok(val) = raster.get_pixel(x, y) {
152 if !raster.is_nodata(val) && val.is_finite() {
153 row_count += 1;
154 row_sum += val;
155 row_sum_sq += val * val;
156 row_min = row_min.min(val);
157 row_max = row_max.max(val);
158
159 if row_samples.len() < 10000 {
161 row_samples.push(val);
162 } else {
163 let idx = ((row_count.wrapping_mul(1103515245).wrapping_add(12345))
165 >> 16)
166 % row_count;
167 if idx < 10000 {
168 row_samples[idx] = val;
169 }
170 }
171 }
172 }
173 }
174
175 (
176 row_count,
177 row_sum,
178 row_sum_sq,
179 row_min,
180 row_max,
181 row_samples,
182 )
183 })
184 .reduce(
185 || (0, 0.0, 0.0, f64::INFINITY, f64::NEG_INFINITY, Vec::new()),
186 |(c1, s1, sq1, min1, max1, mut samples1), (c2, s2, sq2, min2, max2, samples2)| {
187 let total = c1 + c2;
189 if total > 0 {
190 for val in samples2 {
191 if samples1.len() < 10000 {
192 samples1.push(val);
193 } else {
194 let idx = ((total.wrapping_mul(1103515245).wrapping_add(12345)) >> 16)
195 % total;
196 if idx < 10000 {
197 let len = samples1.len();
198 samples1[idx % len] = val;
199 }
200 }
201 }
202 }
203
204 (
205 c1 + c2,
206 s1 + s2,
207 sq1 + sq2,
208 min1.min(min2),
209 max1.max(max2),
210 samples1,
211 )
212 },
213 );
214
215 if count == 0 {
216 return Err(AlgorithmError::InsufficientData {
217 operation: "compute_statistics",
218 message: "No valid pixels found".to_string(),
219 });
220 }
221
222 let mean = sum / count as f64;
223
224 let variance = (sum_sq / count as f64) - (mean * mean);
226 let stddev = variance.sqrt();
227
228 let mut samples = median_samples;
230 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
231
232 let median = if samples.is_empty() {
233 mean } else if samples.len() % 2 == 0 {
235 let mid = samples.len() / 2;
236 (samples[mid - 1] + samples[mid]) / 2.0
237 } else {
238 samples[samples.len() / 2]
239 };
240
241 Ok(RasterStatistics {
242 count,
243 min,
244 max,
245 mean,
246 median,
247 stddev,
248 variance,
249 sum,
250 })
251}
252
253fn compute_statistics_sequential(raster: &RasterBuffer) -> Result<RasterStatistics> {
259 let mut count = 0usize;
260 let mut sum = 0.0f64;
261 let mut sum_sq = 0.0f64;
262 let mut min = f64::INFINITY;
263 let mut max = f64::NEG_INFINITY;
264 let mut median_samples = Vec::new();
265
266 for y in 0..raster.height() {
268 for x in 0..raster.width() {
269 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
270 if !raster.is_nodata(val) && val.is_finite() {
271 count += 1;
272 sum += val;
273 sum_sq += val * val;
274 min = min.min(val);
275 max = max.max(val);
276
277 if median_samples.len() < 10000 {
279 median_samples.push(val);
280 } else {
281 let idx = ((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
284 if idx < 10000 {
285 median_samples[idx] = val;
286 }
287 }
288 }
289 }
290 }
291
292 if count == 0 {
293 return Err(AlgorithmError::InsufficientData {
294 operation: "compute_statistics",
295 message: "No valid pixels found".to_string(),
296 });
297 }
298
299 let mean = sum / count as f64;
300 let variance = (sum_sq / count as f64) - (mean * mean);
301 let stddev = variance.sqrt();
302
303 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
305
306 let median = if median_samples.is_empty() {
307 mean
308 } else if median_samples.len() % 2 == 0 {
309 let mid = median_samples.len() / 2;
310 (median_samples[mid - 1] + median_samples[mid]) / 2.0
311 } else {
312 median_samples[median_samples.len() / 2]
313 };
314
315 Ok(RasterStatistics {
316 count,
317 min,
318 max,
319 mean,
320 median,
321 stddev,
322 variance,
323 sum,
324 })
325}
326
327#[cfg(feature = "parallel")]
336pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
337 compute_percentiles_parallel(raster)
338}
339
340#[cfg(not(feature = "parallel"))]
342pub fn compute_percentiles(raster: &RasterBuffer) -> Result<Percentiles> {
343 compute_percentiles_sequential(raster)
344}
345
346#[cfg(feature = "parallel")]
348fn compute_percentiles_parallel(raster: &RasterBuffer) -> Result<Percentiles> {
349 const SAMPLE_SIZE: usize = 10000;
350
351 let (count, samples) = (0..raster.height())
353 .into_par_iter()
354 .map(|y| {
355 let mut row_count = 0usize;
356 let mut row_samples = Vec::with_capacity(SAMPLE_SIZE.min(raster.width() as usize));
357
358 for x in 0..raster.width() {
359 if let Ok(val) = raster.get_pixel(x, y) {
360 if !raster.is_nodata(val) && val.is_finite() {
361 row_count += 1;
362
363 if row_samples.len() < SAMPLE_SIZE {
364 row_samples.push(val);
365 } else {
366 let idx = ((row_count.wrapping_mul(1103515245).wrapping_add(12345))
367 >> 16)
368 % row_count;
369 if idx < SAMPLE_SIZE {
370 row_samples[idx] = val;
371 }
372 }
373 }
374 }
375 }
376
377 (row_count, row_samples)
378 })
379 .reduce(
380 || (0, Vec::new()),
381 |(c1, mut s1), (c2, s2)| {
382 let total = c1 + c2;
383
384 for val in s2 {
386 if s1.len() < SAMPLE_SIZE {
387 s1.push(val);
388 } else if total > 0 {
389 let idx =
390 ((total.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % total;
391 if idx < SAMPLE_SIZE {
392 let len = s1.len();
393 s1[idx % len] = val;
394 }
395 }
396 }
397
398 (total, s1)
399 },
400 );
401
402 if count == 0 || samples.is_empty() {
403 return Err(AlgorithmError::InsufficientData {
404 operation: "compute_percentiles",
405 message: "No valid pixels found".to_string(),
406 });
407 }
408
409 let mut sorted_samples = samples;
411 sorted_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
412
413 let p10 = percentile(&sorted_samples, 10.0)?;
414 let p25 = percentile(&sorted_samples, 25.0)?;
415 let p50 = percentile(&sorted_samples, 50.0)?;
416 let p75 = percentile(&sorted_samples, 75.0)?;
417 let p90 = percentile(&sorted_samples, 90.0)?;
418
419 Ok(Percentiles {
420 p10,
421 p25,
422 p50,
423 p75,
424 p90,
425 })
426}
427
428fn compute_percentiles_sequential(raster: &RasterBuffer) -> Result<Percentiles> {
430 const SAMPLE_SIZE: usize = 10000;
431
432 let mut count = 0usize;
433 let mut samples = Vec::with_capacity(SAMPLE_SIZE);
434
435 for y in 0..raster.height() {
437 for x in 0..raster.width() {
438 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
439 if !raster.is_nodata(val) && val.is_finite() {
440 count += 1;
441
442 if samples.len() < SAMPLE_SIZE {
443 samples.push(val);
444 } else {
445 let idx = ((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
446 if idx < SAMPLE_SIZE {
447 samples[idx] = val;
448 }
449 }
450 }
451 }
452 }
453
454 if count == 0 || samples.is_empty() {
455 return Err(AlgorithmError::InsufficientData {
456 operation: "compute_percentiles",
457 message: "No valid pixels found".to_string(),
458 });
459 }
460
461 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
463
464 let p10 = percentile(&samples, 10.0)?;
465 let p25 = percentile(&samples, 25.0)?;
466 let p50 = percentile(&samples, 50.0)?;
467 let p75 = percentile(&samples, 75.0)?;
468 let p90 = percentile(&samples, 90.0)?;
469
470 Ok(Percentiles {
471 p10,
472 p25,
473 p50,
474 p75,
475 p90,
476 })
477}
478
479fn percentile(sorted_values: &[f64], p: f64) -> Result<f64> {
481 if sorted_values.is_empty() {
482 return Err(AlgorithmError::EmptyInput {
483 operation: "percentile",
484 });
485 }
486
487 if !(0.0..=100.0).contains(&p) {
488 return Err(AlgorithmError::InvalidParameter {
489 parameter: "percentile",
490 message: format!("Percentile must be between 0 and 100, got {p}"),
491 });
492 }
493
494 let n = sorted_values.len();
495 let rank = p / 100.0 * (n - 1) as f64;
496 let lower = rank.floor() as usize;
497 let upper = rank.ceil() as usize;
498
499 if lower == upper {
500 Ok(sorted_values[lower])
501 } else {
502 let fraction = rank - lower as f64;
503 Ok(sorted_values[lower] * (1.0 - fraction) + sorted_values[upper] * fraction)
504 }
505}
506
507pub fn compute_histogram(
520 raster: &RasterBuffer,
521 bins: usize,
522 min_val: Option<f64>,
523 max_val: Option<f64>,
524) -> Result<Histogram> {
525 if bins == 0 {
526 return Err(AlgorithmError::InvalidParameter {
527 parameter: "bins",
528 message: "Number of bins must be greater than 0".to_string(),
529 });
530 }
531
532 let mut values = Vec::new();
533
534 for y in 0..raster.height() {
536 for x in 0..raster.width() {
537 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
538 if !raster.is_nodata(val) && val.is_finite() {
539 values.push(val);
540 }
541 }
542 }
543
544 if values.is_empty() {
545 return Err(AlgorithmError::InsufficientData {
546 operation: "compute_histogram",
547 message: "No valid pixels found".to_string(),
548 });
549 }
550
551 let data_min = values.iter().copied().fold(f64::INFINITY, f64::min);
553 let data_max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
554
555 let min = min_val.unwrap_or(data_min);
556 let max = max_val.unwrap_or(data_max);
557
558 if max <= min {
559 return Err(AlgorithmError::InvalidParameter {
560 parameter: "min/max",
561 message: format!("max ({max}) must be greater than min ({min})"),
562 });
563 }
564
565 let bin_width = (max - min) / bins as f64;
567 let mut edges = Vec::with_capacity(bins + 1);
568 for i in 0..=bins {
569 edges.push(min + i as f64 * bin_width);
570 }
571
572 let mut counts = vec![0usize; bins];
574 for &val in &values {
575 if val < min || val > max {
576 continue;
577 }
578
579 let bin_idx = if (val - max).abs() < f64::EPSILON {
580 bins - 1 } else {
582 ((val - min) / bin_width).floor() as usize
583 };
584
585 if bin_idx < bins {
586 counts[bin_idx] += 1;
587 }
588 }
589
590 Ok(Histogram {
591 edges,
592 counts,
593 total: values.len(),
594 })
595}
596
597#[cfg(feature = "parallel")]
599#[allow(dead_code)]
600fn compute_histogram_parallel(
601 raster: &RasterBuffer,
602 bins: usize,
603 min_val: Option<f64>,
604 max_val: Option<f64>,
605) -> Result<Histogram> {
606 if bins == 0 {
607 return Err(AlgorithmError::InvalidParameter {
608 parameter: "bins",
609 message: "Number of bins must be greater than 0".to_string(),
610 });
611 }
612
613 let (min, max) = if min_val.is_none() || max_val.is_none() {
615 let (count, data_min, data_max) = (0..raster.height())
616 .into_par_iter()
617 .map(|y| {
618 let mut row_count = 0usize;
619 let mut row_min = f64::INFINITY;
620 let mut row_max = f64::NEG_INFINITY;
621
622 for x in 0..raster.width() {
623 if let Ok(val) = raster.get_pixel(x, y) {
624 if !raster.is_nodata(val) && val.is_finite() {
625 row_count += 1;
626 row_min = row_min.min(val);
627 row_max = row_max.max(val);
628 }
629 }
630 }
631
632 (row_count, row_min, row_max)
633 })
634 .reduce(
635 || (0, f64::INFINITY, f64::NEG_INFINITY),
636 |(c1, min1, max1), (c2, min2, max2)| (c1 + c2, min1.min(min2), max1.max(max2)),
637 );
638
639 if count == 0 {
640 return Err(AlgorithmError::InsufficientData {
641 operation: "compute_histogram",
642 message: "No valid pixels found".to_string(),
643 });
644 }
645
646 (min_val.unwrap_or(data_min), max_val.unwrap_or(data_max))
647 } else {
648 (min_val.unwrap_or(0.0), max_val.unwrap_or(0.0))
649 };
650
651 if max <= min {
652 return Err(AlgorithmError::InvalidParameter {
653 parameter: "min/max",
654 message: format!("max ({max}) must be greater than min ({min})"),
655 });
656 }
657
658 let bin_width = (max - min) / bins as f64;
660 let mut edges = Vec::with_capacity(bins + 1);
661 for i in 0..=bins {
662 edges.push(min + i as f64 * bin_width);
663 }
664
665 let (total, counts) = (0..raster.height())
667 .into_par_iter()
668 .map(|y| {
669 let mut row_total = 0usize;
670 let mut row_counts = vec![0usize; bins];
671
672 for x in 0..raster.width() {
673 if let Ok(val) = raster.get_pixel(x, y) {
674 if !raster.is_nodata(val) && val.is_finite() {
675 if val >= min && val <= max {
676 row_total += 1;
677
678 let bin_idx = if (val - max).abs() < f64::EPSILON {
679 bins - 1
680 } else {
681 let idx = ((val - min) / bin_width).floor() as usize;
682 idx.min(bins - 1)
683 };
684
685 row_counts[bin_idx] += 1;
686 }
687 }
688 }
689 }
690
691 (row_total, row_counts)
692 })
693 .reduce(
694 || (0, vec![0usize; bins]),
695 |(t1, mut c1), (t2, c2)| {
696 for (i, &count) in c2.iter().enumerate() {
697 c1[i] += count;
698 }
699 (t1 + t2, c1)
700 },
701 );
702
703 Ok(Histogram {
704 edges,
705 counts,
706 total,
707 })
708}
709
710pub fn compute_mode(raster: &RasterBuffer, bins: usize) -> Result<f64> {
719 let histogram = compute_histogram(raster, bins, None, None)?;
720
721 let max_bin = histogram
723 .counts
724 .iter()
725 .enumerate()
726 .max_by_key(|(_, count)| *count)
727 .map(|(idx, _)| idx)
728 .ok_or(AlgorithmError::InsufficientData {
729 operation: "compute_mode",
730 message: "No bins found".to_string(),
731 })?;
732
733 let mode = (histogram.edges[max_bin] + histogram.edges[max_bin + 1]) / 2.0;
735 Ok(mode)
736}
737
738#[derive(Debug, Clone)]
740pub struct Zone {
741 pub id: usize,
743 pub pixels: Vec<(u64, u64)>,
745}
746
747#[cfg(feature = "parallel")]
761pub fn compute_zonal_statistics(
762 raster: &RasterBuffer,
763 zones: &[Zone],
764) -> Result<Vec<(usize, RasterStatistics)>> {
765 compute_zonal_statistics_parallel(raster, zones)
766}
767
768#[cfg(not(feature = "parallel"))]
770pub fn compute_zonal_statistics(
771 raster: &RasterBuffer,
772 zones: &[Zone],
773) -> Result<Vec<(usize, RasterStatistics)>> {
774 compute_zonal_statistics_sequential(raster, zones)
775}
776
777#[cfg(feature = "parallel")]
779fn compute_zonal_statistics_parallel(
780 raster: &RasterBuffer,
781 zones: &[Zone],
782) -> Result<Vec<(usize, RasterStatistics)>> {
783 zones
784 .par_iter()
785 .map(|zone| {
786 let mut count = 0usize;
788 let mut sum = 0.0f64;
789 let mut sum_sq = 0.0f64;
790 let mut min = f64::INFINITY;
791 let mut max = f64::NEG_INFINITY;
792 let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
793
794 for &(x, y) in &zone.pixels {
795 if x < raster.width() && y < raster.height() {
796 if let Ok(val) = raster.get_pixel(x, y) {
797 if !raster.is_nodata(val) && val.is_finite() {
798 count += 1;
799 sum += val;
800 sum_sq += val * val;
801 min = min.min(val);
802 max = max.max(val);
803
804 if median_samples.len() < 10000 {
806 median_samples.push(val);
807 } else {
808 let idx = fastrand::usize(0..count);
809 if idx < 10000 {
810 median_samples[idx] = val;
811 }
812 }
813 }
814 }
815 }
816 }
817
818 if count == 0 {
819 return Err(AlgorithmError::InsufficientData {
820 operation: "compute_zonal_statistics",
821 message: format!("Zone {} has no valid pixels", zone.id),
822 });
823 }
824
825 let mean = sum / count as f64;
826 let variance = (sum_sq / count as f64) - (mean * mean);
827 let stddev = variance.sqrt();
828
829 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
831
832 let median = if median_samples.is_empty() {
833 mean
834 } else if median_samples.len() % 2 == 0 {
835 let mid = median_samples.len() / 2;
836 (median_samples[mid - 1] + median_samples[mid]) / 2.0
837 } else {
838 median_samples[median_samples.len() / 2]
839 };
840
841 Ok((
842 zone.id,
843 RasterStatistics {
844 count,
845 min,
846 max,
847 mean,
848 median,
849 stddev,
850 variance,
851 sum,
852 },
853 ))
854 })
855 .collect()
856}
857
858fn compute_zonal_statistics_sequential(
860 raster: &RasterBuffer,
861 zones: &[Zone],
862) -> Result<Vec<(usize, RasterStatistics)>> {
863 let mut results = Vec::with_capacity(zones.len());
864
865 for zone in zones {
866 let mut count = 0usize;
868 let mut sum = 0.0f64;
869 let mut sum_sq = 0.0f64;
870 let mut min = f64::INFINITY;
871 let mut max = f64::NEG_INFINITY;
872 let mut median_samples = Vec::with_capacity(10000.min(zone.pixels.len()));
873
874 for &(x, y) in &zone.pixels {
875 if x < raster.width() && y < raster.height() {
876 let val = raster.get_pixel(x, y).map_err(AlgorithmError::Core)?;
877 if !raster.is_nodata(val) && val.is_finite() {
878 count += 1;
879 sum += val;
880 sum_sq += val * val;
881 min = min.min(val);
882 max = max.max(val);
883
884 if median_samples.len() < 10000 {
886 median_samples.push(val);
887 } else {
888 let idx =
890 ((count.wrapping_mul(1103515245).wrapping_add(12345)) >> 16) % count;
891 if idx < 10000 {
892 median_samples[idx] = val;
893 }
894 }
895 }
896 }
897 }
898
899 if count == 0 {
900 return Err(AlgorithmError::InsufficientData {
901 operation: "compute_zonal_statistics",
902 message: format!("Zone {} has no valid pixels", zone.id),
903 });
904 }
905
906 let mean = sum / count as f64;
907 let variance = (sum_sq / count as f64) - (mean * mean);
908 let stddev = variance.sqrt();
909
910 median_samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
912
913 let median = if median_samples.is_empty() {
914 mean
915 } else if median_samples.len() % 2 == 0 {
916 let mid = median_samples.len() / 2;
917 (median_samples[mid - 1] + median_samples[mid]) / 2.0
918 } else {
919 median_samples[median_samples.len() / 2]
920 };
921
922 results.push((
923 zone.id,
924 RasterStatistics {
925 count,
926 min,
927 max,
928 mean,
929 median,
930 stddev,
931 variance,
932 sum,
933 },
934 ));
935 }
936
937 Ok(results)
938}
939
940#[cfg(test)]
941#[allow(clippy::panic)]
942mod tests {
943 use super::*;
944 use oxigdal_core::types::RasterDataType;
945
946 #[test]
947 fn test_basic_statistics() {
948 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
949
950 for y in 0..10 {
952 for x in 0..10 {
953 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
954 }
955 }
956
957 let stats = compute_statistics(&raster);
958 assert!(stats.is_ok());
959 let s = stats.expect("Stats should be ok");
960
961 assert_eq!(s.count, 100);
962 assert!((s.min - 0.0).abs() < f64::EPSILON);
963 assert!((s.max - 99.0).abs() < f64::EPSILON);
964 assert!((s.mean - 49.5).abs() < 0.1);
965 }
966
967 #[test]
968 fn test_percentiles() {
969 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
970
971 for y in 0..10 {
972 for x in 0..10 {
973 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
974 }
975 }
976
977 let perc = compute_percentiles(&raster);
978 assert!(perc.is_ok());
979 let p = perc.expect("Percentiles should be ok");
980
981 assert!((p.p50 - 49.5).abs() < 1.0); }
983
984 #[test]
985 fn test_histogram() {
986 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
987
988 for y in 0..10 {
989 for x in 0..10 {
990 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
991 }
992 }
993
994 let hist = compute_histogram(&raster, 10, None, None);
995 assert!(hist.is_ok());
996 let h = hist.expect("Histogram should be ok");
997
998 assert_eq!(h.counts.len(), 10);
999 assert_eq!(h.edges.len(), 11);
1000 assert_eq!(h.total, 100);
1001 }
1002
1003 #[test]
1004 fn test_zonal_stats() {
1005 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1006
1007 for y in 0..10 {
1008 for x in 0..10 {
1009 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1010 }
1011 }
1012
1013 let zone1 = Zone {
1015 id: 1,
1016 pixels: vec![(0, 0), (1, 0), (2, 0)], };
1018
1019 let zone2 = Zone {
1020 id: 2,
1021 pixels: vec![(7, 9), (8, 9), (9, 9)], };
1023
1024 let result = compute_zonal_statistics(&raster, &[zone1, zone2]);
1025 assert!(result.is_ok());
1026 let zones = result.expect("Zonal stats should be ok");
1027
1028 assert_eq!(zones.len(), 2);
1029 assert_eq!(zones[0].0, 1);
1030 assert!((zones[0].1.mean - 1.0).abs() < f64::EPSILON);
1031
1032 assert_eq!(zones[1].0, 2);
1033 assert!((zones[1].1.mean - 98.0).abs() < f64::EPSILON);
1034 }
1035
1036 #[test]
1039 fn test_statistics_single_pixel() {
1040 let mut raster = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
1041 raster.set_pixel(0, 0, 42.0).ok();
1042
1043 let stats = compute_statistics(&raster);
1044 assert!(stats.is_ok());
1045 let s = stats.expect("Should succeed");
1046
1047 assert_eq!(s.count, 1);
1048 assert!((s.min - 42.0).abs() < f64::EPSILON);
1049 assert!((s.max - 42.0).abs() < f64::EPSILON);
1050 assert!((s.mean - 42.0).abs() < f64::EPSILON);
1051 assert!((s.median - 42.0).abs() < f64::EPSILON);
1052 assert!((s.stddev - 0.0).abs() < f64::EPSILON);
1053 }
1054
1055 #[test]
1056 fn test_histogram_zero_bins() {
1057 let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1058
1059 let result = compute_histogram(&raster, 0, None, None);
1060 assert!(result.is_err());
1061 if let Err(AlgorithmError::InvalidParameter { .. }) = result {
1062 } else {
1064 panic!("Expected InvalidParameter error");
1065 }
1066 }
1067
1068 #[test]
1069 fn test_histogram_invalid_range() {
1070 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1071 for y in 0..5 {
1072 for x in 0..5 {
1073 raster.set_pixel(x, y, (x + y) as f64).ok();
1074 }
1075 }
1076
1077 let result = compute_histogram(&raster, 10, Some(100.0), Some(50.0)); assert!(result.is_err());
1079 }
1080
1081 #[test]
1082 fn test_percentile_out_of_range() {
1083 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1084
1085 let result = percentile(&values, 150.0); assert!(result.is_err());
1087 }
1088
1089 #[test]
1090 fn test_percentile_negative() {
1091 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1092
1093 let result = percentile(&values, -10.0);
1094 assert!(result.is_err());
1095 }
1096
1097 #[test]
1098 fn test_percentile_empty_array() {
1099 let values: Vec<f64> = vec![];
1100
1101 let result = percentile(&values, 50.0);
1102 assert!(result.is_err());
1103 if let Err(AlgorithmError::EmptyInput { .. }) = result {
1104 } else {
1106 panic!("Expected EmptyInput error");
1107 }
1108 }
1109
1110 #[test]
1113 fn test_compute_mode() {
1114 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1115
1116 for y in 0..10 {
1118 for x in 0..10 {
1119 let val = if (x + y) % 3 == 0 {
1120 50.0
1121 } else {
1122 (x * 10) as f64
1123 };
1124 raster.set_pixel(x, y, val).ok();
1125 }
1126 }
1127
1128 let result = compute_mode(&raster, 20);
1129 assert!(result.is_ok());
1130 }
1131
1132 #[test]
1135 fn test_statistics_with_nodata() {
1136 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1137
1138 for y in 0..5 {
1139 for x in 0..5 {
1140 if x == 2 && y == 2 {
1141 raster.set_pixel(x, y, f64::NAN).ok(); } else {
1143 raster.set_pixel(x, y, (x + y) as f64).ok();
1144 }
1145 }
1146 }
1147
1148 let stats = compute_statistics(&raster);
1149 assert!(stats.is_ok());
1150 let s = stats.expect("Should succeed");
1151
1152 assert_eq!(s.count, 24);
1154 }
1155
1156 #[test]
1157 fn test_percentiles_extreme_values() {
1158 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1159
1160 for y in 0..10 {
1161 for x in 0..10 {
1162 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1163 }
1164 }
1165
1166 let perc = compute_percentiles(&raster);
1167 assert!(perc.is_ok());
1168 let p = perc.expect("Should succeed");
1169
1170 assert!(p.p10 < 15.0);
1172 assert!(p.p10 > 5.0);
1173
1174 assert!(p.p90 > 85.0);
1176 assert!(p.p90 < 95.0);
1177 }
1178
1179 #[test]
1180 fn test_histogram_custom_range() {
1181 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1182
1183 for y in 0..10 {
1184 for x in 0..10 {
1185 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1186 }
1187 }
1188
1189 let hist = compute_histogram(&raster, 5, Some(0.0), Some(100.0));
1190 assert!(hist.is_ok());
1191 let h = hist.expect("Should succeed");
1192
1193 assert_eq!(h.counts.len(), 5);
1194 assert_eq!(h.edges.len(), 6);
1195 assert_eq!(h.total, 100);
1196
1197 assert!((h.edges[0] - 0.0).abs() < f64::EPSILON);
1199 assert!((h.edges[5] - 100.0).abs() < f64::EPSILON);
1200 }
1201
1202 #[test]
1203 fn test_histogram_frequencies() {
1204 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1205
1206 for y in 0..10 {
1207 for x in 0..10 {
1208 raster.set_pixel(x, y, (y * 10 + x) as f64).ok();
1209 }
1210 }
1211
1212 let hist = compute_histogram(&raster, 10, None, None);
1213 assert!(hist.is_ok());
1214 let h = hist.expect("Should succeed");
1215
1216 let freqs = h.frequencies();
1217 assert_eq!(freqs.len(), 10);
1218
1219 let sum: f64 = freqs.iter().sum();
1221 assert!((sum - 1.0).abs() < 0.001);
1222 }
1223
1224 #[test]
1227 fn test_zonal_stats_single_zone() {
1228 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1229
1230 for y in 0..5 {
1231 for x in 0..5 {
1232 raster.set_pixel(x, y, 10.0).ok();
1233 }
1234 }
1235
1236 let zone = Zone {
1237 id: 1,
1238 pixels: vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
1239 };
1240
1241 let result = compute_zonal_statistics(&raster, &[zone]);
1242 assert!(result.is_ok());
1243 let zones = result.expect("Should succeed");
1244
1245 assert_eq!(zones.len(), 1);
1246 assert!((zones[0].1.mean - 10.0).abs() < f64::EPSILON);
1247 }
1248
1249 #[test]
1250 fn test_zonal_stats_empty_zone() {
1251 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1252
1253 for y in 0..5 {
1254 for x in 0..5 {
1255 raster.set_pixel(x, y, f64::NAN).ok(); }
1257 }
1258
1259 let zone = Zone {
1260 id: 1,
1261 pixels: vec![(0, 0), (1, 1)],
1262 };
1263
1264 let result = compute_zonal_statistics(&raster, &[zone]);
1265 assert!(result.is_err()); }
1267
1268 #[test]
1269 fn test_zonal_stats_out_of_bounds() {
1270 let raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1271
1272 let zone = Zone {
1273 id: 1,
1274 pixels: vec![(10, 10), (20, 20)], };
1276
1277 let result = compute_zonal_statistics(&raster, &[zone]);
1278 assert!(result.is_err()); }
1280
1281 #[test]
1282 fn test_zonal_stats_multiple_zones() {
1283 let mut raster = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
1284
1285 for y in 0..20 {
1286 for x in 0..20 {
1287 raster.set_pixel(x, y, (y * 20 + x) as f64).ok();
1288 }
1289 }
1290
1291 let zones = vec![
1292 Zone {
1293 id: 1,
1294 pixels: (0..10).flat_map(|y| (0..10).map(move |x| (x, y))).collect(),
1295 },
1296 Zone {
1297 id: 2,
1298 pixels: (10..20)
1299 .flat_map(|y| (10..20).map(move |x| (x, y)))
1300 .collect(),
1301 },
1302 Zone {
1303 id: 3,
1304 pixels: (0..10)
1305 .flat_map(|y| (10..20).map(move |x| (x, y)))
1306 .collect(),
1307 },
1308 ];
1309
1310 let result = compute_zonal_statistics(&raster, &zones);
1311 assert!(result.is_ok());
1312 let zone_stats = result.expect("Should succeed");
1313
1314 assert_eq!(zone_stats.len(), 3);
1315 }
1316
1317 #[test]
1320 fn test_variance_and_stddev_relationship() {
1321 let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1322
1323 for y in 0..10 {
1324 for x in 0..10 {
1325 raster.set_pixel(x, y, (x + y) as f64).ok();
1326 }
1327 }
1328
1329 let stats = compute_statistics(&raster);
1330 assert!(stats.is_ok());
1331 let s = stats.expect("Should succeed");
1332
1333 assert!((s.stddev * s.stddev - s.variance).abs() < 0.001);
1335 }
1336
1337 #[test]
1338 fn test_median_vs_mean() {
1339 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1340
1341 for y in 0..5 {
1343 for x in 0..5 {
1344 let val = if x == 4 && y == 4 {
1345 1000.0 } else {
1347 10.0
1348 };
1349 raster.set_pixel(x, y, val).ok();
1350 }
1351 }
1352
1353 let stats = compute_statistics(&raster);
1354 assert!(stats.is_ok());
1355 let s = stats.expect("Should succeed");
1356
1357 assert!((s.median - 10.0).abs() < 1.0);
1359 assert!(s.mean > s.median);
1361 }
1362
1363 #[test]
1364 fn test_percentile_interpolation() {
1365 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1366
1367 let p50 = percentile(&values, 50.0);
1369 assert!(p50.is_ok());
1370 assert!((p50.expect("Should succeed") - 3.0).abs() < f64::EPSILON);
1371
1372 let p25 = percentile(&values, 25.0);
1374 assert!(p25.is_ok());
1375 assert!((p25.expect("Should succeed") - 2.0).abs() < 0.1);
1376
1377 let p75 = percentile(&values, 75.0);
1379 assert!(p75.is_ok());
1380 assert!((p75.expect("Should succeed") - 4.0).abs() < 0.1);
1381 }
1382
1383 #[test]
1384 fn test_histogram_edge_case_last_value() {
1385 let mut raster = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
1386
1387 for y in 0..5 {
1388 for x in 0..5 {
1389 raster.set_pixel(x, y, (x + y) as f64).ok();
1390 }
1391 }
1392
1393 let hist = compute_histogram(&raster, 8, Some(0.0), Some(8.0));
1394 assert!(hist.is_ok());
1395 let h = hist.expect("Should succeed");
1396
1397 assert_eq!(h.counts.len(), 8);
1399 }
1400}