1use std::sync::{
2 Arc, Mutex,
3 atomic::{AtomicU64, Ordering},
4};
5
6use crate::{
7 LadduDataError, LadduDataResult,
8 data::event::{Event, EventBatch, OwnedEvent},
9 io::{EventSink, EventSource, ReadPlan, SourceCapabilities, WritePlan, memory::MemorySource},
10 schema::Schema,
11};
12use laddu_memory::{MemoryBudget, MemoryDecision};
13use num::complex::Complex64;
14
15#[cfg(feature = "parallel")]
16pub mod accurate;
17mod execution;
18mod ops;
19
20use execution::{DatasetExecutionPlan, DatasetExecutor, visit_events};
21use ops::DatasetOp;
22#[cfg(test)]
23use ops::{poisson1_from_hash, uniform_hash_01};
24
25static NEXT_DATASET_IDENTITY: AtomicU64 = AtomicU64::new(1);
26
27fn next_dataset_identity() -> u64 {
28 NEXT_DATASET_IDENTITY.fetch_add(1, Ordering::Relaxed)
29}
30
31#[derive(Copy, Clone, Debug, PartialEq)]
33pub struct DatasetStats {
34 events: u64,
35 sum_weights: f64,
36}
37
38impl DatasetStats {
39 pub fn events(&self) -> u64 {
41 self.events
42 }
43
44 pub fn sum_weights(&self) -> f64 {
46 self.sum_weights
47 }
48}
49
50#[derive(Default)]
51struct DatasetStatsCache {
52 events: Option<u64>,
53 sum_weights: Option<f64>,
54}
55
56#[derive(Clone)]
58pub struct Dataset {
59 identity: u64,
60 source: Arc<dyn EventSource>,
61 plan: ReadPlan,
62 ops: Arc<[DatasetOp]>,
63 cache_storage: CacheStorage,
64 memory_policy: MemoryPolicy,
65 memory_budget: MemoryBudget,
66 last_memory_decision: Arc<Mutex<Option<MemoryDecision>>>,
67 stats: Arc<Mutex<DatasetStatsCache>>,
68 source_traversals: Arc<AtomicU64>,
69}
70
71#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
73pub enum CacheStorage {
74 #[default]
76 Resident,
77 Streaming,
79}
80
81#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
83pub enum MemoryPolicy {
84 #[default]
86 Fastest,
87 Resident,
89 Streaming,
91}
92
93impl Dataset {
94 pub fn new<S>(source: S) -> Self
96 where
97 S: EventSource + 'static,
98 {
99 Self {
100 identity: next_dataset_identity(),
101 source: Arc::new(source),
102 plan: ReadPlan::default(),
103 ops: Arc::from([]),
104 cache_storage: CacheStorage::Resident,
105 memory_policy: MemoryPolicy::Fastest,
106 memory_budget: MemoryBudget::Auto,
107 last_memory_decision: Default::default(),
108 stats: Default::default(),
109 source_traversals: Default::default(),
110 }
111 }
112
113 pub fn from_arc(source: Arc<dyn EventSource>) -> Self {
115 Self {
116 identity: next_dataset_identity(),
117 source,
118 plan: ReadPlan::default(),
119 ops: Arc::from([]),
120 cache_storage: CacheStorage::Resident,
121 memory_policy: MemoryPolicy::Fastest,
122 memory_budget: MemoryBudget::Auto,
123 last_memory_decision: Default::default(),
124 stats: Default::default(),
125 source_traversals: Default::default(),
126 }
127 }
128
129 pub fn with_derived_source<S>(&self, source: S) -> Self
134 where
135 S: EventSource + 'static,
136 {
137 Self {
138 identity: next_dataset_identity(),
139 source: Arc::new(source),
140 plan: self.plan,
141 ops: Arc::from([]),
142 cache_storage: self.cache_storage,
143 memory_policy: self.memory_policy,
144 memory_budget: self.memory_budget,
145 last_memory_decision: Default::default(),
146 stats: Default::default(),
147 source_traversals: Default::default(),
148 }
149 }
150
151 pub fn empty_derived(&self) -> LadduDataResult<Self> {
157 Ok(self.with_derived_source(MemorySource::empty(self.schema()?)))
158 }
159
160 pub fn from_batch(batch: EventBatch) -> Self {
162 Self::new(MemorySource::new(batch))
163 }
164
165 pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
171 Ok(Self::new(MemorySource::from_batches(batches)?))
172 }
173
174 pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
180 where
181 I: IntoIterator<Item = OwnedEvent>,
182 {
183 Ok(Self::new(MemorySource::from_events(schema, events)?))
184 }
185
186 pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
193 self.source.schema()
194 }
195
196 pub fn capabilities(&self) -> SourceCapabilities {
198 self.source.capabilities()
199 }
200
201 pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
207 {
208 let stats = self.stats.lock().unwrap_or_else(|error| error.into_inner());
209 if let Some(events) = stats.events {
210 return Ok(Some(events));
211 }
212 }
213
214 if self
215 .ops
216 .iter()
217 .any(|op| !matches!(op, DatasetOp::Bootstrap { .. }))
218 {
219 return Ok(None);
220 }
221
222 let events = self.source.num_events()?;
223 if let Some(events) = events {
224 self.stats
225 .lock()
226 .unwrap_or_else(|error| error.into_inner())
227 .events = Some(events);
228 }
229 Ok(events)
230 }
231
232 pub fn stats(&self) -> LadduDataResult<DatasetStats> {
241 {
242 let cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
243 if let (Some(events), Some(sum_weights)) = (cache.events, cache.sum_weights) {
244 return Ok(DatasetStats {
245 events,
246 sum_weights,
247 });
248 }
249 }
250
251 if self.ops.is_empty()
252 && let (Some(events), Some(sum_weights)) =
253 (self.source.num_events()?, self.source.weighted_total()?)
254 {
255 let stats = DatasetStats {
256 events,
257 sum_weights,
258 };
259 let mut cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
260 cache.events = Some(events);
261 cache.sum_weights = Some(sum_weights);
262 return Ok(stats);
263 }
264
265 let mut executor = self.executor_with_plan(self.plan)?;
266 for batch in &mut executor {
267 batch?;
268 }
269
270 Ok(executor.stats())
271 }
272
273 pub fn read_plan(&self) -> ReadPlan {
275 self.plan
276 }
277
278 pub fn cache_storage(&self) -> CacheStorage {
280 self.cache_storage
281 }
282
283 pub fn memory_policy(&self) -> MemoryPolicy {
285 self.memory_policy
286 }
287
288 pub fn memory_budget(&self) -> MemoryBudget {
290 self.memory_budget
291 }
292
293 pub fn last_memory_decision(&self) -> Option<MemoryDecision> {
295 self.last_memory_decision
296 .lock()
297 .unwrap_or_else(|error| error.into_inner())
298 .clone()
299 }
300
301 pub fn source_traversals(&self) -> u64 {
303 self.source_traversals.load(Ordering::Relaxed)
304 }
305
306 #[doc(hidden)]
311 pub fn identity(&self) -> u64 {
312 self.identity
313 }
314
315 pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
317 self.memory_budget = budget;
318 self
319 }
320
321 pub fn fastest(mut self) -> Self {
323 self.memory_policy = MemoryPolicy::Fastest;
324 self.cache_storage = CacheStorage::Resident;
325 self
326 }
327
328 pub fn resident(mut self) -> Self {
334 self.memory_policy = MemoryPolicy::Resident;
335 self.cache_storage = CacheStorage::Resident;
336 self
337 }
338
339 pub fn streaming(mut self) -> Self {
344 self.memory_policy = MemoryPolicy::Streaming;
345 self.cache_storage = CacheStorage::Streaming;
346 self
347 }
348
349 pub fn chunked(mut self, chunk_size: usize) -> LadduDataResult<Self> {
359 if chunk_size == 0 {
360 return Err(LadduDataError::InvalidArgument(
361 "chunk_size must be nonzero",
362 ));
363 }
364 self.plan.chunk_size = Some(chunk_size);
365 Ok(self)
366 }
367
368 pub fn unchunked(mut self) -> Self {
370 self.plan.chunk_size = None;
371 self
372 }
373
374 pub fn filter<F>(self, f: F) -> Self
376 where
377 F: Fn(Event<'_>) -> bool + Send + Sync + 'static,
378 {
379 self.push_op(DatasetOp::Filter(Arc::new(f)))
380 }
381
382 pub fn subsample(self, fraction: f64, seed: u64) -> LadduDataResult<Self> {
389 if !(0.0..=1.0).contains(&fraction) {
390 return Err(LadduDataError::InvalidArgument(
391 "fraction must be in [0, 1]",
392 ));
393 }
394
395 Ok(self.push_op(DatasetOp::Subsample { fraction, seed }))
396 }
397
398 pub fn bootstrap(self, seed: u64) -> Self {
400 self.push_op(DatasetOp::Bootstrap { seed })
401 }
402
403 pub fn for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
410 where
411 F: FnMut(Event<'_>),
412 {
413 self.try_for_each_event(|ev| {
414 f(ev);
415 Ok(())
416 })
417 }
418
419 pub fn try_for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
426 where
427 F: FnMut(Event<'_>) -> LadduDataResult<()>,
428 {
429 visit_events(
430 self,
431 DatasetExecutionPlan::resolve(self, self.plan)?,
432 &mut f,
433 )
434 }
435
436 pub fn try_map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
443 where
444 F: FnMut(Event<'_>) -> LadduDataResult<T>,
445 {
446 let mut out = Vec::new();
447 self.try_for_each_event(|ev| {
448 out.push(f(ev)?);
449 Ok(())
450 })?;
451
452 Ok(out)
453 }
454
455 pub fn map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
462 where
463 F: FnMut(Event<'_>) -> T,
464 {
465 let mut out = Vec::new();
466 self.try_for_each_event(|ev| {
467 out.push(f(ev));
468 Ok(())
469 })?;
470
471 Ok(out)
472 }
473
474 pub fn try_fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
481 where
482 F: FnMut(T, Event<'_>) -> LadduDataResult<T>,
483 {
484 let mut acc = Some(init);
485
486 self.try_for_each_event(|ev| {
487 let current = acc.take().ok_or_else(|| {
488 LadduDataError::Source("dataset fold accumulator was consumed".into())
489 })?;
490 acc = Some(f(current, ev)?);
491 Ok(())
492 })?;
493
494 acc.ok_or_else(|| LadduDataError::Source("dataset fold produced no accumulator".into()))
495 }
496
497 pub fn fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
504 where
505 F: FnMut(T, Event<'_>) -> T,
506 {
507 self.try_fold_events(init, |acc, ev| Ok(f(acc, ev)))
508 }
509
510 pub fn try_fold_batches<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
519 where
520 F: FnMut(T, EventBatch) -> LadduDataResult<T>,
521 {
522 let mut acc = init;
523 for batch in self.batches()? {
524 acc = f(acc, batch?)?;
525 }
526 Ok(acc)
527 }
528
529 pub fn try_accumulate_events<T, F>(&self, mut acc: T, mut f: F) -> LadduDataResult<T>
536 where
537 F: FnMut(&mut T, Event<'_>) -> LadduDataResult<()>,
538 {
539 self.try_for_each_event(|ev| f(&mut acc, ev))?;
540 Ok(acc)
541 }
542
543 pub fn accumulate_events<T, F>(&self, acc: T, mut f: F) -> LadduDataResult<T>
550 where
551 F: FnMut(&mut T, Event<'_>),
552 {
553 self.try_accumulate_events(acc, |acc, ev| {
554 f(acc, ev);
555 Ok(())
556 })
557 }
558
559 pub fn sum_weights(&self) -> LadduDataResult<f64> {
566 Ok(self.stats()?.sum_weights())
567 }
568
569 pub fn weighted_sum<F>(&self, mut f: F) -> LadduDataResult<f64>
576 where
577 F: FnMut(Event<'_>) -> f64,
578 {
579 self.fold_events(0.0, |sum, ev| sum + ev.weight() * f(ev))
580 }
581
582 pub fn weighted_complex_sum<F>(&self, mut f: F) -> LadduDataResult<Complex64>
589 where
590 F: FnMut(Event<'_>) -> Complex64,
591 {
592 self.fold_events(0.0.into(), |sum, ev| sum + ev.weight() * f(ev))
593 }
594
595 pub fn batches(
602 &self,
603 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
604 self.stream_with_plan(self.plan)
605 }
606
607 #[doc(hidden)]
608 pub fn stream_with_plan(
615 &self,
616 plan: ReadPlan,
617 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
618 Ok(Box::new(self.executor_with_plan(plan)?))
619 }
620
621 #[doc(hidden)]
622 pub fn batches_with_plan(
624 &self,
625 plan: ReadPlan,
626 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
627 self.stream_with_plan(plan)
628 }
629
630 fn executor_with_plan(&self, plan: ReadPlan) -> LadduDataResult<DatasetExecutor> {
631 DatasetExecutor::new(self, DatasetExecutionPlan::resolve(self, plan)?)
632 }
633
634 pub fn try_for_each_batch<F>(&self, mut f: F) -> LadduDataResult<()>
641 where
642 F: FnMut(EventBatch) -> LadduDataResult<()>,
643 {
644 for batch in self.batches()? {
645 f(batch?)?;
646 }
647
648 Ok(())
649 }
650
651 pub fn map_batches<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
657 where
658 F: FnMut(EventBatch) -> T,
659 {
660 let mut out = Vec::new();
661
662 self.try_for_each_batch(|batch| {
663 out.push(f(batch));
664 Ok(())
665 })?;
666
667 Ok(out)
668 }
669
670 pub fn write_to<S: EventSink>(&self, sink: &mut S) -> LadduDataResult<()> {
677 sink.begin(self.schema()?, WritePlan::from(self.plan))?;
678
679 let result = (|| {
680 for batch in self.batches()? {
681 sink.write_batch(&batch?)?;
682 }
683
684 sink.finish()
685 })();
686
687 if result.is_err() {
688 let _ = sink.abort();
691 }
692
693 result
694 }
695
696 fn push_op(self, op: DatasetOp) -> Self {
697 let preserved_events = if matches!(&op, DatasetOp::Bootstrap { .. }) {
698 self.num_events().ok().flatten()
699 } else {
700 None
701 };
702 let mut ops = self.ops.to_vec();
703 ops.push(op);
704
705 Self {
706 identity: next_dataset_identity(),
707 source: self.source,
708 plan: self.plan,
709 ops: ops.into(),
710 cache_storage: self.cache_storage,
711 memory_policy: self.memory_policy,
712 memory_budget: self.memory_budget,
713 last_memory_decision: Default::default(),
714 stats: Arc::new(Mutex::new(DatasetStatsCache {
715 events: preserved_events,
716 sum_weights: None,
717 })),
718 source_traversals: Default::default(),
719 }
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::ops::materialize_batch;
726 use super::*;
727 use crate::io::{EventBatchIter, EventSource, ReadPlan, memory::MemorySink};
728 use laddu_physics::vectors::RealVec4;
729 use std::sync::atomic::{AtomicUsize, Ordering};
730
731 #[derive(Clone)]
732 struct CountingSource {
733 batch: EventBatch,
734 reads: Arc<AtomicUsize>,
735 }
736
737 impl EventSource for CountingSource {
738 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
739 Ok(Arc::clone(self.batch.schema()))
740 }
741
742 fn batches(
743 &self,
744 _plan: ReadPlan,
745 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
746 self.reads.fetch_add(1, Ordering::Relaxed);
747 Ok(Box::new(std::iter::once(Ok(self.batch.clone()))))
748 }
749 }
750
751 #[derive(Clone)]
752 struct ErrorSource {
753 schema: Arc<Schema>,
754 items: Arc<[LadduDataResult<EventBatch>]>,
755 }
756
757 impl EventSource for ErrorSource {
758 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
759 Ok(Arc::clone(&self.schema))
760 }
761
762 fn batches(&self, _plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
763 let items = Arc::clone(&self.items);
764 Ok(Box::new(
765 (0..items.len()).map(move |index| items[index].clone()),
766 ))
767 }
768 }
769
770 fn v(x: f64) -> RealVec4 {
771 RealVec4 {
772 e: x + 0.3,
773 px: x,
774 py: x + 0.1,
775 pz: x + 0.2,
776 }
777 }
778
779 fn schema_with_weight() -> Arc<Schema> {
780 Arc::new(Schema::new(["p"], ["x"], true).unwrap())
781 }
782
783 fn schema_without_weight() -> Arc<Schema> {
784 Arc::new(Schema::new(["p"], ["x"], false).unwrap())
785 }
786
787 fn weighted_batch(start: usize, len: usize) -> EventBatch {
788 let schema = schema_with_weight();
789
790 let events = (start..start + len)
791 .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
792
793 EventBatch::from_events(schema, events).unwrap()
794 }
795
796 fn unweighted_batch(start: usize, len: usize) -> EventBatch {
797 let schema = schema_without_weight();
798
799 let events =
800 (start..start + len).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64]));
801
802 EventBatch::from_events(schema, events).unwrap()
803 }
804
805 fn error_source(error_after: Option<EventBatch>) -> ErrorSource {
806 let schema = schema_with_weight();
807 let mut items = Vec::new();
808 if let Some(batch) = error_after {
809 items.push(Ok(batch));
810 }
811 items.push(Err(LadduDataError::Unsupported("source")));
812 ErrorSource {
813 schema,
814 items: items.into(),
815 }
816 }
817
818 fn scalar_values(batch: &EventBatch) -> Vec<f64> {
819 batch.scalar_column(0).to_vec()
820 }
821
822 #[test]
823 fn dataset_statistics_are_shared_per_view_and_invalidated_by_selection() {
824 let reads = Arc::new(AtomicUsize::new(0));
825 let dataset = Dataset::new(CountingSource {
826 batch: weighted_batch(0, 5),
827 reads: Arc::clone(&reads),
828 });
829 let clone = dataset.clone();
830
831 assert_eq!(dataset.num_events().unwrap(), None);
832 assert_eq!(dataset.stats().unwrap().events(), 5);
833 assert_eq!(clone.sum_weights().unwrap(), 60.0);
834 assert_eq!(clone.num_events().unwrap(), Some(5));
835 assert_eq!(reads.load(Ordering::Relaxed), 1);
836
837 let bootstrapped = dataset.clone().bootstrap(7);
838 assert_eq!(bootstrapped.num_events().unwrap(), Some(5));
839 assert_eq!(reads.load(Ordering::Relaxed), 1);
840
841 let filtered = dataset.filter(|event| event.scalar(0) >= 2.0);
842 assert_eq!(filtered.num_events().unwrap(), None);
843 assert_eq!(
844 filtered.map_events(|event| event.scalar(0)).unwrap(),
845 [2.0, 3.0, 4.0]
846 );
847 assert_eq!(reads.load(Ordering::Relaxed), 2);
848 assert_eq!(filtered.stats().unwrap().events(), 3);
849 assert_eq!(reads.load(Ordering::Relaxed), 2);
850 }
851
852 #[test]
853 fn transformed_fragments_are_coalesced_to_the_read_chunk_size() {
854 let fragments = (0..10)
855 .map(|index| weighted_batch(index, 1))
856 .collect::<Vec<_>>();
857 let dataset = Dataset::from_batches(fragments)
858 .unwrap()
859 .filter(|_| true)
860 .chunked(4)
861 .unwrap();
862
863 let batches = dataset
864 .batches()
865 .unwrap()
866 .collect::<LadduDataResult<Vec<_>>>()
867 .unwrap();
868 assert_eq!(
869 batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
870 [4, 4, 2]
871 );
872 assert_eq!(
873 batches
874 .iter()
875 .flat_map(|batch| batch.scalar_column(0).iter().copied())
876 .collect::<Vec<_>>(),
877 (0..10).map(|value| value as f64).collect::<Vec<_>>()
878 );
879 }
880
881 #[test]
882 fn shared_stream_preserves_pending_batches_before_source_errors() {
883 let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
884 .chunked(4)
885 .unwrap();
886 let mut batches = dataset.batches().unwrap();
887
888 assert_eq!(batches.next().unwrap().unwrap().len(), 2);
889 assert!(matches!(
890 batches.next().unwrap(),
891 Err(LadduDataError::Unsupported("source"))
892 ));
893 assert!(batches.next().is_none());
894
895 assert!(matches!(
896 dataset.stats(),
897 Err(LadduDataError::Unsupported("source"))
898 ));
899 assert_eq!(dataset.source_traversals(), 2);
900 }
901
902 #[test]
903 fn event_visitors_share_the_execution_plan_without_changing_source_rows() {
904 let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
905 .chunked(4)
906 .unwrap()
907 .filter(|event| event.scalar(0) >= 0.0);
908 let mut rows = Vec::new();
909
910 let error = dataset
911 .try_for_each_event(|event| {
912 rows.push(event.row());
913 Ok(())
914 })
915 .unwrap_err();
916
917 assert!(matches!(error, LadduDataError::Unsupported("source")));
918 assert_eq!(rows, [0, 1]);
919 }
920
921 #[test]
922 fn dataset_map_fold_accumulate_complex_sum_and_error_paths_use_transformed_events() {
923 let dataset =
924 Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) % 2.0 == 0.0);
925
926 let rows = dataset
927 .map_events(|ev| (ev.row(), ev.scalar(0), ev.weight()))
928 .unwrap();
929
930 assert_eq!(rows, vec![(0, 0.0, 10.0), (2, 2.0, 12.0), (4, 4.0, 14.0)]);
931
932 let folded = dataset
933 .fold_events(String::new(), |mut out, ev| {
934 out.push_str(&format!("{};", ev.scalar(0)));
935 out
936 })
937 .unwrap();
938
939 assert_eq!(folded, "0;2;4;");
940
941 let accumulated = dataset
942 .accumulate_events(Vec::<f64>::new(), |values, ev| values.push(ev.weight()))
943 .unwrap();
944
945 assert_eq!(accumulated, vec![10.0, 12.0, 14.0]);
946
947 let weighted_sum = dataset.weighted_sum(|ev| ev.scalar(0)).unwrap();
948 assert_eq!(weighted_sum, 0.0 * 10.0 + 2.0 * 12.0 + 4.0 * 14.0);
949
950 let complex_sum = dataset
951 .weighted_complex_sum(|ev| Complex64::new(ev.scalar(0), 1.0))
952 .unwrap();
953
954 assert_eq!(complex_sum.re, weighted_sum);
955 assert_eq!(complex_sum.im, 10.0 + 12.0 + 14.0);
956
957 let err = dataset
958 .try_map_events(|ev| {
959 if ev.scalar(0) == 2.0 {
960 Err(LadduDataError::Unsupported("stop"))
961 } else {
962 Ok(ev.scalar(0))
963 }
964 })
965 .unwrap_err();
966
967 assert!(matches!(err, LadduDataError::Unsupported("stop")));
968 }
969
970 #[test]
971 fn batch_folds_and_empty_derived_sources_preserve_schema_and_errors() {
972 let dataset = Dataset::from_batches(vec![weighted_batch(0, 2), weighted_batch(2, 2)])
973 .unwrap()
974 .chunked(2)
975 .unwrap();
976 let event_count = dataset
977 .try_fold_batches(0usize, |count, batch| Ok(count + batch.len()))
978 .unwrap();
979 assert_eq!(event_count, 4);
980 let rows = dataset
981 .try_fold_batches(Vec::new(), |mut rows, batch| {
982 rows.extend((0..batch.len()).map(|row| batch.scalar_at(0, row)));
983 Ok(rows)
984 })
985 .unwrap();
986 assert_eq!(rows, [0.0, 1.0, 2.0, 3.0]);
987
988 let error = dataset
989 .try_fold_batches(0usize, |_count, _batch| {
990 Err(LadduDataError::Unsupported("stop"))
991 })
992 .unwrap_err();
993 assert!(matches!(error, LadduDataError::Unsupported("stop")));
994
995 let empty = dataset.empty_derived().unwrap();
996 assert_eq!(
997 empty.schema().unwrap().as_ref(),
998 dataset.schema().unwrap().as_ref()
999 );
1000 assert_eq!(empty.num_events().unwrap(), Some(0));
1001 assert!(empty.batches().unwrap().next().is_none());
1002 }
1003
1004 #[test]
1005 fn deterministic_subsample_and_bootstrap_use_global_event_ids_across_batches() {
1006 let seed = 0x0BAD_5EED;
1007 let bootstrap_seed = 0xB007_57A9;
1008
1009 let dataset = Dataset::from_batches(vec![weighted_batch(0, 3), weighted_batch(3, 3)])
1010 .unwrap()
1011 .subsample(0.5, seed)
1012 .unwrap()
1013 .bootstrap(bootstrap_seed);
1014
1015 let observed = dataset
1016 .map_events(|ev| (ev.scalar(0) as u64, ev.weight()))
1017 .unwrap();
1018
1019 let expected: Vec<(u64, f64)> = (0_u64..6)
1020 .filter(|&event_id| uniform_hash_01(seed, event_id) < 0.5)
1021 .map(|event_id| {
1022 let original_weight = 10.0 + event_id as f64;
1023 let bootstrap_weight =
1024 poisson1_from_hash(bootstrap_seed, event_id) as f64 * original_weight;
1025 (event_id, bootstrap_weight)
1026 })
1027 .collect();
1028
1029 assert_eq!(observed, expected);
1030 }
1031
1032 #[test]
1033 fn materialized_batches_store_weights_only_when_needed() {
1034 let unweighted = unweighted_batch(0, 4);
1035
1036 let filtered = Dataset::from_batch(unweighted.clone())
1037 .filter(|ev| ev.scalar(0) >= 1.0)
1038 .subsample(1.0, 123)
1039 .unwrap();
1040
1041 let filtered_batch = filtered.batches().unwrap().next().unwrap().unwrap();
1042
1043 assert_eq!(scalar_values(&filtered_batch), vec![1.0, 2.0, 3.0]);
1044 assert!(filtered_batch.weights_column().is_none());
1045
1046 let bootstrapped = Dataset::from_batch(unweighted).bootstrap(999);
1047 let bootstrapped_batch = bootstrapped.batches().unwrap().next().unwrap().unwrap();
1048
1049 assert!(bootstrapped_batch.weights_column().is_some());
1050
1051 let source = weighted_batch(0, 2);
1052 let empty_weighted =
1053 materialize_batch(&source, &[DatasetOp::Filter(Arc::new(|_| false))], 0).unwrap();
1054 assert!(empty_weighted.is_empty());
1055 assert_eq!(empty_weighted.weights_column(), Some([].as_slice()));
1056 }
1057
1058 #[test]
1059 fn write_to_memory_sink_captures_transformed_dataset() {
1060 let dataset = Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) >= 2.0);
1061
1062 let mut sink = MemorySink::new();
1063 dataset.write_to(&mut sink).unwrap();
1064
1065 let captured = sink.into_batch().unwrap();
1066
1067 assert_eq!(scalar_values(&captured), vec![2.0, 3.0, 4.0]);
1068 assert_eq!(captured.weights_column().unwrap(), &[12.0, 13.0, 14.0]);
1069 }
1070
1071 #[test]
1072 fn immutable_dataset_identity_tracks_semantic_views() {
1073 let dataset = Dataset::from_batch(weighted_batch(0, 3));
1074 assert_eq!(dataset.identity(), dataset.clone().identity());
1075 assert_eq!(dataset.identity(), dataset.clone().streaming().identity());
1076 assert_ne!(
1077 dataset.identity(),
1078 dataset.clone().subsample(1.0, 7).unwrap().identity()
1079 );
1080 assert_ne!(dataset.identity(), dataset.clone().bootstrap(7).identity());
1081 assert_ne!(
1082 dataset.identity(),
1083 dataset.clone().filter(|_| true).identity()
1084 );
1085 }
1086}