1use std::sync::{Arc, Mutex};
2
3use crate::{
4 LadduDataError, LadduDataResult,
5 data::event::{Event, EventBatch, OwnedEvent},
6 io::{EventSink, EventSource, ReadPlan, SourceCapabilities, WritePlan, memory::MemorySource},
7 schema::Schema,
8};
9use laddu_memory::{MemoryBudget, MemoryDecision, MemoryState};
10use laddu_physics::vectors::RealVec4;
11use num::complex::Complex64;
12
13#[derive(Clone)]
14enum DatasetOp {
15 Filter(Arc<dyn Fn(Event<'_>) -> bool + Send + Sync>),
16 Subsample { fraction: f64, seed: u64 },
17 Bootstrap { seed: u64 },
18}
19
20#[derive(Clone)]
22pub struct Dataset {
23 source: Arc<dyn EventSource>,
24 plan: ReadPlan,
25 ops: Arc<[DatasetOp]>,
26 cache_storage: CacheStorage,
27 memory_policy: MemoryPolicy,
28 memory_budget: MemoryBudget,
29 last_memory_decision: Arc<Mutex<Option<MemoryDecision>>>,
30}
31
32#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
34pub enum CacheStorage {
35 #[default]
37 Resident,
38 Streaming,
40}
41
42#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
44pub enum MemoryPolicy {
45 #[default]
47 Fastest,
48 Resident,
50 Streaming,
52}
53
54impl Dataset {
55 pub fn new<S>(source: S) -> Self
57 where
58 S: EventSource + 'static,
59 {
60 Self {
61 source: Arc::new(source),
62 plan: ReadPlan::default(),
63 ops: Arc::from([]),
64 cache_storage: CacheStorage::Resident,
65 memory_policy: MemoryPolicy::Fastest,
66 memory_budget: MemoryBudget::Auto,
67 last_memory_decision: Default::default(),
68 }
69 }
70
71 pub fn from_arc(source: Arc<dyn EventSource>) -> Self {
73 Self {
74 source,
75 plan: ReadPlan::default(),
76 ops: Arc::from([]),
77 cache_storage: CacheStorage::Resident,
78 memory_policy: MemoryPolicy::Fastest,
79 memory_budget: MemoryBudget::Auto,
80 last_memory_decision: Default::default(),
81 }
82 }
83
84 #[doc(hidden)]
86 pub fn with_derived_source<S>(&self, source: S) -> Self
87 where
88 S: EventSource + 'static,
89 {
90 Self {
91 source: Arc::new(source),
92 plan: self.plan,
93 ops: Arc::from([]),
94 cache_storage: self.cache_storage,
95 memory_policy: self.memory_policy,
96 memory_budget: self.memory_budget,
97 last_memory_decision: Default::default(),
98 }
99 }
100
101 pub fn from_batch(batch: EventBatch) -> Self {
103 Self::new(MemorySource::new(batch))
104 }
105
106 pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
112 Ok(Self::new(MemorySource::from_batches(batches)?))
113 }
114
115 pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
121 where
122 I: IntoIterator<Item = OwnedEvent>,
123 {
124 Ok(Self::new(MemorySource::from_events(schema, events)?))
125 }
126
127 pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
134 self.source.schema()
135 }
136
137 pub fn capabilities(&self) -> SourceCapabilities {
139 self.source.capabilities()
140 }
141
142 pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
148 self.source.num_events()
149 }
150
151 pub fn read_plan(&self) -> ReadPlan {
153 self.plan
154 }
155
156 pub fn cache_storage(&self) -> CacheStorage {
158 self.cache_storage
159 }
160
161 pub fn memory_policy(&self) -> MemoryPolicy {
163 self.memory_policy
164 }
165
166 pub fn memory_budget(&self) -> MemoryBudget {
168 self.memory_budget
169 }
170
171 pub fn last_memory_decision(&self) -> Option<MemoryDecision> {
173 self.last_memory_decision
174 .lock()
175 .unwrap_or_else(|error| error.into_inner())
176 .clone()
177 }
178
179 pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
181 self.memory_budget = budget;
182 self
183 }
184
185 pub fn fastest(mut self) -> Self {
187 self.memory_policy = MemoryPolicy::Fastest;
188 self.cache_storage = CacheStorage::Resident;
189 self
190 }
191
192 pub fn resident(mut self) -> Self {
198 self.memory_policy = MemoryPolicy::Resident;
199 self.cache_storage = CacheStorage::Resident;
200 self
201 }
202
203 pub fn streaming(mut self) -> Self {
208 self.memory_policy = MemoryPolicy::Streaming;
209 self.cache_storage = CacheStorage::Streaming;
210 self
211 }
212
213 pub fn chunked(mut self, chunk_size: usize) -> LadduDataResult<Self> {
223 if chunk_size == 0 {
224 return Err(LadduDataError::InvalidArgument(
225 "chunk_size must be nonzero",
226 ));
227 }
228 self.plan.chunk_size = Some(chunk_size);
229 Ok(self)
230 }
231
232 pub fn unchunked(mut self) -> Self {
234 self.plan.chunk_size = None;
235 self
236 }
237
238 pub fn filter<F>(self, f: F) -> Self
240 where
241 F: Fn(Event<'_>) -> bool + Send + Sync + 'static,
242 {
243 self.push_op(DatasetOp::Filter(Arc::new(f)))
244 }
245
246 pub fn subsample(self, fraction: f64, seed: u64) -> LadduDataResult<Self> {
253 if !(0.0..=1.0).contains(&fraction) {
254 return Err(LadduDataError::InvalidArgument(
255 "fraction must be in [0, 1]",
256 ));
257 }
258
259 Ok(self.push_op(DatasetOp::Subsample { fraction, seed }))
260 }
261
262 pub fn bootstrap(self, seed: u64) -> Self {
264 self.push_op(DatasetOp::Bootstrap { seed })
265 }
266
267 pub fn for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
274 where
275 F: FnMut(Event<'_>),
276 {
277 self.try_for_each_event(|ev| {
278 f(ev);
279 Ok(())
280 })
281 }
282
283 pub fn try_for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
290 where
291 F: FnMut(Event<'_>) -> LadduDataResult<()>,
292 {
293 let mut offset = 0_u64;
294
295 for batch in self.source.batches(self.plan)? {
296 let batch = batch?;
297 let base = offset;
298 offset += batch.len() as u64;
299 eval_batch(&batch, &self.ops, base, &mut f)?;
300 }
301
302 Ok(())
303 }
304
305 pub fn try_map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
312 where
313 F: FnMut(Event<'_>) -> LadduDataResult<T>,
314 {
315 let mut out = Vec::new();
316 self.try_for_each_event(|ev| {
317 out.push(f(ev)?);
318 Ok(())
319 })?;
320
321 Ok(out)
322 }
323
324 pub fn map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
331 where
332 F: FnMut(Event<'_>) -> T,
333 {
334 let mut out = Vec::new();
335 self.try_for_each_event(|ev| {
336 out.push(f(ev));
337 Ok(())
338 })?;
339
340 Ok(out)
341 }
342
343 pub fn try_fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
350 where
351 F: FnMut(T, Event<'_>) -> LadduDataResult<T>,
352 {
353 let mut acc = Some(init);
354
355 self.try_for_each_event(|ev| {
356 let current = acc.take().ok_or_else(|| {
357 LadduDataError::Source("dataset fold accumulator was consumed".into())
358 })?;
359 acc = Some(f(current, ev)?);
360 Ok(())
361 })?;
362
363 acc.ok_or_else(|| LadduDataError::Source("dataset fold produced no accumulator".into()))
364 }
365
366 pub fn fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
373 where
374 F: FnMut(T, Event<'_>) -> T,
375 {
376 self.try_fold_events(init, |acc, ev| Ok(f(acc, ev)))
377 }
378
379 pub fn try_accumulate_events<T, F>(&self, mut acc: T, mut f: F) -> LadduDataResult<T>
386 where
387 F: FnMut(&mut T, Event<'_>) -> LadduDataResult<()>,
388 {
389 self.try_for_each_event(|ev| f(&mut acc, ev))?;
390 Ok(acc)
391 }
392
393 pub fn accumulate_events<T, F>(&self, acc: T, mut f: F) -> LadduDataResult<T>
400 where
401 F: FnMut(&mut T, Event<'_>),
402 {
403 self.try_accumulate_events(acc, |acc, ev| {
404 f(acc, ev);
405 Ok(())
406 })
407 }
408
409 pub fn sum_weights(&self) -> LadduDataResult<f64> {
416 self.fold_events(0.0, |sum, ev| sum + ev.weight())
417 }
418
419 pub fn weighted_sum<F>(&self, mut f: F) -> LadduDataResult<f64>
426 where
427 F: FnMut(Event<'_>) -> f64,
428 {
429 self.fold_events(0.0, |sum, ev| sum + ev.weight() * f(ev))
430 }
431
432 pub fn weighted_complex_sum<F>(&self, mut f: F) -> LadduDataResult<Complex64>
439 where
440 F: FnMut(Event<'_>) -> Complex64,
441 {
442 self.fold_events(0.0.into(), |sum, ev| sum + ev.weight() * f(ev))
443 }
444
445 pub fn batches(
452 &self,
453 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
454 self.batches_with_plan(self.plan)
455 }
456
457 #[doc(hidden)]
458 pub fn batches_with_plan(
465 &self,
466 mut plan: ReadPlan,
467 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
468 if plan.chunk_size.is_none() {
469 let schema = self.schema()?;
470 let bytes_per_event = 4_u64
471 .saturating_mul(schema.n_p4s() as u64)
472 .saturating_add(schema.n_scalars() as u64)
473 .saturating_add(u64::from(schema.has_weight()))
474 .saturating_mul(size_of::<f64>() as u64);
475 let copies = if self.ops.is_empty() { 1 } else { 2 };
476 let peak_per_event = bytes_per_event.saturating_mul(copies);
477 let state = MemoryState::current();
478 state.refresh();
479 let available = self
480 .memory_budget
481 .resolve(&state.host())
482 .map_err(|error| LadduDataError::Source(error.to_string()))?;
483 let event_limit = self
484 .source
485 .num_events()?
486 .and_then(|events| usize::try_from(events).ok())
487 .unwrap_or(usize::MAX);
488 let decision = MemoryDecision::fit(
489 "dataset read",
490 0,
491 peak_per_event,
492 available,
493 event_limit,
494 "memory-derived streaming",
495 )
496 .map_err(|error| LadduDataError::Source(error.to_string()))?;
497 plan.chunk_size = Some(decision.chunk_events.max(1));
498 *self
499 .last_memory_decision
500 .lock()
501 .unwrap_or_else(|error| error.into_inner()) = Some(decision);
502 }
503 let iter = self.source.batches(plan)?;
504 let ops = Arc::clone(&self.ops);
505
506 Ok(Box::new(iter.scan(0_u64, move |offset, batch| {
507 let batch = match batch {
508 Ok(batch) => batch,
509 Err(err) => return Some(Err(err)),
510 };
511
512 let base = *offset;
513 *offset += batch.len() as u64;
514
515 Some(materialize_batch(&batch, &ops, base))
516 })))
517 }
518
519 pub fn try_for_each_batch<F>(&self, mut f: F) -> LadduDataResult<()>
526 where
527 F: FnMut(EventBatch) -> LadduDataResult<()>,
528 {
529 for batch in self.batches()? {
530 f(batch?)?;
531 }
532
533 Ok(())
534 }
535
536 pub fn map_batches<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
542 where
543 F: FnMut(EventBatch) -> T,
544 {
545 let mut out = Vec::new();
546
547 self.try_for_each_batch(|batch| {
548 out.push(f(batch));
549 Ok(())
550 })?;
551
552 Ok(out)
553 }
554
555 pub fn write_to<S: EventSink>(&self, sink: &mut S) -> LadduDataResult<()> {
562 sink.begin(self.schema()?, WritePlan::from(self.plan))?;
563
564 for batch in self.batches()? {
565 sink.write_batch(&batch?)?;
566 }
567
568 sink.finish()
569 }
570
571 fn push_op(self, op: DatasetOp) -> Self {
572 let mut ops = self.ops.to_vec();
573 ops.push(op);
574
575 Self {
576 source: self.source,
577 plan: self.plan,
578 ops: ops.into(),
579 cache_storage: self.cache_storage,
580 memory_policy: self.memory_policy,
581 memory_budget: self.memory_budget,
582 last_memory_decision: Default::default(),
583 }
584 }
585}
586
587fn eval_batch<F>(batch: &EventBatch, ops: &[DatasetOp], base: u64, mut f: F) -> LadduDataResult<()>
588where
589 F: FnMut(Event<'_>) -> LadduDataResult<()>,
590{
591 'rows: for row in 0..batch.len() {
592 let event_id = base + row as u64;
593 let mut weight = batch.weights_at(row);
594
595 for op in ops {
596 match op {
597 DatasetOp::Filter(pred) => {
598 let ev = Event { batch, row, weight };
599 if !pred(ev) {
600 continue 'rows;
601 }
602 }
603 DatasetOp::Subsample { fraction, seed } => {
604 if uniform_hash_01(*seed, event_id) >= *fraction {
605 continue 'rows;
606 }
607 }
608 DatasetOp::Bootstrap { seed } => {
609 let k = poisson1_from_hash(*seed, event_id);
610 weight *= k as f64;
611 }
612 }
613 }
614
615 f(Event { batch, row, weight })?;
616 }
617 Ok(())
618}
619
620fn materialize_batch(
621 batch: &EventBatch,
622 ops: &[DatasetOp],
623 base: u64,
624) -> LadduDataResult<EventBatch> {
625 if ops.is_empty() {
626 return Ok(batch.clone());
627 }
628
629 let schema = Arc::clone(batch.schema());
630
631 let store_weights = batch.weights_column().is_some()
632 || ops
633 .iter()
634 .any(|op| matches!(op, DatasetOp::Bootstrap { .. }));
635
636 let mut p4s: Vec<Vec<RealVec4>> = (0..schema.n_p4s())
637 .map(|_| Vec::with_capacity(batch.len()))
638 .collect();
639 let mut scalars: Vec<Vec<f64>> = (0..schema.n_scalars())
640 .map(|_| Vec::with_capacity(batch.len()))
641 .collect();
642 let mut weights = if store_weights {
643 Some(Vec::with_capacity(batch.len()))
644 } else {
645 None
646 };
647
648 eval_batch(batch, ops, base, |ev| {
649 for (col, p4) in p4s.iter_mut().enumerate() {
650 p4.push(ev.p4(col));
651 }
652
653 for (col, scalar) in scalars.iter_mut().enumerate() {
654 scalar.push(ev.scalar(col));
655 }
656
657 if let Some(weights) = weights.as_mut() {
658 weights.push(ev.weight());
659 }
660
661 Ok(())
662 })?;
663
664 let p4s = p4s.into_iter().map(Arc::from).collect();
665 let scalars = scalars.into_iter().map(Arc::from).collect();
666 let weights = weights.map(Arc::from);
667
668 EventBatch::new(schema, p4s, scalars, weights)
669}
670
671fn uniform_hash_01(seed: u64, index: u64) -> f64 {
672 let x = splitmix64(seed ^ index);
673 ((x >> 11) as f64) / ((1_u64 << 53) as f64)
674}
675
676fn splitmix64(mut x: u64) -> u64 {
677 x = x.wrapping_add(0x9E3779B97F4A7C15);
678
679 let mut z = x;
680 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
681 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
682 z ^ (z >> 31)
683}
684
685fn poisson1_from_hash(seed: u64, index: u64) -> u32 {
686 let u = uniform_hash_01(seed, index);
687
688 let mut k = 0;
689 let mut p = (-1.0_f64).exp();
690 let mut cdf = p;
691
692 while u > cdf {
693 k += 1;
694 p /= k as f64;
695 cdf += p;
696 }
697
698 k
699}
700
701#[cfg(feature = "parallel")]
702pub mod accurate {
704 use accurate::{sum::Sum2, traits::*};
705 use num::complex::Complex64;
706
707 #[derive(Clone)]
709 pub struct AccurateF64 {
710 sum: Sum2<f64>,
711 }
712
713 impl AccurateF64 {
714 pub fn zero() -> Self {
716 Self { sum: Sum2::zero() }
717 }
718
719 pub fn push(&mut self, value: f64) {
721 let sum = std::mem::replace(&mut self.sum, Sum2::zero());
722 self.sum = sum + value;
723 }
724
725 pub fn merge(&mut self, other: Self) {
727 self.push(other.finish());
728 }
729
730 pub fn finish(self) -> f64 {
732 self.sum.sum()
733 }
734 }
735
736 #[derive(Clone)]
738 pub struct AccurateComplex64 {
739 re: AccurateF64,
740 im: AccurateF64,
741 }
742
743 impl AccurateComplex64 {
744 pub fn zero() -> Self {
746 Self {
747 re: AccurateF64::zero(),
748 im: AccurateF64::zero(),
749 }
750 }
751
752 pub fn push(&mut self, value: Complex64) {
754 self.re.push(value.re);
755 self.im.push(value.im);
756 }
757
758 pub fn merge(&mut self, other: Self) {
760 self.re.merge(other.re);
761 self.im.merge(other.im);
762 }
763
764 pub fn finish(self) -> Complex64 {
766 Complex64::new(self.re.finish(), self.im.finish())
767 }
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use crate::io::memory::MemorySink;
775
776 fn v(x: f64) -> RealVec4 {
777 RealVec4 {
778 e: x + 0.3,
779 px: x,
780 py: x + 0.1,
781 pz: x + 0.2,
782 }
783 }
784
785 fn schema_with_weight() -> Arc<Schema> {
786 Arc::new(Schema::new(["p"], ["x"], true).unwrap())
787 }
788
789 fn schema_without_weight() -> Arc<Schema> {
790 Arc::new(Schema::new(["p"], ["x"], false).unwrap())
791 }
792
793 fn weighted_batch(start: usize, len: usize) -> EventBatch {
794 let schema = schema_with_weight();
795
796 let events = (start..start + len)
797 .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
798
799 EventBatch::from_events(schema, events).unwrap()
800 }
801
802 fn unweighted_batch(start: usize, len: usize) -> EventBatch {
803 let schema = schema_without_weight();
804
805 let events =
806 (start..start + len).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64]));
807
808 EventBatch::from_events(schema, events).unwrap()
809 }
810
811 fn scalar_values(batch: &EventBatch) -> Vec<f64> {
812 batch.scalar_column(0).to_vec()
813 }
814
815 #[test]
816 fn dataset_map_fold_accumulate_complex_sum_and_error_paths_use_transformed_events() {
817 let dataset =
818 Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) % 2.0 == 0.0);
819
820 let rows = dataset
821 .map_events(|ev| (ev.row(), ev.scalar(0), ev.weight()))
822 .unwrap();
823
824 assert_eq!(rows, vec![(0, 0.0, 10.0), (2, 2.0, 12.0), (4, 4.0, 14.0)]);
825
826 let folded = dataset
827 .fold_events(String::new(), |mut out, ev| {
828 out.push_str(&format!("{};", ev.scalar(0)));
829 out
830 })
831 .unwrap();
832
833 assert_eq!(folded, "0;2;4;");
834
835 let accumulated = dataset
836 .accumulate_events(Vec::<f64>::new(), |values, ev| values.push(ev.weight()))
837 .unwrap();
838
839 assert_eq!(accumulated, vec![10.0, 12.0, 14.0]);
840
841 let weighted_sum = dataset.weighted_sum(|ev| ev.scalar(0)).unwrap();
842 assert_eq!(weighted_sum, 0.0 * 10.0 + 2.0 * 12.0 + 4.0 * 14.0);
843
844 let complex_sum = dataset
845 .weighted_complex_sum(|ev| Complex64::new(ev.scalar(0), 1.0))
846 .unwrap();
847
848 assert_eq!(complex_sum.re, weighted_sum);
849 assert_eq!(complex_sum.im, 10.0 + 12.0 + 14.0);
850
851 let err = dataset
852 .try_map_events(|ev| {
853 if ev.scalar(0) == 2.0 {
854 Err(LadduDataError::Unsupported("stop"))
855 } else {
856 Ok(ev.scalar(0))
857 }
858 })
859 .unwrap_err();
860
861 assert!(matches!(err, LadduDataError::Unsupported("stop")));
862 }
863
864 #[test]
865 fn deterministic_subsample_and_bootstrap_use_global_event_ids_across_batches() {
866 let seed = 0x0BAD_5EED;
867 let bootstrap_seed = 0xB007_57A9;
868
869 let dataset = Dataset::from_batches(vec![weighted_batch(0, 3), weighted_batch(3, 3)])
870 .unwrap()
871 .subsample(0.5, seed)
872 .unwrap()
873 .bootstrap(bootstrap_seed);
874
875 let observed = dataset
876 .map_events(|ev| (ev.scalar(0) as u64, ev.weight()))
877 .unwrap();
878
879 let expected: Vec<(u64, f64)> = (0_u64..6)
880 .filter(|&event_id| uniform_hash_01(seed, event_id) < 0.5)
881 .map(|event_id| {
882 let original_weight = 10.0 + event_id as f64;
883 let bootstrap_weight =
884 poisson1_from_hash(bootstrap_seed, event_id) as f64 * original_weight;
885 (event_id, bootstrap_weight)
886 })
887 .collect();
888
889 assert_eq!(observed, expected);
890 }
891
892 #[test]
893 fn materialized_batches_store_weights_only_when_needed() {
894 let unweighted = unweighted_batch(0, 4);
895
896 let filtered = Dataset::from_batch(unweighted.clone())
897 .filter(|ev| ev.scalar(0) >= 1.0)
898 .subsample(1.0, 123)
899 .unwrap();
900
901 let filtered_batch = filtered.batches().unwrap().next().unwrap().unwrap();
902
903 assert_eq!(scalar_values(&filtered_batch), vec![1.0, 2.0, 3.0]);
904 assert!(filtered_batch.weights_column().is_none());
905
906 let bootstrapped = Dataset::from_batch(unweighted).bootstrap(999);
907 let bootstrapped_batch = bootstrapped.batches().unwrap().next().unwrap().unwrap();
908
909 assert!(bootstrapped_batch.weights_column().is_some());
910 }
911
912 #[test]
913 fn write_to_memory_sink_captures_transformed_dataset() {
914 let dataset = Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) >= 2.0);
915
916 let mut sink = MemorySink::new();
917 dataset.write_to(&mut sink).unwrap();
918
919 let captured = sink.into_batch().unwrap();
920
921 assert_eq!(scalar_values(&captured), vec![2.0, 3.0, 4.0]);
922 assert_eq!(captured.weights_column().unwrap(), &[12.0, 13.0, 14.0]);
923 }
924}