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::{BootstrapGrouping, 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 sum_squared_weights: f64,
37 positive_weights: f64,
38 negative_weights: f64,
39}
40
41impl DatasetStats {
42 pub fn events(&self) -> u64 {
44 self.events
45 }
46
47 pub fn sum_weights(&self) -> f64 {
49 self.sum_weights
50 }
51
52 pub fn sum_squared_weights(&self) -> f64 {
54 self.sum_squared_weights
55 }
56
57 pub fn effective_entries(&self) -> Option<f64> {
62 (self.sum_squared_weights > 0.0)
63 .then(|| self.sum_weights * self.sum_weights / self.sum_squared_weights)
64 }
65
66 pub fn positive_weights(&self) -> f64 {
68 self.positive_weights
69 }
70
71 pub fn negative_weights(&self) -> f64 {
73 self.negative_weights
74 }
75}
76
77#[derive(Default)]
78struct DatasetStatsCache {
79 events: Option<u64>,
80 sum_weights: Option<f64>,
81 sum_squared_weights: Option<f64>,
82 positive_weights: Option<f64>,
83 negative_weights: Option<f64>,
84}
85
86#[derive(Clone)]
88pub struct Dataset {
89 identity: u64,
90 source: Arc<dyn EventSource>,
91 plan: ReadPlan,
92 ops: Arc<[DatasetOp]>,
93 bootstrap_grouping: Option<Arc<BootstrapGrouping>>,
94 cache_storage: CacheStorage,
95 memory_policy: MemoryPolicy,
96 memory_budget: MemoryBudget,
97 last_memory_decision: Arc<Mutex<Option<MemoryDecision>>>,
98 stats: Arc<Mutex<DatasetStatsCache>>,
99 source_traversals: Arc<AtomicU64>,
100}
101
102#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
104pub enum CacheStorage {
105 #[default]
107 Resident,
108 Streaming,
110}
111
112#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
114pub enum MemoryPolicy {
115 #[default]
117 Fastest,
118 Resident,
120 Streaming,
122}
123
124impl Dataset {
125 pub fn new<S>(source: S) -> Self
127 where
128 S: EventSource + 'static,
129 {
130 Self {
131 identity: next_dataset_identity(),
132 source: Arc::new(source),
133 plan: ReadPlan::default(),
134 ops: Arc::from([]),
135 bootstrap_grouping: None,
136 cache_storage: CacheStorage::Resident,
137 memory_policy: MemoryPolicy::Fastest,
138 memory_budget: MemoryBudget::Auto,
139 last_memory_decision: Default::default(),
140 stats: Default::default(),
141 source_traversals: Default::default(),
142 }
143 }
144
145 pub fn from_arc(source: Arc<dyn EventSource>) -> Self {
147 Self {
148 identity: next_dataset_identity(),
149 source,
150 plan: ReadPlan::default(),
151 ops: Arc::from([]),
152 bootstrap_grouping: None,
153 cache_storage: CacheStorage::Resident,
154 memory_policy: MemoryPolicy::Fastest,
155 memory_budget: MemoryBudget::Auto,
156 last_memory_decision: Default::default(),
157 stats: Default::default(),
158 source_traversals: Default::default(),
159 }
160 }
161
162 pub fn with_derived_source<S>(&self, source: S) -> Self
167 where
168 S: EventSource + 'static,
169 {
170 Self {
171 identity: next_dataset_identity(),
172 source: Arc::new(source),
173 plan: self.plan,
174 ops: Arc::from([]),
175 bootstrap_grouping: self.bootstrap_grouping.clone(),
176 cache_storage: self.cache_storage,
177 memory_policy: self.memory_policy,
178 memory_budget: self.memory_budget,
179 last_memory_decision: Default::default(),
180 stats: Default::default(),
181 source_traversals: Default::default(),
182 }
183 }
184
185 pub fn empty_derived(&self) -> LadduDataResult<Self> {
191 Ok(self.with_derived_source(MemorySource::empty(self.schema()?)))
192 }
193
194 pub fn from_batch(batch: EventBatch) -> Self {
196 Self::new(MemorySource::new(batch))
197 }
198
199 pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
205 Ok(Self::new(MemorySource::from_batches(batches)?))
206 }
207
208 pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
214 where
215 I: IntoIterator<Item = OwnedEvent>,
216 {
217 Ok(Self::new(MemorySource::from_events(schema, events)?))
218 }
219
220 pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
227 self.source.schema()
228 }
229
230 pub fn column(&self, name: &str) -> LadduDataResult<crate::columns::Column> {
235 let schema = self.schema()?;
236 let index = schema
237 .column_index(name)
238 .ok_or_else(|| LadduDataError::MissingColumn(crate::Name::from(name)))?;
239 let mut values = crate::columns::ColumnBuffer::new(schema.columns()[index].1, 0);
240 for batch in self.batches()? {
241 values.extend(batch?.column(index))?;
242 }
243 Ok(values.finish())
244 }
245
246 pub fn capabilities(&self) -> SourceCapabilities {
248 self.source.capabilities()
249 }
250
251 pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
257 {
258 let stats = self.stats.lock().unwrap_or_else(|error| error.into_inner());
259 if let Some(events) = stats.events {
260 return Ok(Some(events));
261 }
262 }
263
264 if self
265 .ops
266 .iter()
267 .any(|op| !matches!(op, DatasetOp::Bootstrap { .. }))
268 {
269 return Ok(None);
270 }
271
272 let events = self.source.num_events()?;
273 if let Some(events) = events {
274 self.stats
275 .lock()
276 .unwrap_or_else(|error| error.into_inner())
277 .events = Some(events);
278 }
279 Ok(events)
280 }
281
282 pub fn stats(&self) -> LadduDataResult<DatasetStats> {
291 {
292 let cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
293 if let (
294 Some(events),
295 Some(sum_weights),
296 Some(sum_squared_weights),
297 Some(positive_weights),
298 Some(negative_weights),
299 ) = (
300 cache.events,
301 cache.sum_weights,
302 cache.sum_squared_weights,
303 cache.positive_weights,
304 cache.negative_weights,
305 ) {
306 return Ok(DatasetStats {
307 events,
308 sum_weights,
309 sum_squared_weights,
310 positive_weights,
311 negative_weights,
312 });
313 }
314 }
315
316 let mut executor = self.executor_with_plan(self.plan)?;
317 for batch in &mut executor {
318 batch?;
319 }
320
321 Ok(executor.stats())
322 }
323
324 pub fn read_plan(&self) -> ReadPlan {
326 self.plan
327 }
328
329 pub fn cache_storage(&self) -> CacheStorage {
331 self.cache_storage
332 }
333
334 pub fn memory_policy(&self) -> MemoryPolicy {
336 self.memory_policy
337 }
338
339 pub fn memory_budget(&self) -> MemoryBudget {
341 self.memory_budget
342 }
343
344 pub fn last_memory_decision(&self) -> Option<MemoryDecision> {
346 self.last_memory_decision
347 .lock()
348 .unwrap_or_else(|error| error.into_inner())
349 .clone()
350 }
351
352 pub fn source_traversals(&self) -> u64 {
354 self.source_traversals.load(Ordering::Relaxed)
355 }
356
357 #[doc(hidden)]
362 pub fn identity(&self) -> u64 {
363 self.identity
364 }
365
366 pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
368 self.memory_budget = budget;
369 self
370 }
371
372 pub fn fastest(mut self) -> Self {
374 self.memory_policy = MemoryPolicy::Fastest;
375 self.cache_storage = CacheStorage::Resident;
376 self
377 }
378
379 pub fn resident(mut self) -> Self {
385 self.memory_policy = MemoryPolicy::Resident;
386 self.cache_storage = CacheStorage::Resident;
387 self
388 }
389
390 pub fn streaming(mut self) -> Self {
395 self.memory_policy = MemoryPolicy::Streaming;
396 self.cache_storage = CacheStorage::Streaming;
397 self
398 }
399
400 pub fn chunked(mut self, chunk_size: usize) -> LadduDataResult<Self> {
410 if chunk_size == 0 {
411 return Err(LadduDataError::InvalidArgument(
412 "chunk_size must be nonzero",
413 ));
414 }
415 self.plan.chunk_size = Some(chunk_size);
416 Ok(self)
417 }
418
419 pub fn unchunked(mut self) -> Self {
421 self.plan.chunk_size = None;
422 self
423 }
424
425 pub fn filter<F>(self, f: F) -> Self
427 where
428 F: Fn(Event<'_>) -> bool + Send + Sync + 'static,
429 {
430 self.push_op(DatasetOp::Filter(Arc::new(f)))
431 }
432
433 pub fn subsample(self, fraction: f64, seed: u64) -> LadduDataResult<Self> {
440 if !(0.0..=1.0).contains(&fraction) {
441 return Err(LadduDataError::InvalidArgument(
442 "fraction must be in [0, 1]",
443 ));
444 }
445
446 Ok(self.push_op(DatasetOp::Subsample { fraction, seed }))
447 }
448
449 pub fn with_bootstrap_groups(
459 mut self,
460 columns: Vec<String>,
461 namespace: String,
462 ) -> LadduDataResult<Self> {
463 if columns.is_empty()
464 || columns
465 .iter()
466 .enumerate()
467 .any(|(i, name)| columns[..i].contains(name))
468 {
469 return Err(LadduDataError::InvalidArgument(
470 "bootstrap group columns must be nonempty and unique",
471 ));
472 }
473 let schema = self.schema()?;
474 for name in &columns {
475 if schema.column_index(name).is_none() {
476 return Err(LadduDataError::MissingColumn(crate::Name::from(
477 name.as_str(),
478 )));
479 }
480 }
481 self.bootstrap_grouping = Some(Arc::new(BootstrapGrouping {
482 columns: columns.into(),
483 namespace,
484 }));
485 self.identity = next_dataset_identity();
486 Ok(self)
487 }
488
489 pub fn has_bootstrap_groups(&self) -> bool {
491 self.bootstrap_grouping.is_some()
492 }
493
494 pub fn bootstrap(self, seed: u64) -> Self {
496 let grouping = self.bootstrap_grouping.clone();
497 self.push_op(DatasetOp::Bootstrap { seed, grouping })
498 }
499
500 pub fn for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
507 where
508 F: FnMut(Event<'_>),
509 {
510 self.try_for_each_event(|ev| {
511 f(ev);
512 Ok(())
513 })
514 }
515
516 pub fn try_for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
523 where
524 F: FnMut(Event<'_>) -> LadduDataResult<()>,
525 {
526 visit_events(
527 self,
528 DatasetExecutionPlan::resolve(self, self.plan)?,
529 &mut f,
530 )
531 }
532
533 pub fn try_map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
540 where
541 F: FnMut(Event<'_>) -> LadduDataResult<T>,
542 {
543 let mut out = Vec::new();
544 self.try_for_each_event(|ev| {
545 out.push(f(ev)?);
546 Ok(())
547 })?;
548
549 Ok(out)
550 }
551
552 pub fn map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
559 where
560 F: FnMut(Event<'_>) -> T,
561 {
562 let mut out = Vec::new();
563 self.try_for_each_event(|ev| {
564 out.push(f(ev));
565 Ok(())
566 })?;
567
568 Ok(out)
569 }
570
571 pub fn try_fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
578 where
579 F: FnMut(T, Event<'_>) -> LadduDataResult<T>,
580 {
581 let mut acc = Some(init);
582
583 self.try_for_each_event(|ev| {
584 let current = acc.take().ok_or_else(|| {
585 LadduDataError::Source("dataset fold accumulator was consumed".into())
586 })?;
587 acc = Some(f(current, ev)?);
588 Ok(())
589 })?;
590
591 acc.ok_or_else(|| LadduDataError::Source("dataset fold produced no accumulator".into()))
592 }
593
594 pub fn fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
601 where
602 F: FnMut(T, Event<'_>) -> T,
603 {
604 self.try_fold_events(init, |acc, ev| Ok(f(acc, ev)))
605 }
606
607 pub fn try_fold_batches<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
616 where
617 F: FnMut(T, EventBatch) -> LadduDataResult<T>,
618 {
619 let mut acc = init;
620 for batch in self.batches()? {
621 acc = f(acc, batch?)?;
622 }
623 Ok(acc)
624 }
625
626 pub fn try_accumulate_events<T, F>(&self, mut acc: T, mut f: F) -> LadduDataResult<T>
633 where
634 F: FnMut(&mut T, Event<'_>) -> LadduDataResult<()>,
635 {
636 self.try_for_each_event(|ev| f(&mut acc, ev))?;
637 Ok(acc)
638 }
639
640 pub fn accumulate_events<T, F>(&self, acc: T, mut f: F) -> LadduDataResult<T>
647 where
648 F: FnMut(&mut T, Event<'_>),
649 {
650 self.try_accumulate_events(acc, |acc, ev| {
651 f(acc, ev);
652 Ok(())
653 })
654 }
655
656 pub fn sum_weights(&self) -> LadduDataResult<f64> {
663 Ok(self.stats()?.sum_weights())
664 }
665
666 pub fn weighted_sum<F>(&self, mut f: F) -> LadduDataResult<f64>
673 where
674 F: FnMut(Event<'_>) -> f64,
675 {
676 self.fold_events(0.0, |sum, ev| sum + ev.weight() * f(ev))
677 }
678
679 pub fn weighted_complex_sum<F>(&self, mut f: F) -> LadduDataResult<Complex64>
686 where
687 F: FnMut(Event<'_>) -> Complex64,
688 {
689 self.fold_events(0.0.into(), |sum, ev| sum + ev.weight() * f(ev))
690 }
691
692 pub fn batches(
699 &self,
700 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
701 self.stream_with_plan(self.plan)
702 }
703
704 #[doc(hidden)]
705 pub fn stream_with_plan(
712 &self,
713 plan: ReadPlan,
714 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
715 Ok(Box::new(self.executor_with_plan(plan)?))
716 }
717
718 #[doc(hidden)]
719 pub fn batches_with_plan(
721 &self,
722 plan: ReadPlan,
723 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
724 self.stream_with_plan(plan)
725 }
726
727 fn executor_with_plan(&self, plan: ReadPlan) -> LadduDataResult<DatasetExecutor> {
728 DatasetExecutor::new(self, DatasetExecutionPlan::resolve(self, plan)?)
729 }
730
731 pub fn try_for_each_batch<F>(&self, mut f: F) -> LadduDataResult<()>
738 where
739 F: FnMut(EventBatch) -> LadduDataResult<()>,
740 {
741 for batch in self.batches()? {
742 f(batch?)?;
743 }
744
745 Ok(())
746 }
747
748 pub fn map_batches<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
754 where
755 F: FnMut(EventBatch) -> T,
756 {
757 let mut out = Vec::new();
758
759 self.try_for_each_batch(|batch| {
760 out.push(f(batch));
761 Ok(())
762 })?;
763
764 Ok(out)
765 }
766
767 pub fn write_to<S: EventSink>(&self, sink: &mut S) -> LadduDataResult<()> {
774 sink.begin(self.schema()?, WritePlan::from(self.plan))?;
775
776 let result = (|| {
777 for batch in self.batches()? {
778 sink.write_batch(&batch?)?;
779 }
780
781 sink.finish()
782 })();
783
784 if result.is_err() {
785 let _ = sink.abort();
788 }
789
790 result
791 }
792
793 fn push_op(self, op: DatasetOp) -> Self {
794 let preserved_events = if matches!(&op, DatasetOp::Bootstrap { .. }) {
795 self.num_events().ok().flatten()
796 } else {
797 None
798 };
799 let mut ops = self.ops.to_vec();
800 ops.push(op);
801
802 Self {
803 identity: next_dataset_identity(),
804 source: self.source,
805 plan: self.plan,
806 ops: ops.into(),
807 bootstrap_grouping: self.bootstrap_grouping,
808 cache_storage: self.cache_storage,
809 memory_policy: self.memory_policy,
810 memory_budget: self.memory_budget,
811 last_memory_decision: Default::default(),
812 stats: Arc::new(Mutex::new(DatasetStatsCache {
813 events: preserved_events,
814 sum_weights: None,
815 sum_squared_weights: None,
816 positive_weights: None,
817 negative_weights: None,
818 })),
819 source_traversals: Default::default(),
820 }
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use super::ops::materialize_batch;
827 use super::*;
828 use crate::io::{EventBatchIter, EventSource, ReadPlan, memory::MemorySink};
829 use laddu_physics::vectors::RealVec4;
830 use std::sync::atomic::{AtomicUsize, Ordering};
831
832 #[derive(Clone)]
833 struct CountingSource {
834 batch: EventBatch,
835 reads: Arc<AtomicUsize>,
836 }
837
838 impl EventSource for CountingSource {
839 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
840 Ok(Arc::clone(self.batch.schema()))
841 }
842
843 fn batches(
844 &self,
845 _plan: ReadPlan,
846 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
847 self.reads.fetch_add(1, Ordering::Relaxed);
848 Ok(Box::new(std::iter::once(Ok(self.batch.clone()))))
849 }
850 }
851
852 #[derive(Clone)]
853 struct ErrorSource {
854 schema: Arc<Schema>,
855 items: Arc<[LadduDataResult<EventBatch>]>,
856 }
857
858 impl EventSource for ErrorSource {
859 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
860 Ok(Arc::clone(&self.schema))
861 }
862
863 fn batches(&self, _plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
864 let items = Arc::clone(&self.items);
865 Ok(Box::new(
866 (0..items.len()).map(move |index| items[index].clone()),
867 ))
868 }
869 }
870
871 fn v(x: f64) -> RealVec4 {
872 RealVec4 {
873 e: x + 0.3,
874 px: x,
875 py: x + 0.1,
876 pz: x + 0.2,
877 }
878 }
879
880 fn schema_with_weight() -> Arc<Schema> {
881 Arc::new(Schema::new(["p"], ["x"], true).unwrap())
882 }
883
884 fn schema_without_weight() -> Arc<Schema> {
885 Arc::new(Schema::new(["p"], ["x"], false).unwrap())
886 }
887
888 fn weighted_batch(start: usize, len: usize) -> EventBatch {
889 let schema = schema_with_weight();
890
891 let events = (start..start + len)
892 .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
893
894 EventBatch::from_events(schema, events).unwrap()
895 }
896
897 fn unweighted_batch(start: usize, len: usize) -> EventBatch {
898 let schema = schema_without_weight();
899
900 let events =
901 (start..start + len).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64]));
902
903 EventBatch::from_events(schema, events).unwrap()
904 }
905
906 fn error_source(error_after: Option<EventBatch>) -> ErrorSource {
907 let schema = schema_with_weight();
908 let mut items = Vec::new();
909 if let Some(batch) = error_after {
910 items.push(Ok(batch));
911 }
912 items.push(Err(LadduDataError::Unsupported("source")));
913 ErrorSource {
914 schema,
915 items: items.into(),
916 }
917 }
918
919 fn scalar_values(batch: &EventBatch) -> Vec<f64> {
920 batch.scalar_column(0).to_vec()
921 }
922
923 fn stats_dataset(weights: &[f64]) -> Dataset {
924 let schema = schema_with_weight();
925 let events = weights.iter().enumerate().map(|(index, &weight)| {
926 OwnedEvent::weighted(vec![v(index as f64)], vec![index as f64], weight)
927 });
928 Dataset::from_batch(EventBatch::from_events(schema, events).unwrap())
929 }
930
931 #[test]
932 fn dataset_statistics_are_shared_per_view_and_invalidated_by_selection() {
933 let reads = Arc::new(AtomicUsize::new(0));
934 let dataset = Dataset::new(CountingSource {
935 batch: weighted_batch(0, 5),
936 reads: Arc::clone(&reads),
937 });
938 let clone = dataset.clone();
939
940 assert_eq!(dataset.num_events().unwrap(), None);
941 assert_eq!(dataset.stats().unwrap().events(), 5);
942 assert_eq!(clone.sum_weights().unwrap(), 60.0);
943 let stats = clone.stats().unwrap();
944 assert_eq!(stats.sum_squared_weights(), 730.0);
945 assert_eq!(stats.positive_weights(), 60.0);
946 assert_eq!(stats.negative_weights(), 0.0);
947 assert_eq!(stats.effective_entries(), Some(3600.0 / 730.0));
948 assert_eq!(clone.num_events().unwrap(), Some(5));
949 assert_eq!(reads.load(Ordering::Relaxed), 1);
950
951 let bootstrapped = dataset.clone().bootstrap(7);
952 assert_eq!(bootstrapped.num_events().unwrap(), Some(5));
953 assert_eq!(reads.load(Ordering::Relaxed), 1);
954
955 let filtered = dataset.filter(|event| event.scalar(0) >= 2.0);
956 assert_eq!(filtered.num_events().unwrap(), None);
957 assert_eq!(
958 filtered.map_events(|event| event.scalar(0)).unwrap(),
959 [2.0, 3.0, 4.0]
960 );
961 assert_eq!(reads.load(Ordering::Relaxed), 2);
962 assert_eq!(filtered.stats().unwrap().events(), 3);
963 assert_eq!(reads.load(Ordering::Relaxed), 2);
964 }
965
966 #[test]
967 fn dataset_statistics_preserve_signed_and_squared_weight_diagnostics() {
968 let stats = stats_dataset(&[2.0, -1.0, 3.0, -4.0]).stats().unwrap();
969
970 assert_eq!(stats.events(), 4);
971 assert_eq!(stats.sum_weights(), 0.0);
972 assert_eq!(stats.sum_squared_weights(), 30.0);
973 assert_eq!(stats.positive_weights(), 5.0);
974 assert_eq!(stats.negative_weights(), -5.0);
975 assert_eq!(stats.effective_entries(), Some(0.0));
976 }
977
978 #[test]
979 fn dataset_statistics_make_effective_entries_unavailable_without_squared_weight() {
980 let zero = stats_dataset(&[0.0, 0.0]).stats().unwrap();
981 assert_eq!(zero.events(), 2);
982 assert_eq!(zero.sum_weights(), 0.0);
983 assert_eq!(zero.sum_squared_weights(), 0.0);
984 assert_eq!(zero.positive_weights(), 0.0);
985 assert_eq!(zero.negative_weights(), 0.0);
986 assert_eq!(zero.effective_entries(), None);
987
988 let empty = stats_dataset(&[1.0])
989 .empty_derived()
990 .unwrap()
991 .stats()
992 .unwrap();
993 assert_eq!(empty.events(), 0);
994 assert_eq!(empty.sum_weights(), 0.0);
995 assert_eq!(empty.sum_squared_weights(), 0.0);
996 assert_eq!(empty.effective_entries(), None);
997 }
998
999 #[test]
1000 fn dataset_statistics_match_across_resident_streaming_and_chunked_views() {
1001 let resident = Dataset::from_batch(weighted_batch(0, 10));
1002 let streaming = Dataset::new(CountingSource {
1003 batch: weighted_batch(0, 10),
1004 reads: Arc::new(AtomicUsize::new(0)),
1005 });
1006 let chunked = Dataset::from_batches(
1007 (0..10)
1008 .map(|index| weighted_batch(index, 1))
1009 .collect::<Vec<_>>(),
1010 )
1011 .unwrap()
1012 .chunked(3)
1013 .unwrap();
1014
1015 let expected = resident.stats().unwrap();
1016 assert_eq!(streaming.stats().unwrap(), expected);
1017 assert_eq!(chunked.stats().unwrap(), expected);
1018 }
1019
1020 #[test]
1021 fn transformed_fragments_are_coalesced_to_the_read_chunk_size() {
1022 let fragments = (0..10)
1023 .map(|index| weighted_batch(index, 1))
1024 .collect::<Vec<_>>();
1025 let dataset = Dataset::from_batches(fragments)
1026 .unwrap()
1027 .filter(|_| true)
1028 .chunked(4)
1029 .unwrap();
1030
1031 let batches = dataset
1032 .batches()
1033 .unwrap()
1034 .collect::<LadduDataResult<Vec<_>>>()
1035 .unwrap();
1036 assert_eq!(
1037 batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
1038 [4, 4, 2]
1039 );
1040 assert_eq!(
1041 batches
1042 .iter()
1043 .flat_map(|batch| batch.scalar_column(0).iter().copied())
1044 .collect::<Vec<_>>(),
1045 (0..10).map(|value| value as f64).collect::<Vec<_>>()
1046 );
1047 }
1048
1049 #[test]
1050 fn shared_stream_preserves_pending_batches_before_source_errors() {
1051 let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
1052 .chunked(4)
1053 .unwrap();
1054 let mut batches = dataset.batches().unwrap();
1055
1056 assert_eq!(batches.next().unwrap().unwrap().len(), 2);
1057 assert!(matches!(
1058 batches.next().unwrap(),
1059 Err(LadduDataError::Unsupported("source"))
1060 ));
1061 assert!(batches.next().is_none());
1062
1063 assert!(matches!(
1064 dataset.stats(),
1065 Err(LadduDataError::Unsupported("source"))
1066 ));
1067 assert_eq!(dataset.source_traversals(), 2);
1068 }
1069
1070 #[test]
1071 fn event_visitors_share_the_execution_plan_without_changing_source_rows() {
1072 let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
1073 .chunked(4)
1074 .unwrap()
1075 .filter(|event| event.scalar(0) >= 0.0);
1076 let mut rows = Vec::new();
1077
1078 let error = dataset
1079 .try_for_each_event(|event| {
1080 rows.push(event.row());
1081 Ok(())
1082 })
1083 .unwrap_err();
1084
1085 assert!(matches!(error, LadduDataError::Unsupported("source")));
1086 assert_eq!(rows, [0, 1]);
1087 }
1088
1089 #[test]
1090 fn dataset_map_fold_accumulate_complex_sum_and_error_paths_use_transformed_events() {
1091 let dataset =
1092 Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) % 2.0 == 0.0);
1093
1094 let rows = dataset
1095 .map_events(|ev| (ev.row(), ev.scalar(0), ev.weight()))
1096 .unwrap();
1097
1098 assert_eq!(rows, vec![(0, 0.0, 10.0), (2, 2.0, 12.0), (4, 4.0, 14.0)]);
1099
1100 let folded = dataset
1101 .fold_events(String::new(), |mut out, ev| {
1102 out.push_str(&format!("{};", ev.scalar(0)));
1103 out
1104 })
1105 .unwrap();
1106
1107 assert_eq!(folded, "0;2;4;");
1108
1109 let accumulated = dataset
1110 .accumulate_events(Vec::<f64>::new(), |values, ev| values.push(ev.weight()))
1111 .unwrap();
1112
1113 assert_eq!(accumulated, vec![10.0, 12.0, 14.0]);
1114
1115 let weighted_sum = dataset.weighted_sum(|ev| ev.scalar(0)).unwrap();
1116 assert_eq!(weighted_sum, 0.0 * 10.0 + 2.0 * 12.0 + 4.0 * 14.0);
1117
1118 let complex_sum = dataset
1119 .weighted_complex_sum(|ev| Complex64::new(ev.scalar(0), 1.0))
1120 .unwrap();
1121
1122 assert_eq!(complex_sum.re, weighted_sum);
1123 assert_eq!(complex_sum.im, 10.0 + 12.0 + 14.0);
1124
1125 let err = dataset
1126 .try_map_events(|ev| {
1127 if ev.scalar(0) == 2.0 {
1128 Err(LadduDataError::Unsupported("stop"))
1129 } else {
1130 Ok(ev.scalar(0))
1131 }
1132 })
1133 .unwrap_err();
1134
1135 assert!(matches!(err, LadduDataError::Unsupported("stop")));
1136 }
1137
1138 #[test]
1139 fn batch_folds_and_empty_derived_sources_preserve_schema_and_errors() {
1140 let dataset = Dataset::from_batches(vec![weighted_batch(0, 2), weighted_batch(2, 2)])
1141 .unwrap()
1142 .chunked(2)
1143 .unwrap();
1144 let event_count = dataset
1145 .try_fold_batches(0usize, |count, batch| Ok(count + batch.len()))
1146 .unwrap();
1147 assert_eq!(event_count, 4);
1148 let rows = dataset
1149 .try_fold_batches(Vec::new(), |mut rows, batch| {
1150 rows.extend((0..batch.len()).map(|row| batch.scalar_at(0, row)));
1151 Ok(rows)
1152 })
1153 .unwrap();
1154 assert_eq!(rows, [0.0, 1.0, 2.0, 3.0]);
1155
1156 let error = dataset
1157 .try_fold_batches(0usize, |_count, _batch| {
1158 Err(LadduDataError::Unsupported("stop"))
1159 })
1160 .unwrap_err();
1161 assert!(matches!(error, LadduDataError::Unsupported("stop")));
1162
1163 let empty = dataset.empty_derived().unwrap();
1164 assert_eq!(
1165 empty.schema().unwrap().as_ref(),
1166 dataset.schema().unwrap().as_ref()
1167 );
1168 assert_eq!(empty.num_events().unwrap(), Some(0));
1169 assert!(empty.batches().unwrap().next().is_none());
1170 }
1171
1172 #[test]
1173 fn deterministic_subsample_and_bootstrap_use_global_event_ids_across_batches() {
1174 let seed = 0x0BAD_5EED;
1175 let bootstrap_seed = 0xB007_57A9;
1176
1177 let dataset = Dataset::from_batches(vec![weighted_batch(0, 3), weighted_batch(3, 3)])
1178 .unwrap()
1179 .subsample(0.5, seed)
1180 .unwrap()
1181 .bootstrap(bootstrap_seed);
1182
1183 let observed = dataset
1184 .map_events(|ev| (ev.scalar(0) as u64, ev.weight()))
1185 .unwrap();
1186
1187 let expected: Vec<(u64, f64)> = (0_u64..6)
1188 .filter(|&event_id| uniform_hash_01(seed, event_id) < 0.5)
1189 .map(|event_id| {
1190 let original_weight = 10.0 + event_id as f64;
1191 let bootstrap_weight =
1192 poisson1_from_hash(bootstrap_seed, event_id) as f64 * original_weight;
1193 (event_id, bootstrap_weight)
1194 })
1195 .collect();
1196
1197 assert_eq!(observed, expected);
1198 }
1199
1200 #[test]
1201 fn materialized_batches_store_weights_only_when_needed() {
1202 let unweighted = unweighted_batch(0, 4);
1203
1204 let filtered = Dataset::from_batch(unweighted.clone())
1205 .filter(|ev| ev.scalar(0) >= 1.0)
1206 .subsample(1.0, 123)
1207 .unwrap();
1208
1209 let filtered_batch = filtered.batches().unwrap().next().unwrap().unwrap();
1210
1211 assert_eq!(scalar_values(&filtered_batch), vec![1.0, 2.0, 3.0]);
1212 assert!(filtered_batch.weights_column().is_none());
1213
1214 let bootstrapped = Dataset::from_batch(unweighted).bootstrap(999);
1215 let bootstrapped_batch = bootstrapped.batches().unwrap().next().unwrap().unwrap();
1216
1217 assert!(bootstrapped_batch.weights_column().is_some());
1218
1219 let source = weighted_batch(0, 2);
1220 let empty_weighted =
1221 materialize_batch(&source, &[DatasetOp::Filter(Arc::new(|_| false))], 0).unwrap();
1222 assert!(empty_weighted.is_empty());
1223 assert_eq!(empty_weighted.weights_column(), Some([].as_slice()));
1224 }
1225
1226 #[test]
1227 fn write_to_memory_sink_captures_transformed_dataset() {
1228 let dataset = Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) >= 2.0);
1229
1230 let mut sink = MemorySink::new();
1231 dataset.write_to(&mut sink).unwrap();
1232
1233 let captured = sink.into_batch().unwrap();
1234
1235 assert_eq!(scalar_values(&captured), vec![2.0, 3.0, 4.0]);
1236 assert_eq!(captured.weights_column().unwrap(), &[12.0, 13.0, 14.0]);
1237 }
1238
1239 #[test]
1240 fn immutable_dataset_identity_tracks_semantic_views() {
1241 let dataset = Dataset::from_batch(weighted_batch(0, 3));
1242 assert_eq!(dataset.identity(), dataset.clone().identity());
1243 assert_eq!(dataset.identity(), dataset.clone().streaming().identity());
1244 assert_ne!(
1245 dataset.identity(),
1246 dataset.clone().subsample(1.0, 7).unwrap().identity()
1247 );
1248 assert_ne!(dataset.identity(), dataset.clone().bootstrap(7).identity());
1249 assert_ne!(
1250 dataset.identity(),
1251 dataset.clone().filter(|_| true).identity()
1252 );
1253 }
1254
1255 trait TestWeights {
1256 fn test_weights(&self) -> LadduDataResult<Vec<f64>>;
1257 }
1258 impl TestWeights for Dataset {
1259 fn test_weights(&self) -> LadduDataResult<Vec<f64>> {
1260 let mut weights = Vec::new();
1261 self.for_each_event(|event| weights.push(event.weight()))?;
1262 Ok(weights)
1263 }
1264 }
1265
1266 fn grouped_batch(runs: &[u32], events: &[u64], weights: &[f64]) -> EventBatch {
1267 use crate::columns::{Column, ColumnDType};
1268 let schema = Arc::new(
1269 Schema::new(Vec::<&str>::new(), Vec::<&str>::new(), true)
1270 .unwrap()
1271 .with_columns([("run", ColumnDType::U32), ("event", ColumnDType::U64)])
1272 .unwrap(),
1273 );
1274 EventBatch::new_with_columns(
1275 schema,
1276 vec![],
1277 vec![],
1278 vec![Column::U32(Arc::from(runs)), Column::U64(Arc::from(events))],
1279 Some(Arc::from(weights)),
1280 )
1281 .unwrap()
1282 }
1283
1284 #[test]
1285 fn grouped_bootstrap_pairs_hypotheses_across_batches_filters_and_row_order() {
1286 let big = 1_u64 << 54;
1287 let first = grouped_batch(&[1, 1, 2], &[big, big, big], &[1.0, -0.25, 1.0]);
1288 let second = grouped_batch(&[1, 2, 1], &[big, big + 1, big + 1], &[2.0, 1.0, 1.0]);
1289 let source = Dataset::new(MemorySource::from_batches(vec![first, second]).unwrap());
1290 let grouped = source
1291 .clone()
1292 .with_bootstrap_groups(vec!["run".into(), "event".into()], "sample".into())
1293 .unwrap();
1294 assert_ne!(source.identity(), grouped.identity());
1295 assert_eq!(
1296 source.stats().unwrap().sum_weights(),
1297 grouped.stats().unwrap().sum_weights()
1298 );
1299 let resampled = grouped.clone().bootstrap(42).test_weights().unwrap();
1300 assert_eq!(resampled[0], resampled[1] / -0.25);
1301 assert_eq!(resampled[0], resampled[3] / 2.0);
1302 let filtered = grouped
1303 .filter(|ev| {
1304 matches!(
1305 ev.column_named("run"),
1306 Some(crate::columns::ColumnValue::U32(1))
1307 )
1308 })
1309 .bootstrap(42)
1310 .test_weights()
1311 .unwrap();
1312 assert_eq!(
1313 filtered,
1314 vec![resampled[0], resampled[1], resampled[3], resampled[5]]
1315 );
1316 let reverse = Dataset::from_batch(grouped_batch(
1317 &[1, 2, 1, 2, 1, 1],
1318 &[big + 1, big + 1, big, big, big, big],
1319 &[1.0, 1.0, 2.0, 1.0, -0.25, 1.0],
1320 ))
1321 .with_bootstrap_groups(vec!["run".into(), "event".into()], "sample".into())
1322 .unwrap()
1323 .bootstrap(42)
1324 .test_weights()
1325 .unwrap();
1326 assert_eq!(reverse, resampled.into_iter().rev().collect::<Vec<_>>());
1327 }
1328
1329 #[test]
1330 fn grouped_bootstrap_validates_keys_and_separates_namespaces_and_exact_ids() {
1331 let events: Vec<_> = (0..1024).map(|i| (1_u64 << 54) + i).collect();
1332 let data = Dataset::from_batch(grouped_batch(&vec![1; 1024], &events, &vec![1.0; 1024]));
1333 assert!(
1334 data.clone()
1335 .with_bootstrap_groups(vec![], "".into())
1336 .is_err()
1337 );
1338 assert!(
1339 data.clone()
1340 .with_bootstrap_groups(vec!["event".into(), "event".into()], "".into())
1341 .is_err()
1342 );
1343 assert!(
1344 data.clone()
1345 .with_bootstrap_groups(vec!["missing".into()], "".into())
1346 .is_err()
1347 );
1348 let first = data
1349 .clone()
1350 .with_bootstrap_groups(vec!["event".into()], "first".into())
1351 .unwrap()
1352 .bootstrap(7)
1353 .test_weights()
1354 .unwrap();
1355 let second = data
1356 .with_bootstrap_groups(vec!["event".into()], "second".into())
1357 .unwrap()
1358 .bootstrap(7)
1359 .test_weights()
1360 .unwrap();
1361 assert_ne!(first, second);
1362 assert!(first.windows(2).any(|w| w[0] != w[1]));
1363 let mean = first.iter().sum::<f64>() / 1024.0;
1364 assert!((mean - 1.0).abs() < 0.15);
1365 }
1366}