1use crate::error::{FFTError, FFTResult};
8use crate::fft::fft;
9use scirs2_core::ndarray::{s, ArrayBase, ArrayD, Data, Dimension, IxDyn};
10use scirs2_core::numeric::Complex64;
11use scirs2_core::numeric::NumCast;
12use std::fmt::Debug;
13use std::sync::Arc;
14use std::time::Instant;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DecompositionStrategy {
19 Slab,
21 Pencil,
23 Volumetric,
25 Adaptive,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CommunicationPattern {
32 AllToAll,
34 PointToPoint,
36 Neighbor,
38 Hybrid,
40}
41
42#[derive(Debug, Clone)]
44pub struct DistributedConfig {
45 pub node_count: usize,
47 pub rank: usize,
49 pub decomposition: DecompositionStrategy,
51 pub communication: CommunicationPattern,
53 pub process_grid: Vec<usize>,
55 pub local_size: Vec<usize>,
57 pub max_local_size: usize,
59}
60
61impl Default for DistributedConfig {
62 fn default() -> Self {
63 Self {
64 node_count: 1,
65 rank: 0,
66 decomposition: DecompositionStrategy::Slab,
67 communication: CommunicationPattern::AllToAll,
68 process_grid: vec![1],
69 local_size: vec![],
70 max_local_size: 1024, }
72 }
73}
74
75pub struct DistributedFFT {
77 config: DistributedConfig,
79 #[allow(dead_code)]
81 communicator: Arc<dyn Communicator>,
82}
83
84pub trait Communicator: Send + Sync + Debug {
86 fn send(&self, data: &[Complex64], dest: usize, tag: usize) -> FFTResult<()>;
88
89 fn recv(&self, src: usize, tag: usize, size: usize) -> FFTResult<Vec<Complex64>>;
91
92 fn all_to_all(&self, senddata: &[Complex64]) -> FFTResult<Vec<Complex64>>;
94
95 fn barrier(&self) -> FFTResult<()>;
97
98 fn size(&self) -> usize;
100
101 fn rank(&self) -> usize;
103}
104
105impl DistributedFFT {
106 pub fn new(config: DistributedConfig, communicator: Arc<dyn Communicator>) -> Self {
108 Self {
109 config,
110 communicator,
111 }
112 }
113
114 pub fn distributed_fft<S, D>(&self, input: &ArrayBase<S, D>) -> FFTResult<ArrayD<Complex64>>
116 where
117 S: Data,
118 D: Dimension,
119 S::Elem: Into<Complex64> + Copy + Debug + NumCast,
120 {
121 let start = Instant::now();
123
124 let input_dyn = input.to_owned().into_dyn();
126
127 let local_data = self.decompose_data(&input_dyn)?;
129
130 let decomp_time = start.elapsed();
132
133 let mut local_result = ArrayD::zeros(local_data.dim());
135 self.perform_local_fft(&local_data, &mut local_result)?;
136
137 let local_fft_time = start.elapsed() - decomp_time;
139
140 let exchanged_data = self.exchange_data(&local_result)?;
142
143 let comm_time = start.elapsed() - decomp_time - local_fft_time;
145
146 let final_result = self.finalize_result(&exchanged_data, input.shape())?;
148
149 let total_time = start.elapsed();
151
152 if cfg!(debug_assertions) {
154 println!("Distributed FFT Performance:");
155 println!(" Decomposition: {:?}", decomp_time);
156 println!(" Local FFT: {:?}", local_fft_time);
157 println!(" Communication: {:?}", comm_time);
158 println!(" Total time: {:?}", total_time);
159 }
160
161 Ok(final_result)
162 }
163
164 pub fn decompose_data<T>(&self, input: &ArrayD<T>) -> FFTResult<ArrayD<Complex64>>
166 where
167 T: Into<Complex64> + Copy + NumCast,
168 {
169 let is_testing = cfg!(test) || std::env::var("RUST_TEST").is_ok();
171
172 match self.config.decomposition {
173 DecompositionStrategy::Slab => self.slab_decomposition(input, is_testing),
174 DecompositionStrategy::Pencil => self.pencil_decomposition(input, is_testing),
175 DecompositionStrategy::Volumetric => self.volumetric_decomposition(input, is_testing),
176 DecompositionStrategy::Adaptive => self.adaptive_decomposition(input, is_testing),
177 }
178 }
179
180 fn perform_local_fft(
182 &self,
183 input: &ArrayD<Complex64>,
184 output: &mut ArrayD<Complex64>,
185 ) -> FFTResult<()> {
186 if input.ndim() == 1
188 || (input.ndim() >= 2 && self.config.decomposition == DecompositionStrategy::Slab)
189 {
190 if input.ndim() >= 2 {
192 for i in 0..input.shape()[0].min(self.config.max_local_size) {
193 let row = input.slice(s![i, ..]).to_vec();
194 let result = fft(&row, None)?;
195 let mut output_row = output.slice_mut(s![i, ..]);
196 for (j, val) in result.iter().enumerate().take(output_row.len()) {
197 output_row[j] = *val;
198 }
199 }
200 } else {
201 let result = fft(input.as_slice().unwrap_or(&[]), None)?;
203 for (i, val) in result.iter().enumerate().take(output.len()) {
204 output[i] = *val;
205 }
206 }
207 } else if input.ndim() >= 2 && self.config.decomposition == DecompositionStrategy::Pencil {
208 for i in 0..input.shape()[0].min(self.config.max_local_size) {
211 for j in 0..input.shape()[1].min(self.config.max_local_size) {
212 let column = input.slice(s![i, j, ..]).to_vec();
213 let result = fft(&column, None)?;
214 let mut output_col = output.slice_mut(s![i, j, ..]);
215 for (k, val) in result.iter().enumerate().take(output_col.len()) {
216 output_col[k] = *val;
217 }
218 }
219 }
220 } else {
221 return Err(FFTError::DimensionError(format!(
223 "Unsupported decomposition strategy for input of dimension {}",
224 input.ndim()
225 )));
226 }
227
228 Ok(())
229 }
230
231 fn exchange_data(&self, localresult: &ArrayD<Complex64>) -> FFTResult<ArrayD<Complex64>> {
240 if self.config.node_count == 1 {
242 return Ok(localresult.clone());
243 }
244
245 let flattened: Vec<Complex64> = localresult.iter().copied().collect();
247
248 match self.config.communication {
249 CommunicationPattern::AllToAll => {
250 let exchanged = self.communicator.all_to_all(&flattened)?;
255 Ok(ArrayD::from_shape_vec(IxDyn(&[exchanged.len()]), exchanged)
256 .map_err(|e| FFTError::DimensionError(e.to_string()))?)
257 }
258 CommunicationPattern::PointToPoint
259 | CommunicationPattern::Neighbor
260 | CommunicationPattern::Hybrid => {
261 Err(FFTError::NotImplementedError(format!(
265 "Communication pattern {:?} is not implemented for multi-node \
266 ({} nodes) distributed FFT; only single-node and AllToAll \
267 (via the active communicator) are supported.",
268 self.config.communication, self.config.node_count
269 )))
270 }
271 }
272 }
273
274 fn finalize_result(
276 &self,
277 exchanged_data: &ArrayD<Complex64>,
278 output_dim: &[usize],
279 ) -> FFTResult<ArrayD<Complex64>> {
280 if self.config.node_count == 1 || self.config.rank == 0 {
285 let limitedshape: Vec<usize> = output_dim
287 .iter()
288 .map(|&d| d.min(self.config.max_local_size))
289 .collect();
290
291 let mut output = ArrayD::zeros(IxDyn(&limitedshape));
293
294 if output_dim.len() == limitedshape.len() {
296 let mut all_match = true;
297 for (a, b) in output_dim.iter().zip(limitedshape.iter()) {
298 if a != b {
299 all_match = false;
300 break;
301 }
302 }
303
304 if all_match && !output.is_empty() && !exchanged_data.is_empty() {
305 let flat_output = output.as_slice_mut().expect("Operation failed");
307 for (i, &val) in exchanged_data.iter().enumerate().take(flat_output.len()) {
308 flat_output[i] = val;
309 }
310 } else {
311 if !output.is_empty() && !exchanged_data.is_empty() {
315 let flat_output = output.as_slice_mut().expect("Operation failed");
316 let copy_len = flat_output.len().min(exchanged_data.len());
317
318 for i in 0..copy_len {
319 flat_output[i] =
320 *exchanged_data.iter().nth(i).expect("Operation failed");
321 }
322 }
323 }
324 }
325
326 Ok(output)
327 } else {
328 Err(FFTError::ValueError(
331 "Only the root node (rank 0) produces the final output".to_string(),
332 ))
333 }
334 }
335
336 fn slab_decomposition<T>(
339 &self,
340 input: &ArrayD<T>,
341 is_testing: bool,
342 ) -> FFTResult<ArrayD<Complex64>>
343 where
344 T: Into<Complex64> + Copy + NumCast,
345 {
346 let shape = input.shape();
347
348 let max_size = if is_testing {
350 self.config.max_local_size
351 } else {
352 usize::MAX
353 };
354
355 if shape.is_empty() {
357 return Err(FFTError::DimensionError(
358 "Cannot perform FFT on empty array".to_string(),
359 ));
360 }
361
362 let total_slabs = shape[0];
364 let slabs_per_node = total_slabs.div_ceil(self.config.node_count);
365
366 let my_start = self.config.rank * slabs_per_node;
368 let my_end = (my_start + slabs_per_node).min(total_slabs);
369
370 if my_start >= total_slabs {
372 return Ok(ArrayD::zeros(IxDyn(&[0])));
374 }
375
376 let actual_end = my_end.min(my_start.saturating_add(max_size));
378
379 let mut myshape: Vec<usize> = shape.to_vec();
381 myshape[0] = actual_end - my_start;
382
383 let mut output = ArrayD::zeros(IxDyn(myshape.as_slice()));
385
386 if input.ndim() == 1 {
388 for i in my_start..actual_end {
390 let input_idx = IxDyn(&[i]);
391 let output_idx = IxDyn(&[i - my_start]);
392 let val: Complex64 =
393 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
394 output[output_idx] = val;
395 }
396 } else if input.ndim() == 2 {
397 for i in my_start..actual_end {
399 for j in 0..shape[1].min(max_size) {
400 let input_idx = IxDyn(&[i, j]);
401 let output_idx = IxDyn(&[i - my_start, j]);
402 let val: Complex64 =
403 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
404 output[output_idx] = val;
405 }
406 }
407 } else if input.ndim() == 3 {
408 for i in my_start..actual_end {
410 for j in 0..shape[1].min(max_size) {
411 for k in 0..shape[2].min(max_size) {
412 let input_idx = IxDyn(&[i, j, k]);
413 let output_idx = IxDyn(&[i - my_start, j, k]);
414 let val: Complex64 =
415 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
416 output[output_idx] = val;
417 }
418 }
419 }
420 } else {
421 let ndim = input.ndim();
424 let iter_shape: Vec<usize> = (0..ndim)
426 .map(|ax| {
427 if ax == 0 {
428 actual_end - my_start
429 } else {
430 myshape[ax].min(max_size)
431 }
432 })
433 .collect();
434 let iter_dim = IxDyn(iter_shape.as_slice());
435 for local_idx in scirs2_core::ndarray::indices(iter_dim) {
436 let local_slice = local_idx.slice();
437 let mut global = local_slice.to_vec();
439 global[0] += my_start;
440 let val: Complex64 = NumCast::from(input[IxDyn(global.as_slice())])
441 .unwrap_or(Complex64::new(0.0, 0.0));
442 output[IxDyn(local_slice)] = val;
443 }
444 }
445
446 Ok(output)
447 }
448
449 fn pencil_decomposition<T>(
450 &self,
451 input: &ArrayD<T>,
452 is_testing: bool,
453 ) -> FFTResult<ArrayD<Complex64>>
454 where
455 T: Into<Complex64> + Copy + NumCast,
456 {
457 let shape = input.shape();
458
459 let max_size = if is_testing {
461 self.config.max_local_size
462 } else {
463 usize::MAX
464 };
465
466 if shape.len() < 2 {
468 return Err(FFTError::DimensionError(
469 "Pencil decomposition requires at least 2D input".to_string(),
470 ));
471 }
472
473 let process_grid = &self.config.process_grid;
476 if process_grid.len() < 2 {
477 return Err(FFTError::ValueError(
478 "Pencil decomposition requires a 2D process grid".to_string(),
479 ));
480 }
481
482 let p1 = process_grid[0];
483 let p2 = process_grid[1];
484
485 if p1 * p2 != self.config.node_count {
486 return Err(FFTError::ValueError(format!(
487 "Process grid ({} x {}) doesn't match node count ({})",
488 p1, p2, self.config.node_count
489 )));
490 }
491
492 let my_row = self.config.rank / p2;
494 let my_col = self.config.rank % p2;
495
496 let n1 = shape[0];
498 let n2 = shape[1];
499
500 let rows_per_node = n1.div_ceil(p1);
501 let cols_per_node = n2.div_ceil(p2);
502
503 let my_start_row = my_row * rows_per_node;
504 let my_end_row = (my_start_row + rows_per_node).min(n1);
505
506 let my_start_col = my_col * cols_per_node;
507 let my_end_col = (my_start_col + cols_per_node).min(n2);
508
509 if my_start_row >= n1 || my_start_col >= n2 {
511 return Ok(ArrayD::zeros(IxDyn(&[0])));
513 }
514
515 let actual_end_row = my_end_row.min(my_start_row.saturating_add(max_size));
517 let actual_end_col = my_end_col.min(my_start_col.saturating_add(max_size));
518
519 let mut myshape: Vec<usize> = shape.to_vec();
521 myshape[0] = actual_end_row - my_start_row;
522 myshape[1] = actual_end_col - my_start_col;
523
524 let mut output = ArrayD::zeros(IxDyn(myshape.as_slice()));
526
527 if input.ndim() == 2 {
529 for i in my_start_row..actual_end_row {
531 for j in my_start_col..actual_end_col {
532 let input_idx = IxDyn(&[i, j]);
533 let output_idx = IxDyn(&[i - my_start_row, j - my_start_col]);
534 let val: Complex64 =
535 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
536 output[output_idx] = val;
537 }
538 }
539 } else if input.ndim() == 3 {
540 for i in my_start_row..actual_end_row {
542 for j in my_start_col..actual_end_col {
543 for k in 0..shape[2].min(max_size) {
544 let input_idx = IxDyn(&[i, j, k]);
545 let output_idx = IxDyn(&[i - my_start_row, j - my_start_col, k]);
546 let val: Complex64 =
547 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
548 output[output_idx] = val;
549 }
550 }
551 }
552 } else {
553 let ndim = input.ndim();
556 let iter_shape: Vec<usize> = (0..ndim)
557 .map(|ax| match ax {
558 0 => actual_end_row - my_start_row,
559 1 => actual_end_col - my_start_col,
560 _ => myshape[ax].min(max_size),
561 })
562 .collect();
563 let iter_dim = IxDyn(iter_shape.as_slice());
564 for local_idx in scirs2_core::ndarray::indices(iter_dim) {
565 let local_slice = local_idx.slice();
566 let mut global = local_slice.to_vec();
568 global[0] += my_start_row;
569 global[1] += my_start_col;
570 let val: Complex64 = NumCast::from(input[IxDyn(global.as_slice())])
571 .unwrap_or(Complex64::new(0.0, 0.0));
572 output[IxDyn(local_slice)] = val;
573 }
574 }
575
576 Ok(output)
577 }
578
579 fn volumetric_decomposition<T>(
580 &self,
581 input: &ArrayD<T>,
582 is_testing: bool,
583 ) -> FFTResult<ArrayD<Complex64>>
584 where
585 T: Into<Complex64> + Copy + NumCast,
586 {
587 let shape = input.shape();
588
589 let max_size = if is_testing {
591 self.config.max_local_size
592 } else {
593 usize::MAX
594 };
595
596 if shape.len() < 3 {
598 return Err(FFTError::DimensionError(
599 "Volumetric decomposition requires at least 3D input".to_string(),
600 ));
601 }
602
603 let process_grid = &self.config.process_grid;
606 if process_grid.len() < 3 {
607 return Err(FFTError::ValueError(
608 "Volumetric decomposition requires a 3D process grid".to_string(),
609 ));
610 }
611
612 let p1 = process_grid[0];
613 let p2 = process_grid[1];
614 let p3 = process_grid[2];
615
616 if p1 * p2 * p3 != self.config.node_count {
617 return Err(FFTError::ValueError(format!(
618 "Process grid ({} x {} x {}) doesn't match node count ({})",
619 p1, p2, p3, self.config.node_count
620 )));
621 }
622
623 let my_plane = self.config.rank / (p2 * p3);
625 let remainder = self.config.rank % (p2 * p3);
626 let my_row = remainder / p3;
627 let my_col = remainder % p3;
628
629 let n1 = shape[0];
631 let n2 = shape[1];
632 let n3 = shape[2];
633
634 let planes_per_node = n1.div_ceil(p1);
635 let rows_per_node = n2.div_ceil(p2);
636 let cols_per_node = n3.div_ceil(p3);
637
638 let my_start_plane = my_plane * planes_per_node;
639 let my_end_plane = (my_start_plane + planes_per_node).min(n1);
640
641 let my_start_row = my_row * rows_per_node;
642 let my_end_row = (my_start_row + rows_per_node).min(n2);
643
644 let my_start_col = my_col * cols_per_node;
645 let my_end_col = (my_start_col + cols_per_node).min(n3);
646
647 if my_start_plane >= n1 || my_start_row >= n2 || my_start_col >= n3 {
649 return Ok(ArrayD::zeros(IxDyn(&[0])));
651 }
652
653 let actual_end_plane = my_end_plane.min(my_start_plane.saturating_add(max_size));
655 let actual_end_row = my_end_row.min(my_start_row.saturating_add(max_size));
656 let actual_end_col = my_end_col.min(my_start_col.saturating_add(max_size));
657
658 let mut myshape: Vec<usize> = shape.to_vec();
660 myshape[0] = actual_end_plane - my_start_plane;
661 myshape[1] = actual_end_row - my_start_row;
662 myshape[2] = actual_end_col - my_start_col;
663
664 let mut output = ArrayD::zeros(IxDyn(myshape.as_slice()));
666
667 if input.ndim() == 3 {
669 for i in my_start_plane..actual_end_plane {
671 for j in my_start_row..actual_end_row {
672 for k in my_start_col..actual_end_col {
673 let input_idx = IxDyn(&[i, j, k]);
674 let output_idx =
675 IxDyn(&[i - my_start_plane, j - my_start_row, k - my_start_col]);
676 let val: Complex64 =
677 NumCast::from(input[input_idx]).unwrap_or(Complex64::new(0.0, 0.0));
678 output[output_idx] = val;
679 }
680 }
681 }
682 } else {
683 let ndim = input.ndim();
686 let iter_shape: Vec<usize> = (0..ndim)
687 .map(|ax| match ax {
688 0 => actual_end_plane - my_start_plane,
689 1 => actual_end_row - my_start_row,
690 2 => actual_end_col - my_start_col,
691 _ => myshape[ax].min(max_size),
692 })
693 .collect();
694 let iter_dim = IxDyn(iter_shape.as_slice());
695 for local_idx in scirs2_core::ndarray::indices(iter_dim) {
696 let local_slice = local_idx.slice();
697 let mut global = local_slice.to_vec();
699 global[0] += my_start_plane;
700 global[1] += my_start_row;
701 global[2] += my_start_col;
702 let val: Complex64 = NumCast::from(input[IxDyn(global.as_slice())])
703 .unwrap_or(Complex64::new(0.0, 0.0));
704 output[IxDyn(local_slice)] = val;
705 }
706 }
707
708 Ok(output)
709 }
710
711 fn adaptive_decomposition<T>(
712 &self,
713 input: &ArrayD<T>,
714 is_testing: bool,
715 ) -> FFTResult<ArrayD<Complex64>>
716 where
717 T: Into<Complex64> + Copy + NumCast,
718 {
719 let ndim = input.ndim();
720
721 if ndim == 1 || self.config.node_count == 1 {
723 self.slab_decomposition(input, is_testing)
725 } else if ndim == 2 || self.config.node_count < 8 {
726 self.slab_decomposition(input, is_testing)
728 } else if ndim == 3 && self.config.node_count >= 8 {
729 let mut config = self.config.clone();
732 if config.process_grid.len() < 2 {
733 let sqrt_nodes = (self.config.node_count as f64).sqrt().floor() as usize;
734 config.process_grid = vec![sqrt_nodes, self.config.node_count / sqrt_nodes];
735 }
736
737 let temp_dfft = DistributedFFT {
739 config,
740 communicator: self.communicator.clone(),
741 };
742
743 temp_dfft.pencil_decomposition(input, is_testing)
744 } else if ndim >= 3 && self.config.node_count >= 27 {
745 let mut config = self.config.clone();
748 if config.process_grid.len() < 3 {
749 let cbrt_nodes = (self.config.node_count as f64).cbrt().floor() as usize;
750 let remaining = self.config.node_count / cbrt_nodes;
751 let sqrt_remaining = (remaining as f64).sqrt().floor() as usize;
752 config.process_grid = vec![cbrt_nodes, sqrt_remaining, remaining / sqrt_remaining];
753 }
754
755 let temp_dfft = DistributedFFT {
757 config,
758 communicator: self.communicator.clone(),
759 };
760
761 temp_dfft.volumetric_decomposition(input, is_testing)
762 } else {
763 self.slab_decomposition(input, is_testing)
765 }
766 }
767
768 #[cfg(test)]
770 pub fn new_mock(config: DistributedConfig) -> Self {
771 let communicator = Arc::new(MockCommunicator::new(config.node_count, config.rank));
772 Self {
773 config,
774 communicator,
775 }
776 }
777}
778
779#[derive(Debug)]
781pub struct BasicCommunicator {
782 size: usize,
784 rank: usize,
786}
787
788impl BasicCommunicator {
789 pub fn new(size: usize, rank: usize) -> Self {
791 Self { size, rank }
792 }
793}
794
795impl Communicator for BasicCommunicator {
796 fn send(&self, data: &[Complex64], dest: usize, tag: usize) -> FFTResult<()> {
797 let _ = tag;
798 if dest >= self.size {
799 return Err(FFTError::ValueError(format!(
800 "Invalid destination rank: {} (size: {})",
801 dest, self.size
802 )));
803 }
804 if data.is_empty() {
805 return Err(FFTError::ValueError("Cannot send empty data".to_string()));
806 }
807
808 if dest != self.rank {
813 return Err(FFTError::NotImplementedError(format!(
814 "BasicCommunicator has no inter-process transport: cannot send from \
815 rank {} to rank {}. Use a real MPI-backed communicator for \
816 multi-process runs.",
817 self.rank, dest
818 )));
819 }
820
821 Ok(())
822 }
823
824 fn recv(&self, src: usize, tag: usize, size: usize) -> FFTResult<Vec<Complex64>> {
825 let _ = (tag, size);
826 if src >= self.size {
827 return Err(FFTError::ValueError(format!(
828 "Invalid source rank: {} (size: {})",
829 src, self.size
830 )));
831 }
832
833 Err(FFTError::NotImplementedError(format!(
837 "BasicCommunicator has no inter-process transport: cannot receive on \
838 rank {} from rank {}. Use a real MPI-backed communicator for \
839 multi-process runs.",
840 self.rank, src
841 )))
842 }
843
844 fn all_to_all(&self, senddata: &[Complex64]) -> FFTResult<Vec<Complex64>> {
845 if self.size <= 1 {
849 return Ok(senddata.to_vec());
850 }
851
852 Err(FFTError::NotImplementedError(format!(
855 "BasicCommunicator has no inter-process transport: all-to-all across \
856 {} processes is unavailable. Use a real MPI-backed communicator.",
857 self.size
858 )))
859 }
860
861 fn barrier(&self) -> FFTResult<()> {
862 if self.size <= 1 {
865 Ok(())
866 } else {
867 Err(FFTError::NotImplementedError(format!(
868 "BasicCommunicator has no inter-process transport: barrier across \
869 {} processes is unavailable. Use a real MPI-backed communicator.",
870 self.size
871 )))
872 }
873 }
874
875 fn size(&self) -> usize {
876 self.size
877 }
878
879 fn rank(&self) -> usize {
880 self.rank
881 }
882}
883
884#[derive(Debug)]
886pub struct MockCommunicator {
887 size: usize,
888 rank: usize,
889}
890
891impl MockCommunicator {
892 pub fn new(size: usize, rank: usize) -> Self {
894 Self { size, rank }
895 }
896}
897
898impl Communicator for MockCommunicator {
899 fn send(&self, data: &[Complex64], dest: usize, tag: usize) -> FFTResult<()> {
900 let _ = tag; if dest >= self.size {
902 return Err(FFTError::ValueError(format!(
903 "Invalid destination rank: {} (size: {})",
904 dest, self.size
905 )));
906 }
907
908 Ok(())
910 }
911
912 fn recv(&self, src: usize, tag: usize, size: usize) -> FFTResult<Vec<Complex64>> {
913 let _ = tag; if src >= self.size {
915 return Err(FFTError::ValueError(format!(
916 "Invalid source rank: {} (size: {})",
917 src, self.size
918 )));
919 }
920
921 Ok(vec![Complex64::new(0.0, 0.0); size])
923 }
924
925 fn all_to_all(&self, senddata: &[Complex64]) -> FFTResult<Vec<Complex64>> {
926 Ok(senddata.to_vec())
928 }
929
930 fn barrier(&self) -> FFTResult<()> {
931 Ok(())
933 }
934
935 fn size(&self) -> usize {
936 self.size
937 }
938
939 fn rank(&self) -> usize {
940 self.rank
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947 use scirs2_core::ndarray::{Array1, Array2};
948
949 #[test]
950 fn test_distributed_config_default() {
951 let config = DistributedConfig::default();
952 assert_eq!(config.node_count, 1);
953 assert_eq!(config.rank, 0);
954 assert_eq!(config.decomposition, DecompositionStrategy::Slab);
955 }
956
957 #[test]
958 fn test_mock_communicator() {
959 let comm = MockCommunicator::new(4, 0);
960 assert_eq!(comm.size(), 4);
961 assert_eq!(comm.rank(), 0);
962
963 let data = vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)];
965 let result = comm.send(&data, 1, 0);
966 assert!(result.is_ok());
967
968 let result = comm.send(&data, 4, 0);
970 assert!(result.is_err());
971
972 let result = comm.recv(1, 0, 2);
974 assert!(result.is_ok());
975 assert_eq!(result.expect("Operation failed").len(), 2);
976
977 let result = comm.recv(4, 0, 2);
979 assert!(result.is_err());
980
981 let result = comm.all_to_all(&data);
983 assert!(result.is_ok());
984 assert_eq!(result.expect("Operation failed"), data);
985
986 let result = comm.barrier();
988 assert!(result.is_ok());
989 }
990
991 #[test]
992 fn test_slab_decomposition_1d() {
993 let config = DistributedConfig {
994 node_count: 2,
995 rank: 0,
996 decomposition: DecompositionStrategy::Slab,
997 communication: CommunicationPattern::AllToAll,
998 process_grid: vec![2],
999 local_size: vec![],
1000 max_local_size: 16,
1001 };
1002
1003 let dfft = DistributedFFT::new_mock(config);
1004
1005 let input = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]).into_dyn();
1006 let result = dfft.slab_decomposition(&input, true);
1007 assert!(result.is_ok());
1008
1009 let local_data = result.expect("Operation failed");
1010 assert_eq!(local_data.ndim(), 1);
1011 assert_eq!(local_data.shape()[0], 2); }
1013
1014 #[test]
1015 fn test_slab_decomposition_2d() {
1016 let config = DistributedConfig {
1017 node_count: 2,
1018 rank: 0,
1019 decomposition: DecompositionStrategy::Slab,
1020 communication: CommunicationPattern::AllToAll,
1021 process_grid: vec![2],
1022 local_size: vec![],
1023 max_local_size: 16,
1024 };
1025
1026 let dfft = DistributedFFT::new_mock(config);
1027
1028 let input = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
1029 .expect("Operation failed")
1030 .into_dyn();
1031 let result = dfft.slab_decomposition(&input, true);
1032 assert!(result.is_ok());
1033
1034 let local_data = result.expect("Operation failed");
1035 assert_eq!(local_data.ndim(), 2);
1036 assert_eq!(local_data.shape()[0], 2); assert_eq!(local_data.shape()[1], 2); }
1039
1040 #[test]
1041 fn test_pencil_decomposition_2d() {
1042 let config = DistributedConfig {
1043 node_count: 4,
1044 rank: 0,
1045 decomposition: DecompositionStrategy::Pencil,
1046 communication: CommunicationPattern::AllToAll,
1047 process_grid: vec![2, 2],
1048 local_size: vec![],
1049 max_local_size: 16,
1050 };
1051
1052 let dfft = DistributedFFT::new_mock(config);
1053
1054 let input = Array2::from_shape_vec((4, 4), (1..=16).map(|x| x as f64).collect())
1055 .expect("Operation failed")
1056 .into_dyn();
1057 let result = dfft.pencil_decomposition(&input, true);
1058 assert!(result.is_ok());
1059
1060 let local_data = result.expect("Operation failed");
1061 assert_eq!(local_data.ndim(), 2);
1062 assert_eq!(local_data.shape()[0], 2); assert_eq!(local_data.shape()[1], 2); }
1065
1066 #[test]
1067 fn test_adaptive_decomposition() {
1068 let config1 = DistributedConfig {
1070 node_count: 4,
1071 rank: 0,
1072 decomposition: DecompositionStrategy::Adaptive,
1073 communication: CommunicationPattern::AllToAll,
1074 process_grid: vec![4],
1075 local_size: vec![],
1076 max_local_size: 16,
1077 };
1078
1079 let dfft1 = DistributedFFT::new_mock(config1);
1080 let input1 = Array1::from_vec((1..=16).map(|x| x as f64).collect()).into_dyn();
1081 let result1 = dfft1.adaptive_decomposition(&input1, true);
1082 assert!(result1.is_ok());
1083
1084 let config2 = DistributedConfig {
1086 node_count: 4,
1087 rank: 0,
1088 decomposition: DecompositionStrategy::Adaptive,
1089 communication: CommunicationPattern::AllToAll,
1090 process_grid: vec![2, 2],
1091 local_size: vec![],
1092 max_local_size: 16,
1093 };
1094
1095 let dfft2 = DistributedFFT::new_mock(config2);
1096 let input2 = Array2::from_shape_vec((4, 4), (1..=16).map(|x| x as f64).collect())
1097 .expect("Operation failed")
1098 .into_dyn();
1099 let result2 = dfft2.adaptive_decomposition(&input2, true);
1100 assert!(result2.is_ok());
1101 }
1102}