1use serde::{Deserialize, Serialize};
2use std::{
3 fs,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use crate::{LadduDataError, LadduDataResult, data::EventBatch, schema::Schema};
9
10pub mod memory;
12pub mod parquet;
14pub mod root;
16
17#[cfg(feature = "mpi")]
18#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
20pub enum Distribution {
21 #[default]
23 Serial,
24 Mpi {
26 rank: usize,
28 nranks: usize,
30 partitioning: Partitioning,
32 },
33}
34
35#[cfg(feature = "mpi")]
36impl Distribution {
37 pub fn serial() -> Self {
39 Self::Serial
40 }
41
42 pub fn from_world<C>(world: &C) -> Self
44 where
45 C: mpi::topology::Communicator,
46 {
47 Self::Mpi {
48 rank: world.rank() as usize,
49 nranks: world.size() as usize,
50 partitioning: Partitioning::default(),
51 }
52 }
53
54 pub fn rank(self) -> usize {
56 match self {
57 Self::Serial => 0,
58 Self::Mpi { rank, .. } => rank,
59 }
60 }
61
62 pub fn nranks(self) -> usize {
64 match self {
65 Self::Serial => 1,
66 Self::Mpi { nranks, .. } => nranks,
67 }
68 }
69
70 pub fn partitioning(self) -> Partitioning {
72 match self {
73 Self::Serial => Partitioning::Contiguous,
74 Self::Mpi { partitioning, .. } => partitioning,
75 }
76 }
77
78 pub fn with_partitioning(self, partitioning: Partitioning) -> Self {
80 match self {
81 Self::Serial => Self::Serial,
82 Self::Mpi { rank, nranks, .. } => Self::Mpi {
83 rank,
84 nranks,
85 partitioning,
86 },
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub enum Partitioning {
94 #[default]
96 Contiguous,
97
98 FileGroups,
100
101 Rows,
104}
105
106#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
108pub struct ReadPlan {
109 pub chunk_size: Option<usize>,
111
112 #[cfg(feature = "mpi")]
113 pub distribution: Distribution,
115}
116
117impl ReadPlan {
118 pub fn serial() -> Self {
120 Self::default()
121 }
122
123 pub fn rank(&self) -> usize {
125 #[cfg(feature = "mpi")]
126 {
127 self.distribution.rank()
128 }
129
130 #[cfg(not(feature = "mpi"))]
131 {
132 0
133 }
134 }
135
136 pub fn nranks(&self) -> usize {
138 #[cfg(feature = "mpi")]
139 {
140 self.distribution.nranks()
141 }
142
143 #[cfg(not(feature = "mpi"))]
144 {
145 1
146 }
147 }
148
149 pub fn is_distributed(&self) -> bool {
151 self.nranks() > 1
152 }
153
154 pub fn fragment_partitioning(&self) -> FragmentPartitioning {
156 #[cfg(feature = "mpi")]
157 {
158 match self.distribution.partitioning() {
159 Partitioning::Contiguous => FragmentPartitioning::Contiguous,
160 Partitioning::FileGroups => FragmentPartitioning::RoundRobinFragments,
161 Partitioning::Rows => FragmentPartitioning::StridedRows,
162 }
163 }
164
165 #[cfg(not(feature = "mpi"))]
166 {
167 FragmentPartitioning::Contiguous
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
174pub struct WritePlan {
175 #[cfg(feature = "mpi")]
176 pub distribution: Distribution,
178}
179
180impl From<ReadPlan> for WritePlan {
181 #[cfg_attr(not(feature = "mpi"), allow(unused_variables))]
182 fn from(plan: ReadPlan) -> Self {
183 Self {
184 #[cfg(feature = "mpi")]
185 distribution: plan.distribution,
186 }
187 }
188}
189
190impl WritePlan {
191 pub fn rank(&self) -> usize {
193 #[cfg(feature = "mpi")]
194 {
195 self.distribution.rank()
196 }
197
198 #[cfg(not(feature = "mpi"))]
199 {
200 0
201 }
202 }
203
204 pub fn nranks(&self) -> usize {
206 #[cfg(feature = "mpi")]
207 {
208 self.distribution.nranks()
209 }
210
211 #[cfg(not(feature = "mpi"))]
212 {
213 1
214 }
215 }
216
217 pub fn is_distributed(&self) -> bool {
219 self.nranks() > 1
220 }
221}
222
223#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
225pub enum FragmentPartitioning {
226 Contiguous,
228 RoundRobinFragments,
230 StridedRows,
232}
233
234#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
236pub struct SourceCapabilities {
237 pub exact_len: bool,
239 pub exact_weighted_total: bool,
241 pub random_access: bool,
243 pub deterministic_partitioning: bool,
245 pub predicate_pushdown: bool,
247 pub projection_pushdown: bool,
249 pub streaming: bool,
251}
252
253pub type EventBatchIter = Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>;
255
256pub trait EventSource: Send + Sync {
258 fn schema(&self) -> LadduDataResult<Arc<Schema>>;
265
266 fn capabilities(&self) -> SourceCapabilities {
268 SourceCapabilities::default()
269 }
270
271 fn num_events(&self) -> LadduDataResult<Option<u64>> {
278 Ok(None)
279 }
280
281 fn weighted_total(&self) -> LadduDataResult<Option<f64>> {
288 Ok(None)
289 }
290
291 fn batches(&self, plan: ReadPlan) -> LadduDataResult<EventBatchIter>;
298}
299
300pub trait EventSink: Send {
302 fn retains_batches(&self) -> bool {
304 false
305 }
306
307 fn begin(&mut self, schema: Arc<Schema>, plan: WritePlan) -> LadduDataResult<()>;
314
315 fn write_batch(&mut self, batch: &EventBatch) -> LadduDataResult<()>;
322
323 fn finish(&mut self) -> LadduDataResult<()>;
329}
330
331#[derive(Clone, Debug)]
333pub struct DataFragment<K> {
334 pub key: K,
336 pub global_start: u64,
338 pub rows: u64,
340}
341
342#[derive(Clone, Debug)]
344pub struct FragmentRead<K> {
345 pub key: K,
347 pub selection: FragmentSelection,
349}
350
351#[derive(Clone, Copy, Debug)]
353pub enum FragmentSelection {
354 Range {
356 local_start: usize,
358 local_len: usize,
360 },
361 StridedRows {
363 global_start: u64,
365 rows: usize,
367 rank: usize,
369 nranks: usize,
371 },
372}
373
374pub trait FragmentedSource: Send + Sync {
376 type Key: Clone + Send + Sync + 'static;
378
379 fn fragments(&self) -> LadduDataResult<Vec<DataFragment<Self::Key>>>;
385
386 fn read_fragment_range(
393 &self,
394 key: &Self::Key,
395 local_start: usize,
396 local_len: usize,
397 chunk_size: Option<usize>,
398 ) -> LadduDataResult<EventBatchIter>;
399}
400
401pub fn fragmented_batches<S>(source: Arc<S>, plan: ReadPlan) -> LadduDataResult<EventBatchIter>
408where
409 S: FragmentedSource + 'static,
410{
411 let iter = FragmentBatchIter::new(source, plan)?;
412
413 if plan.chunk_size.is_none() {
414 Ok(Box::new(CoalescedBatchIter::new(iter)))
415 } else {
416 Ok(Box::new(iter))
417 }
418}
419
420pub fn plan_fragments<K: Clone>(
427 fragments: &[DataFragment<K>],
428 plan: ReadPlan,
429) -> LadduDataResult<Vec<FragmentRead<K>>> {
430 let total_rows: u64 = fragments.iter().map(|f| f.rows).sum();
431 let rank = plan.rank();
432 let nranks = plan.nranks();
433
434 if nranks == 1 {
435 return fragments
436 .iter()
437 .map(|f| {
438 Ok(FragmentRead {
439 key: f.key.clone(),
440 selection: FragmentSelection::Range {
441 local_start: 0,
442 local_len: usize_from_u64(f.rows)?,
443 },
444 })
445 })
446 .collect();
447 }
448
449 match plan.fragment_partitioning() {
450 FragmentPartitioning::Contiguous => contiguous_plan(fragments, total_rows, rank, nranks),
451 FragmentPartitioning::RoundRobinFragments => {
452 round_robin_fragment_plan(fragments, rank, nranks)
453 }
454 FragmentPartitioning::StridedRows => strided_row_plan(fragments, rank, nranks),
455 }
456}
457
458fn contiguous_plan<K: Clone>(
459 fragments: &[DataFragment<K>],
460 total_rows: u64,
461 rank: usize,
462 nranks: usize,
463) -> LadduDataResult<Vec<FragmentRead<K>>> {
464 let rank_start = total_rows * rank as u64 / nranks as u64;
465 let rank_end = total_rows * (rank as u64 + 1) / nranks as u64;
466
467 let mut out = Vec::new();
468
469 for f in fragments {
470 let frag_start = f.global_start;
471 let frag_end = f.global_start + f.rows;
472
473 let start = rank_start.max(frag_start);
474 let end = rank_end.min(frag_end);
475
476 if start < end {
477 out.push(FragmentRead {
478 key: f.key.clone(),
479 selection: FragmentSelection::Range {
480 local_start: usize_from_u64(start - frag_start)?,
481 local_len: usize_from_u64(end - start)?,
482 },
483 });
484 }
485 }
486
487 Ok(out)
488}
489
490fn round_robin_fragment_plan<K: Clone>(
491 fragments: &[DataFragment<K>],
492 rank: usize,
493 nranks: usize,
494) -> LadduDataResult<Vec<FragmentRead<K>>> {
495 let mut out = Vec::new();
496
497 for (i, f) in fragments.iter().enumerate() {
498 if i % nranks == rank {
499 out.push(FragmentRead {
500 key: f.key.clone(),
501 selection: FragmentSelection::Range {
502 local_start: 0,
503 local_len: usize_from_u64(f.rows)?,
504 },
505 });
506 }
507 }
508
509 Ok(out)
510}
511
512fn strided_row_plan<K: Clone>(
513 fragments: &[DataFragment<K>],
514 rank: usize,
515 nranks: usize,
516) -> LadduDataResult<Vec<FragmentRead<K>>> {
517 fragments
518 .iter()
519 .map(|f| {
520 Ok(FragmentRead {
521 key: f.key.clone(),
522 selection: FragmentSelection::StridedRows {
523 global_start: f.global_start,
524 rows: usize_from_u64(f.rows)?,
525 rank,
526 nranks,
527 },
528 })
529 })
530 .collect()
531}
532
533fn usize_from_u64(value: u64) -> LadduDataResult<usize> {
534 usize::try_from(value).map_err(|_| LadduDataError::InvalidArgument("row count exceeds usize"))
535}
536
537pub(crate) struct FragmentBatchIter<S>
538where
539 S: FragmentedSource,
540{
541 source: Arc<S>,
542 reads: Vec<FragmentRead<S::Key>>,
543 read_index: usize,
544 current: Option<EventBatchIter>,
545 chunk_size: Option<usize>,
546}
547
548impl<S> FragmentBatchIter<S>
549where
550 S: FragmentedSource,
551{
552 pub(crate) fn new(source: Arc<S>, plan: ReadPlan) -> LadduDataResult<Self> {
553 let fragments = source.fragments()?;
554 let reads = plan_fragments(&fragments, plan)?;
555
556 Ok(Self {
557 source,
558 reads,
559 read_index: 0,
560 current: None,
561 chunk_size: plan.chunk_size,
562 })
563 }
564}
565
566impl<S> Iterator for FragmentBatchIter<S>
567where
568 S: FragmentedSource,
569{
570 type Item = LadduDataResult<EventBatch>;
571
572 fn next(&mut self) -> Option<Self::Item> {
573 loop {
574 if let Some(iter) = self.current.as_mut() {
575 match iter.next() {
576 Some(batch) => return Some(batch),
577 None => self.current = None,
578 }
579 }
580
581 let read = self.reads.get(self.read_index)?.clone();
582 self.read_index += 1;
583
584 let next_iter = match read.selection {
585 FragmentSelection::Range {
586 local_start,
587 local_len,
588 } => self.source.read_fragment_range(
589 &read.key,
590 local_start,
591 local_len,
592 self.chunk_size,
593 ),
594
595 FragmentSelection::StridedRows {
596 global_start,
597 rows,
598 rank,
599 nranks,
600 } => {
601 let inner =
602 self.source
603 .read_fragment_range(&read.key, 0, rows, self.chunk_size);
604
605 inner.and_then(|iter| {
606 let iter = StridedRowsBatchIter::new(iter, global_start, rank, nranks)?;
607 Ok(Box::new(iter) as EventBatchIter)
608 })
609 }
610 };
611
612 match next_iter {
613 Ok(iter) => self.current = Some(iter),
614 Err(err) => return Some(Err(err)),
615 }
616 }
617 }
618}
619
620pub(crate) struct SliceBatchIter<I> {
621 inner: I,
622 start: usize,
623 end: usize,
624 consumed: usize,
625}
626
627impl<I> SliceBatchIter<I> {
628 pub(crate) fn new(inner: I, start: usize, len: usize) -> LadduDataResult<Self> {
629 let end = start
630 .checked_add(len)
631 .ok_or(LadduDataError::InvalidArgument(
632 "slice range overflows usize",
633 ))?;
634 Ok(Self {
635 inner,
636 start,
637 end,
638 consumed: 0,
639 })
640 }
641}
642
643impl<I> Iterator for SliceBatchIter<I>
644where
645 I: Iterator<Item = LadduDataResult<EventBatch>>,
646{
647 type Item = LadduDataResult<EventBatch>;
648
649 fn next(&mut self) -> Option<Self::Item> {
650 while self.consumed < self.end {
651 let batch = match self.inner.next()? {
652 Ok(batch) => batch,
653 Err(err) => return Some(Err(err)),
654 };
655
656 let batch_start = self.consumed;
657 let batch_end = batch_start + batch.len();
658 self.consumed = batch_end;
659
660 let lo = self.start.max(batch_start);
661 let hi = self.end.min(batch_end);
662
663 if lo >= hi {
664 continue;
665 }
666
667 let local_lo = lo - batch_start;
668 let local_hi = hi - batch_start;
669
670 return Some(Ok(batch.slice(local_lo, local_hi)));
671 }
672
673 None
674 }
675}
676
677pub(crate) struct StridedRowsBatchIter<I> {
678 inner: I,
679 global_start: u64,
680 consumed: u64,
681 rank: usize,
682 nranks: usize,
683}
684
685impl<I> StridedRowsBatchIter<I> {
686 pub(crate) fn new(
687 inner: I,
688 global_start: u64,
689 rank: usize,
690 nranks: usize,
691 ) -> LadduDataResult<Self> {
692 if nranks == 0 {
693 return Err(LadduDataError::InvalidArgument("nranks must be nonzero"));
694 }
695 if rank >= nranks {
696 return Err(LadduDataError::InvalidArgument(
697 "rank must be less than nranks",
698 ));
699 }
700 Ok(Self {
701 inner,
702 global_start,
703 consumed: 0,
704 rank,
705 nranks,
706 })
707 }
708}
709
710impl<I> Iterator for StridedRowsBatchIter<I>
711where
712 I: Iterator<Item = LadduDataResult<EventBatch>>,
713{
714 type Item = LadduDataResult<EventBatch>;
715
716 fn next(&mut self) -> Option<Self::Item> {
717 loop {
718 let batch = match self.inner.next()? {
719 Ok(batch) => batch,
720 Err(err) => return Some(Err(err)),
721 };
722
723 let batch_global_start = self.global_start + self.consumed;
724 self.consumed = self.consumed.saturating_add(batch.len() as u64);
725
726 let rows: Vec<usize> = (0..batch.len())
727 .filter(|&i| {
728 ((batch_global_start + i as u64) % self.nranks as u64) == self.rank as u64
729 })
730 .collect();
731
732 if rows.is_empty() {
733 continue;
734 }
735
736 return Some(Ok(batch.select(&rows)));
737 }
738 }
739}
740
741pub(crate) struct CoalescedBatchIter<I> {
742 inner: Option<I>,
743 emitted: bool,
744}
745
746impl<I> CoalescedBatchIter<I> {
747 pub(crate) fn new(inner: I) -> Self {
748 Self {
749 inner: Some(inner),
750 emitted: false,
751 }
752 }
753}
754
755impl<I> Iterator for CoalescedBatchIter<I>
756where
757 I: Iterator<Item = LadduDataResult<EventBatch>>,
758{
759 type Item = LadduDataResult<EventBatch>;
760
761 fn next(&mut self) -> Option<Self::Item> {
762 if self.emitted {
763 return None;
764 }
765
766 self.emitted = true;
767
768 let inner = self.inner.as_mut()?;
769 let mut batches = Vec::new();
770
771 for batch in inner {
772 match batch {
773 Ok(batch) => batches.push(batch),
774 Err(err) => return Some(Err(err)),
775 }
776 }
777
778 if batches.is_empty() {
779 None
780 } else {
781 Some(EventBatch::concat(&batches))
782 }
783 }
784}
785
786#[derive(Clone, Debug)]
788pub struct OutputPath {
789 base: PathBuf,
790 mode: OutputMode,
791}
792
793#[derive(Clone, Copy, Debug, Default)]
795pub enum OutputMode {
796 #[default]
798 Auto,
799 SingleFile,
801 PerRankFiles,
803}
804
805impl OutputPath {
806 pub fn new(path: impl Into<PathBuf>) -> Self {
808 Self {
809 base: path.into(),
810 mode: OutputMode::Auto,
811 }
812 }
813
814 pub fn with_mode(mut self, mode: OutputMode) -> Self {
816 self.mode = mode;
817 self
818 }
819
820 pub fn base(&self) -> &Path {
822 &self.base
823 }
824
825 pub fn mode(&self) -> OutputMode {
827 self.mode
828 }
829
830 pub fn resolve(&self, plan: WritePlan, default_extension: &str) -> LadduDataResult<PathBuf> {
837 let mode = match self.mode {
838 OutputMode::Auto if plan.is_distributed() => OutputMode::PerRankFiles,
839 OutputMode::Auto => OutputMode::SingleFile,
840 mode => mode,
841 };
842
843 match mode {
844 OutputMode::SingleFile => {
845 if plan.is_distributed() {
846 return Err(LadduDataError::Sink(
847 "single-file output is unsafe with multiple MPI ranks; use per-rank output"
848 .into(),
849 ));
850 }
851
852 Ok(self.base.clone())
853 }
854
855 OutputMode::PerRankFiles => Ok(per_rank_path(
856 &self.base,
857 plan.rank(),
858 plan.nranks(),
859 default_extension,
860 )),
861
862 OutputMode::Auto => unreachable!(),
863 }
864 }
865
866 pub fn create_parent_dirs(path: &Path) -> LadduDataResult<()> {
872 if let Some(parent) = path.parent()
873 && !parent.as_os_str().is_empty()
874 {
875 fs::create_dir_all(parent).map_err(|e| LadduDataError::Sink(e.to_string()))?;
876 }
877
878 Ok(())
879 }
880}
881
882fn per_rank_path(base: &Path, rank: usize, nranks: usize, default_extension: &str) -> PathBuf {
883 if base.extension().is_none() {
884 let ext = default_extension.trim_start_matches('.');
885 return base.join(format!("part-rank{rank:05}-of{nranks:05}.{ext}"));
886 }
887
888 let parent = base.parent().unwrap_or_else(|| Path::new(""));
889 let stem = base.file_stem().unwrap_or_default().to_string_lossy();
890 let ext = base.extension().unwrap_or_default().to_string_lossy();
891
892 parent.join(format!("{stem}.rank{rank:05}-of{nranks:05}.{ext}"))
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898 use crate::{
899 data::{EventBatch, EventBatchBuilder},
900 schema::Schema,
901 };
902
903 fn v(x: f64) -> RealVec4 {
904 RealVec4 {
905 e: x,
906 px: x,
907 py: x,
908 pz: x,
909 }
910 }
911
912 fn schema() -> Arc<Schema> {
913 Arc::new(Schema::new(["p"], ["id"], true).unwrap())
914 }
915
916 fn batch(start: usize, len: usize) -> EventBatch {
917 let schema = schema();
918 let mut builder = EventBatchBuilder::with_capacity(schema, len);
919
920 for i in start..start + len {
921 builder
922 .push_weighted([v(i as f64)], [i as f64], 100.0 + i as f64)
923 .unwrap();
924 }
925
926 builder.finish().unwrap()
927 }
928
929 fn concat_values(batches: Vec<EventBatch>) -> Vec<f64> {
930 EventBatch::concat(&batches)
931 .unwrap()
932 .scalar_column(0)
933 .to_vec()
934 }
935
936 #[test]
937 fn slice_batch_iter_slices_across_batch_boundaries_without_losing_alignment() {
938 let inner = vec![Ok(batch(0, 3)), Ok(batch(3, 2)), Ok(batch(5, 4))].into_iter();
939
940 let out: Vec<EventBatch> = SliceBatchIter::new(inner, 2, 5)
941 .unwrap()
942 .map(Result::unwrap)
943 .collect();
944
945 let values = concat_values(out);
946 assert_eq!(values, vec![2.0, 3.0, 4.0, 5.0, 6.0]);
947 }
948
949 #[test]
950 fn strided_rows_batch_iter_uses_global_row_numbers_across_batches() {
951 let inner = vec![Ok(batch(0, 4)), Ok(batch(4, 5))].into_iter();
952
953 let out: Vec<EventBatch> = StridedRowsBatchIter::new(inner, 1, 1, 3)
954 .unwrap()
955 .map(Result::unwrap)
956 .collect();
957
958 assert_eq!(concat_values(out), vec![0.0, 3.0, 6.0]);
962 }
963
964 #[test]
965 fn coalesced_batch_iter_concatenates_successes_and_propagates_first_error() {
966 let success_inner = vec![Ok(batch(0, 2)), Ok(batch(2, 3))].into_iter();
967 let mut success = CoalescedBatchIter::new(success_inner);
968
969 let merged = success.next().unwrap().unwrap();
970 assert_eq!(merged.scalar_column(0), &[0.0, 1.0, 2.0, 3.0, 4.0]);
971 assert!(success.next().is_none());
972
973 let error_inner = vec![
974 Ok(batch(0, 1)),
975 Err(LadduDataError::Source("boom".into())),
976 Ok(batch(1, 1)),
977 ]
978 .into_iter();
979
980 let err = CoalescedBatchIter::new(error_inner)
981 .next()
982 .unwrap()
983 .unwrap_err();
984
985 assert!(matches!(err, LadduDataError::Source(msg) if msg == "boom"));
986 }
987
988 #[test]
989 fn output_path_resolves_single_file_and_per_rank_names() {
990 let plan = WritePlan::default();
991
992 let single = OutputPath::new(PathBuf::from("events.parquet"))
993 .resolve(plan, "parquet")
994 .unwrap();
995
996 assert_eq!(single, PathBuf::from("events.parquet"));
997
998 let per_rank_with_extension = OutputPath::new(PathBuf::from("events.parquet"))
999 .with_mode(OutputMode::PerRankFiles)
1000 .resolve(plan, "parquet")
1001 .unwrap();
1002
1003 assert_eq!(
1004 per_rank_with_extension,
1005 PathBuf::from("events.rank00000-of00001.parquet")
1006 );
1007
1008 let per_rank_without_extension = OutputPath::new(PathBuf::from("events"))
1009 .with_mode(OutputMode::PerRankFiles)
1010 .resolve(plan, "root")
1011 .unwrap();
1012
1013 assert_eq!(
1014 per_rank_without_extension,
1015 PathBuf::from("events").join("part-rank00000-of00001.root")
1016 );
1017 }
1018
1019 #[test]
1020 fn plan_fragments_serial_mode_keeps_all_fragments_in_order() {
1021 let fragments = vec![
1022 DataFragment {
1023 key: "a",
1024 global_start: 0,
1025 rows: 2,
1026 },
1027 DataFragment {
1028 key: "b",
1029 global_start: 2,
1030 rows: 3,
1031 },
1032 ];
1033
1034 let reads = plan_fragments(&fragments, ReadPlan::default()).unwrap();
1035
1036 assert_eq!(reads.len(), 2);
1037
1038 match &reads[0].selection {
1039 FragmentSelection::Range {
1040 local_start,
1041 local_len,
1042 } => {
1043 assert_eq!((*local_start, *local_len), (0, 2));
1044 }
1045 _ => panic!("expected range read"),
1046 }
1047
1048 match &reads[1].selection {
1049 FragmentSelection::Range {
1050 local_start,
1051 local_len,
1052 } => {
1053 assert_eq!((*local_start, *local_len), (0, 3));
1054 }
1055 _ => panic!("expected range read"),
1056 }
1057 }
1058
1059 use laddu_physics::vectors::RealVec4;
1060 #[cfg(feature = "mpi")]
1061 use mpi::traits::*;
1062 #[cfg(feature = "mpi")]
1063 use mpi_test::mpi_test;
1064
1065 #[cfg(feature = "mpi")]
1066 fn distributed_plan(
1067 partitioning: Partitioning,
1068 world: &impl mpi::topology::Communicator,
1069 ) -> ReadPlan {
1070 ReadPlan {
1071 chunk_size: None,
1072 distribution: Distribution::from_world(world).with_partitioning(partitioning),
1073 }
1074 }
1075
1076 #[cfg(feature = "mpi")]
1077 fn expected_contiguous_global_range(total_rows: u64, rank: usize, nranks: usize) -> (u64, u64) {
1078 let start = total_rows * rank as u64 / nranks as u64;
1079 let end = total_rows * (rank as u64 + 1) / nranks as u64;
1080 (start, end)
1081 }
1082
1083 #[cfg(feature = "mpi")]
1084 #[mpi_test(np = [2, 3, 4])]
1085 fn mpi_contiguous_plan_assigns_disjoint_ranges_covering_all_rows() {
1086 let universe = mpi::initialize().unwrap();
1087 let world = universe.world();
1088
1089 let rank = world.rank() as usize;
1090 let nranks = world.size() as usize;
1091
1092 let fragments = vec![
1093 DataFragment {
1094 key: "a",
1095 global_start: 0,
1096 rows: 4,
1097 },
1098 DataFragment {
1099 key: "b",
1100 global_start: 4,
1101 rows: 5,
1102 },
1103 DataFragment {
1104 key: "c",
1105 global_start: 9,
1106 rows: 3,
1107 },
1108 ];
1109
1110 let total_rows = fragments.iter().map(|f| f.rows).sum::<u64>();
1111 let plan = distributed_plan(Partitioning::Contiguous, &world);
1112 let reads = plan_fragments(&fragments, plan).unwrap();
1113
1114 let assigned_rows: u64 = reads
1115 .iter()
1116 .map(|read| match read.selection {
1117 FragmentSelection::Range { local_len, .. } => local_len as u64,
1118 FragmentSelection::StridedRows { .. } => panic!("expected range selection"),
1119 })
1120 .sum();
1121
1122 let (expected_start, expected_end) =
1123 expected_contiguous_global_range(total_rows, rank, nranks);
1124
1125 assert_eq!(assigned_rows, expected_end - expected_start);
1126
1127 for read in reads {
1128 let fragment = fragments
1129 .iter()
1130 .find(|fragment| fragment.key == read.key)
1131 .unwrap();
1132
1133 match read.selection {
1134 FragmentSelection::Range {
1135 local_start,
1136 local_len,
1137 } => {
1138 let global_start = fragment.global_start + local_start as u64;
1139 let global_end = global_start + local_len as u64;
1140
1141 assert!(expected_start <= global_start);
1142 assert!(global_end <= expected_end);
1143 assert!(fragment.global_start <= global_start);
1144 assert!(global_end <= fragment.global_start + fragment.rows);
1145 }
1146 FragmentSelection::StridedRows { .. } => panic!("expected range selection"),
1147 }
1148 }
1149 }
1150
1151 #[cfg(feature = "mpi")]
1152 #[mpi_test(np = [2, 3])]
1153 fn mpi_file_group_plan_assigns_fragment_by_rank_round_robin() {
1154 let universe = mpi::initialize().unwrap();
1155 let world = universe.world();
1156
1157 let rank = world.rank() as usize;
1158 let nranks = world.size() as usize;
1159
1160 let fragments = (0..8)
1161 .map(|i| DataFragment {
1162 key: i,
1163 global_start: 10 * i as u64,
1164 rows: 10,
1165 })
1166 .collect::<Vec<_>>();
1167
1168 let plan = distributed_plan(Partitioning::FileGroups, &world);
1169 let reads = plan_fragments(&fragments, plan).unwrap();
1170
1171 let keys = reads.iter().map(|read| read.key).collect::<Vec<_>>();
1172 let expected = (0..8).filter(|i| i % nranks == rank).collect::<Vec<_>>();
1173
1174 assert_eq!(keys, expected);
1175
1176 for read in reads {
1177 match read.selection {
1178 FragmentSelection::Range {
1179 local_start,
1180 local_len,
1181 } => {
1182 assert_eq!(local_start, 0);
1183 assert_eq!(local_len, 10);
1184 }
1185 FragmentSelection::StridedRows { .. } => panic!("expected range selection"),
1186 }
1187 }
1188 }
1189
1190 #[cfg(feature = "mpi")]
1191 #[mpi_test(np = [2, 3, 4])]
1192 fn mpi_rows_plan_assigns_strided_row_selection_with_world_rank() {
1193 let universe = mpi::initialize().unwrap();
1194 let world = universe.world();
1195
1196 let rank = world.rank() as usize;
1197 let nranks = world.size() as usize;
1198
1199 let fragments = vec![
1200 DataFragment {
1201 key: "a",
1202 global_start: 0,
1203 rows: 4,
1204 },
1205 DataFragment {
1206 key: "b",
1207 global_start: 4,
1208 rows: 5,
1209 },
1210 ];
1211
1212 let plan = distributed_plan(Partitioning::Rows, &world);
1213 let reads = plan_fragments(&fragments, plan).unwrap();
1214
1215 assert_eq!(reads.len(), fragments.len());
1216
1217 for (read, fragment) in reads.iter().zip(fragments.iter()) {
1218 assert_eq!(read.key, fragment.key);
1219
1220 match read.selection {
1221 FragmentSelection::StridedRows {
1222 global_start,
1223 rows,
1224 rank: selected_rank,
1225 nranks: selected_nranks,
1226 } => {
1227 assert_eq!(global_start, fragment.global_start);
1228 assert_eq!(rows, fragment.rows as usize);
1229 assert_eq!(selected_rank, rank);
1230 assert_eq!(selected_nranks, nranks);
1231 }
1232 FragmentSelection::Range { .. } => panic!("expected strided selection"),
1233 }
1234 }
1235 }
1236
1237 #[cfg(feature = "mpi")]
1238 #[mpi_test(np = [2, 3])]
1239 fn mpi_read_plan_and_write_plan_reflect_world_distribution() {
1240 let universe = mpi::initialize().unwrap();
1241 let world = universe.world();
1242
1243 let read_plan = ReadPlan {
1244 chunk_size: Some(7),
1245 distribution: Distribution::from_world(&world).with_partitioning(Partitioning::Rows),
1246 };
1247
1248 assert!(read_plan.is_distributed());
1249 assert_eq!(read_plan.rank(), world.rank() as usize);
1250 assert_eq!(read_plan.nranks(), world.size() as usize);
1251
1252 match read_plan.fragment_partitioning() {
1253 FragmentPartitioning::StridedRows => {}
1254 _ => panic!("expected strided row partitioning"),
1255 }
1256
1257 let write_plan = WritePlan::from(read_plan);
1258
1259 assert!(write_plan.is_distributed());
1260 assert_eq!(write_plan.rank(), world.rank() as usize);
1261 assert_eq!(write_plan.nranks(), world.size() as usize);
1262 }
1263}