1use rayon::prelude::*;
7
8use crate::error::{AlgorithmError, Result};
9use oxigdal_core::buffer::RasterBuffer;
10use oxigdal_core::types::RasterDataType;
11
12#[derive(Debug, Clone)]
14pub struct ChunkConfig {
15 pub num_threads: Option<usize>,
17 pub chunk_size: Option<usize>,
19 pub min_chunk_size: usize,
21}
22
23impl Default for ChunkConfig {
24 fn default() -> Self {
25 Self {
26 num_threads: None,
27 chunk_size: None,
28 min_chunk_size: 8192, }
30 }
31}
32
33impl ChunkConfig {
34 #[must_use]
36 pub const fn new() -> Self {
37 Self {
38 num_threads: None,
39 chunk_size: None,
40 min_chunk_size: 8192,
41 }
42 }
43
44 #[must_use]
46 pub const fn with_threads(mut self, num_threads: usize) -> Self {
47 self.num_threads = Some(num_threads);
48 self
49 }
50
51 #[must_use]
53 pub const fn with_chunk_size(mut self, chunk_size: usize) -> Self {
54 self.chunk_size = Some(chunk_size);
55 self
56 }
57
58 #[must_use]
60 pub fn calculate_chunk_size(&self, buffer: &RasterBuffer) -> usize {
61 if let Some(size) = self.chunk_size {
62 return size.max(self.min_chunk_size);
63 }
64
65 let total_pixels = buffer.pixel_count() as usize;
66 let threads = self.num_threads.unwrap_or_else(rayon::current_num_threads);
67
68 let target_chunks = threads * 7;
70 let chunk_size = total_pixels / target_chunks;
71
72 chunk_size.max(self.min_chunk_size)
73 }
74}
75
76#[derive(Debug, Clone, Copy)]
78pub enum ReduceOp {
79 Sum,
81 Min,
83 Max,
85 Mean,
87 Count,
89}
90
91#[derive(Debug, Clone, Copy)]
93pub struct ReduceResult {
94 pub value: f64,
96 pub count: u64,
98}
99
100pub fn parallel_map_raster<F>(input: &RasterBuffer, func: F) -> Result<RasterBuffer>
137where
138 F: Fn(f64) -> f64 + Sync + Send,
139{
140 let config = ChunkConfig::default();
141 parallel_map_raster_with_config(input, &config, func)
142}
143
144pub fn parallel_map_raster_with_config<F>(
160 input: &RasterBuffer,
161 config: &ChunkConfig,
162 func: F,
163) -> Result<RasterBuffer>
164where
165 F: Fn(f64) -> f64 + Sync + Send,
166{
167 let width = input.width();
168 let height = input.height();
169 let data_type = input.data_type();
170
171 let mut output = RasterBuffer::zeros(width, height, data_type);
173
174 let chunk_size = config.calculate_chunk_size(input);
176 let total_pixels = (width * height) as usize;
177
178 let pixel_indices: Vec<usize> = (0..total_pixels).collect();
180
181 let results: Result<Vec<(usize, f64)>> = pixel_indices
183 .par_chunks(chunk_size)
184 .flat_map(|chunk| {
185 chunk
186 .iter()
187 .map(|&idx| {
188 let x = (idx as u64) % width;
189 let y = (idx as u64) / width;
190 let value = input.get_pixel(x, y)?;
191 let result = func(value);
192 Ok((idx, result))
193 })
194 .collect::<Vec<_>>()
195 })
196 .collect();
197
198 for (idx, value) in results? {
200 let x = (idx as u64) % width;
201 let y = (idx as u64) / width;
202 output.set_pixel(x, y, value)?;
203 }
204
205 Ok(output)
206}
207
208pub fn parallel_reduce_raster(input: &RasterBuffer, op: ReduceOp) -> Result<ReduceResult> {
245 let config = ChunkConfig::default();
246 parallel_reduce_raster_with_config(input, &config, op)
247}
248
249pub fn parallel_reduce_raster_with_config(
265 input: &RasterBuffer,
266 config: &ChunkConfig,
267 op: ReduceOp,
268) -> Result<ReduceResult> {
269 let width = input.width();
270 let height = input.height();
271 let chunk_size = config.calculate_chunk_size(input);
272 let total_pixels = (width * height) as usize;
273
274 let pixel_indices: Vec<usize> = (0..total_pixels).collect();
275
276 let result = pixel_indices
278 .par_chunks(chunk_size)
279 .map(|chunk| {
280 let mut local_sum = 0.0;
281 let mut local_min = f64::MAX;
282 let mut local_max = f64::MIN;
283 let mut local_count = 0u64;
284
285 for &idx in chunk {
286 let x = (idx as u64) % width;
287 let y = (idx as u64) / width;
288
289 match input.get_pixel(x, y) {
290 Ok(value) if !input.is_nodata(value) && value.is_finite() => {
291 match op {
292 ReduceOp::Sum | ReduceOp::Mean => local_sum += value,
293 ReduceOp::Min => local_min = local_min.min(value),
294 ReduceOp::Max => local_max = local_max.max(value),
295 ReduceOp::Count => {}
296 }
297 local_count += 1;
298 }
299 Ok(_) => {} Err(e) => return Err(e),
301 }
302 }
303
304 Ok((local_sum, local_min, local_max, local_count))
305 })
306 .reduce(
307 || Ok((0.0, f64::MAX, f64::MIN, 0u64)),
308 |acc, item| {
309 let (sum1, min1, max1, count1) = acc?;
310 let (sum2, min2, max2, count2) = item?;
311 Ok((sum1 + sum2, min1.min(min2), max1.max(max2), count1 + count2))
312 },
313 )?;
314
315 let (sum, min, max, count) = result;
316
317 let value = match op {
318 ReduceOp::Sum => sum,
319 ReduceOp::Min => min,
320 ReduceOp::Max => max,
321 ReduceOp::Mean => {
322 if count > 0 {
323 sum / count as f64
324 } else {
325 f64::NAN
326 }
327 }
328 ReduceOp::Count => count as f64,
329 };
330
331 Ok(ReduceResult { value, count })
332}
333
334pub fn parallel_transform_raster<F>(
353 input: &RasterBuffer,
354 output_type: RasterDataType,
355 func: F,
356) -> Result<RasterBuffer>
357where
358 F: Fn(u64, u64, f64) -> f64 + Sync + Send,
359{
360 let config = ChunkConfig::default();
361 parallel_transform_raster_with_config(input, output_type, &config, func)
362}
363
364pub fn parallel_transform_raster_with_config<F>(
381 input: &RasterBuffer,
382 output_type: RasterDataType,
383 config: &ChunkConfig,
384 func: F,
385) -> Result<RasterBuffer>
386where
387 F: Fn(u64, u64, f64) -> f64 + Sync + Send,
388{
389 let width = input.width();
390 let height = input.height();
391
392 let mut output = RasterBuffer::zeros(width, height, output_type);
394
395 let chunk_size = config.calculate_chunk_size(input);
397 let total_pixels = (width * height) as usize;
398
399 let pixel_indices: Vec<usize> = (0..total_pixels).collect();
400
401 let results: Result<Vec<(usize, f64)>> = pixel_indices
403 .par_chunks(chunk_size)
404 .flat_map(|chunk| {
405 chunk
406 .iter()
407 .map(|&idx| {
408 let x = (idx as u64) % width;
409 let y = (idx as u64) / width;
410 let value = input.get_pixel(x, y)?;
411 let result = func(x, y, value);
412 Ok((idx, result))
413 })
414 .collect::<Vec<_>>()
415 })
416 .collect();
417
418 for (idx, value) in results? {
420 let x = (idx as u64) % width;
421 let y = (idx as u64) / width;
422 output.set_pixel(x, y, value)?;
423 }
424
425 Ok(output)
426}
427
428pub fn parallel_windowed_operation<F>(
448 input: &RasterBuffer,
449 window_size: usize,
450 func: F,
451) -> Result<RasterBuffer>
452where
453 F: Fn(&[f64]) -> f64 + Sync + Send,
454{
455 if window_size.is_multiple_of(2) {
456 return Err(AlgorithmError::InvalidParameter {
457 parameter: "window_size",
458 message: "Window size must be odd".to_string(),
459 });
460 }
461
462 let width = input.width();
463 let height = input.height();
464 let data_type = input.data_type();
465
466 let mut output = RasterBuffer::zeros(width, height, data_type);
467
468 let radius = (window_size / 2) as i64;
469
470 let row_results: Result<Vec<Vec<f64>>> = (0..height)
472 .into_par_iter()
473 .map(|y| {
474 let mut row = Vec::with_capacity(width as usize);
475
476 for x in 0..width {
477 let mut window = Vec::with_capacity(window_size * window_size);
478
479 for wy in (y as i64 - radius)..=(y as i64 + radius) {
481 for wx in (x as i64 - radius)..=(x as i64 + radius) {
482 if wx >= 0 && wx < width as i64 && wy >= 0 && wy < height as i64 {
483 match input.get_pixel(wx as u64, wy as u64) {
484 Ok(value) if !input.is_nodata(value) => window.push(value),
485 _ => {} }
487 }
488 }
489 }
490
491 let result = if window.is_empty() {
492 input.nodata().as_f64().unwrap_or(f64::NAN)
493 } else {
494 func(&window)
495 };
496
497 row.push(result);
498 }
499
500 Ok(row)
501 })
502 .collect();
503
504 for (y, row) in row_results?.into_iter().enumerate() {
506 for (x, value) in row.into_iter().enumerate() {
507 output.set_pixel(x as u64, y as u64, value)?;
508 }
509 }
510
511 Ok(output)
512}
513
514pub fn parallel_focal_mean(input: &RasterBuffer, window_size: usize) -> Result<RasterBuffer> {
531 parallel_windowed_operation(input, window_size, |window| {
532 if window.is_empty() {
533 f64::NAN
534 } else {
535 window.iter().sum::<f64>() / window.len() as f64
536 }
537 })
538}
539
540pub fn parallel_focal_median(input: &RasterBuffer, window_size: usize) -> Result<RasterBuffer> {
557 parallel_windowed_operation(input, window_size, |window| {
558 if window.is_empty() {
559 return f64::NAN;
560 }
561
562 let mut sorted = window.to_vec();
563 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
564
565 let mid = sorted.len() / 2;
566 if sorted.len() % 2 == 0 {
567 (sorted[mid - 1] + sorted[mid]) / 2.0
568 } else {
569 sorted[mid]
570 }
571 })
572}
573
574#[cfg(feature = "parallel")]
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub enum FocalOp {
585 Mean,
587 Median,
589 Min,
591 Max,
593 Sum,
595 Range,
597 StdDev,
599}
600
601#[cfg(feature = "parallel")]
607const DEFAULT_STRIP_HEIGHT: u64 = 128;
608
609#[cfg(feature = "parallel")]
611fn extract_row_range(src: &RasterBuffer, row_start: u64, row_end: u64) -> Result<RasterBuffer> {
612 let w = src.width();
613 let h = row_end - row_start;
614 let mut sub = RasterBuffer::zeros(w, h, src.data_type());
615 for sy in 0..h {
616 let gy = row_start + sy;
617 for x in 0..w {
618 let v = src.get_pixel(x, gy).map_err(AlgorithmError::Core)?;
619 sub.set_pixel(x, sy, v).map_err(AlgorithmError::Core)?;
620 }
621 }
622 Ok(sub)
623}
624
625#[cfg(feature = "parallel")]
628fn write_strip_to_output(
629 partial: &RasterBuffer,
630 partial_row_start: u64,
631 dst: &mut RasterBuffer,
632 dst_row_start: u64,
633 dst_row_end: u64,
634) -> Result<()> {
635 let w = dst.width();
636 for dy in 0..(dst_row_end - dst_row_start) {
637 let py = partial_row_start + dy;
638 let gy = dst_row_start + dy;
639 for x in 0..w {
640 let v = partial.get_pixel(x, py).map_err(AlgorithmError::Core)?;
641 dst.set_pixel(x, gy, v).map_err(AlgorithmError::Core)?;
642 }
643 }
644 Ok(())
645}
646
647#[cfg(feature = "parallel")]
659pub fn hillshade_parallel(
660 dem: &RasterBuffer,
661 params: crate::raster::HillshadeParams,
662) -> Result<RasterBuffer> {
663 let w = dem.width();
664 let h = dem.height();
665
666 if w < 3 || h < 3 {
668 return crate::raster::hillshade(dem, params);
669 }
670
671 let interior_start: u64 = 1;
673 let interior_end: u64 = h - 1;
674 let interior_h = interior_end - interior_start;
675
676 let strip_h = DEFAULT_STRIP_HEIGHT.min(interior_h).max(1);
677 let n_strips = interior_h.div_ceil(strip_h) as usize;
678
679 let strips: Vec<(u64, u64)> = (0..n_strips)
680 .map(|i| {
681 let s = interior_start + i as u64 * strip_h;
682 let e = (s + strip_h).min(interior_end);
683 (s, e)
684 })
685 .collect();
686
687 let strip_results: Result<Vec<(u64, u64, RasterBuffer)>> = strips
689 .into_par_iter()
690 .map(|(s, e)| {
691 let halo_top = s - 1;
693 let halo_bot = (e + 1).min(h);
694
695 let sub = extract_row_range(dem, halo_top, halo_bot)?;
696 let partial = crate::raster::hillshade(&sub, params)?;
697 Ok((s, e, partial))
698 })
699 .collect();
700
701 let mut out = RasterBuffer::zeros(w, h, dem.data_type());
702 for (s, e, partial) in strip_results? {
703 write_strip_to_output(&partial, 1, &mut out, s, e)?;
706 }
707 Ok(out)
708}
709
710#[cfg(feature = "parallel")]
719pub fn slope_parallel(dem: &RasterBuffer, pixel_size: f64, z_factor: f64) -> Result<RasterBuffer> {
720 let w = dem.width();
721 let h = dem.height();
722
723 if w < 3 || h < 3 {
724 return crate::raster::slope(dem, pixel_size, z_factor);
725 }
726
727 let interior_start: u64 = 1;
728 let interior_end: u64 = h - 1;
729 let interior_h = interior_end - interior_start;
730
731 let strip_h = DEFAULT_STRIP_HEIGHT.min(interior_h).max(1);
732 let n_strips = interior_h.div_ceil(strip_h) as usize;
733
734 let strips: Vec<(u64, u64)> = (0..n_strips)
735 .map(|i| {
736 let s = interior_start + i as u64 * strip_h;
737 let e = (s + strip_h).min(interior_end);
738 (s, e)
739 })
740 .collect();
741
742 let strip_results: Result<Vec<(u64, u64, RasterBuffer)>> = strips
743 .into_par_iter()
744 .map(|(s, e)| {
745 let halo_top = s - 1;
746 let halo_bot = (e + 1).min(h);
747
748 let sub = extract_row_range(dem, halo_top, halo_bot)?;
749 let partial = crate::raster::slope(&sub, pixel_size, z_factor)?;
750 Ok((s, e, partial))
751 })
752 .collect();
753
754 let mut out = RasterBuffer::zeros(w, h, dem.data_type());
755 for (s, e, partial) in strip_results? {
756 write_strip_to_output(&partial, 1, &mut out, s, e)?;
757 }
758 Ok(out)
759}
760
761#[cfg(feature = "parallel")]
763fn dispatch_focal(
764 sub: &RasterBuffer,
765 window: &crate::raster::WindowShape,
766 op: FocalOp,
767 boundary: &crate::raster::FocalBoundaryMode,
768) -> Result<RasterBuffer> {
769 use crate::raster::{
770 focal_max, focal_mean, focal_median, focal_min, focal_range, focal_stddev, focal_sum,
771 };
772 match op {
773 FocalOp::Mean => focal_mean(sub, window, boundary),
774 FocalOp::Median => focal_median(sub, window, boundary),
775 FocalOp::Min => focal_min(sub, window, boundary),
776 FocalOp::Max => focal_max(sub, window, boundary),
777 FocalOp::Sum => focal_sum(sub, window, boundary),
778 FocalOp::Range => focal_range(sub, window, boundary),
779 FocalOp::StdDev => focal_stddev(sub, window, boundary),
780 }
781}
782
783#[cfg(feature = "parallel")]
805pub fn focal_parallel(
806 src: &RasterBuffer,
807 window: &crate::raster::WindowShape,
808 op: FocalOp,
809 boundary: &crate::raster::FocalBoundaryMode,
810) -> Result<RasterBuffer> {
811 let w = src.width();
812 let h = src.height();
813
814 let (_, win_h) = window.dimensions();
816 let halo = (win_h / 2) as u64;
817
818 let strip_h = DEFAULT_STRIP_HEIGHT.min(h).max(1);
819 let n_strips = h.div_ceil(strip_h) as usize;
820
821 let strips: Vec<(u64, u64)> = (0..n_strips)
822 .map(|i| {
823 let s = i as u64 * strip_h;
824 let e = (s + strip_h).min(h);
825 (s, e)
826 })
827 .collect();
828
829 let strip_results: Result<Vec<(u64, u64, RasterBuffer, u64)>> = strips
831 .into_par_iter()
832 .map(|(s, e)| {
833 let halo_top = s.saturating_sub(halo);
834 let halo_bot = (e + halo).min(h);
835
836 let sub = extract_row_range(src, halo_top, halo_bot)?;
837 let partial = dispatch_focal(&sub, window, op, boundary)?;
838
839 let local_start = s - halo_top;
841 Ok((s, e, partial, local_start))
842 })
843 .collect();
844
845 let mut out = RasterBuffer::zeros(w, h, src.data_type());
846 for (s, e, partial, local_start) in strip_results? {
847 write_strip_to_output(&partial, local_start, &mut out, s, e)?;
848 }
849 Ok(out)
850}
851
852#[cfg(test)]
853mod tests {
854 #![allow(clippy::expect_used)]
855
856 use super::*;
857 use approx::assert_relative_eq;
858
859 #[test]
860 fn test_chunk_config() {
861 let config = ChunkConfig::default();
862 assert!(config.num_threads.is_none());
863 assert!(config.chunk_size.is_none());
864 assert_eq!(config.min_chunk_size, 8192);
865 }
866
867 #[test]
868 fn test_chunk_config_builder() {
869 let config = ChunkConfig::new().with_threads(4).with_chunk_size(1024);
870 assert_eq!(config.num_threads, Some(4));
871 assert_eq!(config.chunk_size, Some(1024));
872 }
873
874 #[test]
875 fn test_parallel_map_raster() {
876 let input = RasterBuffer::zeros(100, 100, RasterDataType::Float32);
877 let output = parallel_map_raster(&input, |pixel| pixel + 1.0).expect("should work");
878
879 assert_eq!(output.width(), 100);
880 assert_eq!(output.height(), 100);
881
882 let value = output.get_pixel(50, 50).expect("should work");
883 assert_relative_eq!(value, 1.0, epsilon = 1e-6);
884 }
885
886 #[test]
887 fn test_parallel_reduce_sum() {
888 let mut input = RasterBuffer::zeros(100, 100, RasterDataType::Float32);
889
890 for y in 0..100 {
892 for x in 0..100 {
893 input.set_pixel(x, y, 1.0).expect("should work");
894 }
895 }
896
897 let result = parallel_reduce_raster(&input, ReduceOp::Sum).expect("should work");
898 assert_relative_eq!(result.value, 10000.0, epsilon = 1e-6);
899 assert_eq!(result.count, 10000);
900 }
901
902 #[test]
903 fn test_parallel_reduce_min_max() {
904 let mut input = RasterBuffer::zeros(100, 100, RasterDataType::Float32);
905
906 for y in 0..100 {
908 for x in 0..100 {
909 let value = (y * 100 + x) as f64;
910 input.set_pixel(x, y, value).expect("should work");
911 }
912 }
913
914 let min_result = parallel_reduce_raster(&input, ReduceOp::Min).expect("should work");
915 assert_relative_eq!(min_result.value, 0.0, epsilon = 1e-6);
916
917 let max_result = parallel_reduce_raster(&input, ReduceOp::Max).expect("should work");
918 assert_relative_eq!(max_result.value, 9999.0, epsilon = 1e-6);
919 }
920
921 #[test]
922 fn test_parallel_reduce_mean() {
923 let mut input = RasterBuffer::zeros(100, 100, RasterDataType::Float32);
924
925 for y in 0..100 {
927 for x in 0..100 {
928 let value = (y * 100 + x) as f64;
929 input.set_pixel(x, y, value).expect("should work");
930 }
931 }
932
933 let result = parallel_reduce_raster(&input, ReduceOp::Mean).expect("should work");
934 assert_relative_eq!(result.value, 4999.5, epsilon = 0.1);
935 }
936
937 #[test]
938 fn test_parallel_transform() {
939 let input = RasterBuffer::zeros(100, 100, RasterDataType::Float32);
940
941 let output = parallel_transform_raster(&input, RasterDataType::Float32, |x, _y, value| {
943 value + x as f64
944 })
945 .expect("should work");
946
947 let value = output.get_pixel(50, 25).expect("should work");
948 assert_relative_eq!(value, 50.0, epsilon = 1e-6);
949 }
950
951 #[test]
952 fn test_parallel_focal_mean() {
953 let mut input = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
954
955 input.set_pixel(5, 5, 100.0).expect("should work");
957
958 let output = parallel_focal_mean(&input, 3).expect("should work");
959
960 let value = output.get_pixel(5, 5).expect("should work");
962 assert_relative_eq!(value, 100.0 / 9.0, epsilon = 1e-6);
963 }
964
965 #[test]
966 fn test_parallel_focal_median() {
967 let mut input = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
968
969 input.set_pixel(5, 5, 100.0).expect("should work");
971
972 let output = parallel_focal_median(&input, 3).expect("should work");
973
974 let value = output.get_pixel(5, 5).expect("should work");
976 assert_relative_eq!(value, 0.0, epsilon = 1e-6);
977 }
978
979 #[test]
980 fn test_invalid_window_size() {
981 let input = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
982
983 let result = parallel_focal_mean(&input, 4);
985 assert!(result.is_err());
986 }
987}