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, MemoryState};
13use laddu_physics::vectors::RealVec4;
14use num::complex::Complex64;
15
16static NEXT_DATASET_IDENTITY: AtomicU64 = AtomicU64::new(1);
17
18fn next_dataset_identity() -> u64 {
19 NEXT_DATASET_IDENTITY.fetch_add(1, Ordering::Relaxed)
20}
21
22#[derive(Clone)]
23enum DatasetOp {
24 Filter(Arc<dyn Fn(Event<'_>) -> bool + Send + Sync>),
25 Subsample { fraction: f64, seed: u64 },
26 Bootstrap { seed: u64 },
27}
28
29struct CoalescedBatches {
30 input: Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>,
31 target: usize,
32 pending: Vec<EventBatch>,
33 pending_len: usize,
34 deferred: Option<LadduDataResult<EventBatch>>,
35 finished: bool,
36 stats: Option<Arc<Mutex<DatasetStatsCache>>>,
37 observed_events: u64,
38 observed_sum: f64,
39 observed_correction: f64,
40}
41
42impl CoalescedBatches {
43 fn emit_pending(&mut self) -> Option<LadduDataResult<EventBatch>> {
44 if self.pending.is_empty() {
45 return None;
46 }
47 self.pending_len = 0;
48 let batches = std::mem::take(&mut self.pending);
49 let result = if batches.len() == 1 {
50 Ok(batches.into_iter().next().expect("one pending batch"))
51 } else {
52 EventBatch::concat(&batches)
53 };
54 match &result {
55 Ok(batch) => {
56 self.observed_events = self.observed_events.saturating_add(batch.len() as u64);
57 for row in 0..batch.len() {
58 let corrected = batch.weights_at(row) - self.observed_correction;
59 let next = self.observed_sum + corrected;
60 self.observed_correction = (next - self.observed_sum) - corrected;
61 self.observed_sum = next;
62 }
63 }
64 Err(_) => self.stats = None,
65 }
66 Some(result)
67 }
68
69 fn commit_stats(&mut self) {
70 let Some(stats) = self.stats.take() else {
71 return;
72 };
73 let mut stats = stats.lock().unwrap_or_else(|error| error.into_inner());
74 stats.events = Some(self.observed_events);
75 stats.sum_weights = Some(self.observed_sum);
76 }
77}
78
79impl Iterator for CoalescedBatches {
80 type Item = LadduDataResult<EventBatch>;
81
82 fn next(&mut self) -> Option<Self::Item> {
83 loop {
84 if self.pending_len == self.target {
85 return self.emit_pending();
86 }
87
88 let item = if let Some(item) = self.deferred.take() {
89 Some(item)
90 } else if self.finished {
91 None
92 } else {
93 self.input.next()
94 };
95
96 let Some(item) = item else {
97 self.finished = true;
98 if let Some(batch) = self.emit_pending() {
99 return Some(batch);
100 }
101 self.commit_stats();
102 return None;
103 };
104 let batch = match item {
105 Ok(batch) => batch,
106 Err(error) => {
107 self.stats = None;
108 if self.pending.is_empty() {
109 return Some(Err(error));
110 }
111 self.deferred = Some(Err(error));
112 return self.emit_pending();
113 }
114 };
115 if batch.is_empty() {
116 continue;
117 }
118
119 let available = self.target - self.pending_len;
120 if batch.len() <= available {
121 self.pending_len += batch.len();
122 self.pending.push(batch);
123 continue;
124 }
125
126 self.pending.push(batch.slice(0, available));
127 self.pending_len += available;
128 self.deferred = Some(Ok(batch.slice(available, batch.len())));
129 }
130 }
131}
132
133#[derive(Copy, Clone, Debug, PartialEq)]
135pub struct DatasetStats {
136 events: u64,
137 sum_weights: f64,
138}
139
140impl DatasetStats {
141 pub fn events(&self) -> u64 {
143 self.events
144 }
145
146 pub fn sum_weights(&self) -> f64 {
148 self.sum_weights
149 }
150}
151
152#[derive(Default)]
153struct DatasetStatsCache {
154 events: Option<u64>,
155 sum_weights: Option<f64>,
156}
157
158#[derive(Clone)]
160pub struct Dataset {
161 identity: u64,
162 source: Arc<dyn EventSource>,
163 plan: ReadPlan,
164 ops: Arc<[DatasetOp]>,
165 cache_storage: CacheStorage,
166 memory_policy: MemoryPolicy,
167 memory_budget: MemoryBudget,
168 last_memory_decision: Arc<Mutex<Option<MemoryDecision>>>,
169 stats: Arc<Mutex<DatasetStatsCache>>,
170 source_traversals: Arc<AtomicU64>,
171}
172
173#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
175pub enum CacheStorage {
176 #[default]
178 Resident,
179 Streaming,
181}
182
183#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
185pub enum MemoryPolicy {
186 #[default]
188 Fastest,
189 Resident,
191 Streaming,
193}
194
195impl Dataset {
196 pub fn new<S>(source: S) -> Self
198 where
199 S: EventSource + 'static,
200 {
201 Self {
202 identity: next_dataset_identity(),
203 source: Arc::new(source),
204 plan: ReadPlan::default(),
205 ops: Arc::from([]),
206 cache_storage: CacheStorage::Resident,
207 memory_policy: MemoryPolicy::Fastest,
208 memory_budget: MemoryBudget::Auto,
209 last_memory_decision: Default::default(),
210 stats: Default::default(),
211 source_traversals: Default::default(),
212 }
213 }
214
215 pub fn from_arc(source: Arc<dyn EventSource>) -> Self {
217 Self {
218 identity: next_dataset_identity(),
219 source,
220 plan: ReadPlan::default(),
221 ops: Arc::from([]),
222 cache_storage: CacheStorage::Resident,
223 memory_policy: MemoryPolicy::Fastest,
224 memory_budget: MemoryBudget::Auto,
225 last_memory_decision: Default::default(),
226 stats: Default::default(),
227 source_traversals: Default::default(),
228 }
229 }
230
231 #[doc(hidden)]
233 pub fn with_derived_source<S>(&self, source: S) -> Self
234 where
235 S: EventSource + 'static,
236 {
237 Self {
238 identity: next_dataset_identity(),
239 source: Arc::new(source),
240 plan: self.plan,
241 ops: Arc::from([]),
242 cache_storage: self.cache_storage,
243 memory_policy: self.memory_policy,
244 memory_budget: self.memory_budget,
245 last_memory_decision: Default::default(),
246 stats: Default::default(),
247 source_traversals: Default::default(),
248 }
249 }
250
251 pub fn from_batch(batch: EventBatch) -> Self {
253 Self::new(MemorySource::new(batch))
254 }
255
256 pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
262 Ok(Self::new(MemorySource::from_batches(batches)?))
263 }
264
265 pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
271 where
272 I: IntoIterator<Item = OwnedEvent>,
273 {
274 Ok(Self::new(MemorySource::from_events(schema, events)?))
275 }
276
277 pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
284 self.source.schema()
285 }
286
287 pub fn capabilities(&self) -> SourceCapabilities {
289 self.source.capabilities()
290 }
291
292 pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
298 {
299 let stats = self.stats.lock().unwrap_or_else(|error| error.into_inner());
300 if let Some(events) = stats.events {
301 return Ok(Some(events));
302 }
303 }
304
305 if self
306 .ops
307 .iter()
308 .any(|op| !matches!(op, DatasetOp::Bootstrap { .. }))
309 {
310 return Ok(None);
311 }
312
313 let events = self.source.num_events()?;
314 if let Some(events) = events {
315 self.stats
316 .lock()
317 .unwrap_or_else(|error| error.into_inner())
318 .events = Some(events);
319 }
320 Ok(events)
321 }
322
323 pub fn stats(&self) -> LadduDataResult<DatasetStats> {
332 {
333 let cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
334 if let (Some(events), Some(sum_weights)) = (cache.events, cache.sum_weights) {
335 return Ok(DatasetStats {
336 events,
337 sum_weights,
338 });
339 }
340 }
341
342 if self.ops.is_empty()
343 && let (Some(events), Some(sum_weights)) =
344 (self.source.num_events()?, self.source.weighted_total()?)
345 {
346 let stats = DatasetStats {
347 events,
348 sum_weights,
349 };
350 let mut cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
351 cache.events = Some(events);
352 cache.sum_weights = Some(sum_weights);
353 return Ok(stats);
354 }
355
356 let mut events = 0_u64;
357 let mut sum = 0.0;
358 let mut correction = 0.0;
359 for batch in self.batches()? {
360 let batch = batch?;
361 events = events.saturating_add(batch.len() as u64);
362 for row in 0..batch.len() {
363 let weight = batch.weights_at(row);
364 let corrected = weight - correction;
365 let next = sum + corrected;
366 correction = (next - sum) - corrected;
367 sum = next;
368 }
369 }
370
371 let stats = DatasetStats {
372 events,
373 sum_weights: sum,
374 };
375 let mut cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
376 cache.events = Some(events);
377 cache.sum_weights = Some(sum);
378 Ok(stats)
379 }
380
381 pub fn read_plan(&self) -> ReadPlan {
383 self.plan
384 }
385
386 pub fn cache_storage(&self) -> CacheStorage {
388 self.cache_storage
389 }
390
391 pub fn memory_policy(&self) -> MemoryPolicy {
393 self.memory_policy
394 }
395
396 pub fn memory_budget(&self) -> MemoryBudget {
398 self.memory_budget
399 }
400
401 pub fn last_memory_decision(&self) -> Option<MemoryDecision> {
403 self.last_memory_decision
404 .lock()
405 .unwrap_or_else(|error| error.into_inner())
406 .clone()
407 }
408
409 pub fn source_traversals(&self) -> u64 {
411 self.source_traversals.load(Ordering::Relaxed)
412 }
413
414 #[doc(hidden)]
419 pub fn identity(&self) -> u64 {
420 self.identity
421 }
422
423 pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
425 self.memory_budget = budget;
426 self
427 }
428
429 pub fn fastest(mut self) -> Self {
431 self.memory_policy = MemoryPolicy::Fastest;
432 self.cache_storage = CacheStorage::Resident;
433 self
434 }
435
436 pub fn resident(mut self) -> Self {
442 self.memory_policy = MemoryPolicy::Resident;
443 self.cache_storage = CacheStorage::Resident;
444 self
445 }
446
447 pub fn streaming(mut self) -> Self {
452 self.memory_policy = MemoryPolicy::Streaming;
453 self.cache_storage = CacheStorage::Streaming;
454 self
455 }
456
457 pub fn chunked(mut self, chunk_size: usize) -> LadduDataResult<Self> {
467 if chunk_size == 0 {
468 return Err(LadduDataError::InvalidArgument(
469 "chunk_size must be nonzero",
470 ));
471 }
472 self.plan.chunk_size = Some(chunk_size);
473 Ok(self)
474 }
475
476 pub fn unchunked(mut self) -> Self {
478 self.plan.chunk_size = None;
479 self
480 }
481
482 pub fn filter<F>(self, f: F) -> Self
484 where
485 F: Fn(Event<'_>) -> bool + Send + Sync + 'static,
486 {
487 self.push_op(DatasetOp::Filter(Arc::new(f)))
488 }
489
490 pub fn subsample(self, fraction: f64, seed: u64) -> LadduDataResult<Self> {
497 if !(0.0..=1.0).contains(&fraction) {
498 return Err(LadduDataError::InvalidArgument(
499 "fraction must be in [0, 1]",
500 ));
501 }
502
503 Ok(self.push_op(DatasetOp::Subsample { fraction, seed }))
504 }
505
506 pub fn bootstrap(self, seed: u64) -> Self {
508 self.push_op(DatasetOp::Bootstrap { seed })
509 }
510
511 pub fn for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
518 where
519 F: FnMut(Event<'_>),
520 {
521 self.try_for_each_event(|ev| {
522 f(ev);
523 Ok(())
524 })
525 }
526
527 pub fn try_for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
534 where
535 F: FnMut(Event<'_>) -> LadduDataResult<()>,
536 {
537 let mut offset = 0_u64;
538 let mut events = 0_u64;
539 let mut sum = 0.0;
540 let mut correction = 0.0;
541
542 self.source_traversals.fetch_add(1, Ordering::Relaxed);
543 for batch in self.source.batches(self.plan)? {
544 let batch = batch?;
545 let base = offset;
546 offset += batch.len() as u64;
547 eval_batch(&batch, &self.ops, base, |event| {
548 f(event)?;
549 events = events.saturating_add(1);
550 let corrected = event.weight() - correction;
551 let next = sum + corrected;
552 correction = (next - sum) - corrected;
553 sum = next;
554 Ok(())
555 })?;
556 }
557
558 let mut stats = self.stats.lock().unwrap_or_else(|error| error.into_inner());
559 stats.events = Some(events);
560 stats.sum_weights = Some(sum);
561
562 Ok(())
563 }
564
565 pub fn try_map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
572 where
573 F: FnMut(Event<'_>) -> LadduDataResult<T>,
574 {
575 let mut out = Vec::new();
576 self.try_for_each_event(|ev| {
577 out.push(f(ev)?);
578 Ok(())
579 })?;
580
581 Ok(out)
582 }
583
584 pub fn map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
591 where
592 F: FnMut(Event<'_>) -> T,
593 {
594 let mut out = Vec::new();
595 self.try_for_each_event(|ev| {
596 out.push(f(ev));
597 Ok(())
598 })?;
599
600 Ok(out)
601 }
602
603 pub fn try_fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
610 where
611 F: FnMut(T, Event<'_>) -> LadduDataResult<T>,
612 {
613 let mut acc = Some(init);
614
615 self.try_for_each_event(|ev| {
616 let current = acc.take().ok_or_else(|| {
617 LadduDataError::Source("dataset fold accumulator was consumed".into())
618 })?;
619 acc = Some(f(current, ev)?);
620 Ok(())
621 })?;
622
623 acc.ok_or_else(|| LadduDataError::Source("dataset fold produced no accumulator".into()))
624 }
625
626 pub fn fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
633 where
634 F: FnMut(T, Event<'_>) -> T,
635 {
636 self.try_fold_events(init, |acc, ev| Ok(f(acc, ev)))
637 }
638
639 pub fn try_accumulate_events<T, F>(&self, mut acc: T, mut f: F) -> LadduDataResult<T>
646 where
647 F: FnMut(&mut T, Event<'_>) -> LadduDataResult<()>,
648 {
649 self.try_for_each_event(|ev| f(&mut acc, ev))?;
650 Ok(acc)
651 }
652
653 pub fn accumulate_events<T, F>(&self, acc: T, mut f: F) -> LadduDataResult<T>
660 where
661 F: FnMut(&mut T, Event<'_>),
662 {
663 self.try_accumulate_events(acc, |acc, ev| {
664 f(acc, ev);
665 Ok(())
666 })
667 }
668
669 pub fn sum_weights(&self) -> LadduDataResult<f64> {
676 Ok(self.stats()?.sum_weights())
677 }
678
679 pub fn weighted_sum<F>(&self, mut f: F) -> LadduDataResult<f64>
686 where
687 F: FnMut(Event<'_>) -> f64,
688 {
689 self.fold_events(0.0, |sum, ev| sum + ev.weight() * f(ev))
690 }
691
692 pub fn weighted_complex_sum<F>(&self, mut f: F) -> LadduDataResult<Complex64>
699 where
700 F: FnMut(Event<'_>) -> Complex64,
701 {
702 self.fold_events(0.0.into(), |sum, ev| sum + ev.weight() * f(ev))
703 }
704
705 pub fn batches(
712 &self,
713 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
714 self.batches_with_plan(self.plan)
715 }
716
717 #[doc(hidden)]
718 pub fn batches_with_plan(
725 &self,
726 mut plan: ReadPlan,
727 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
728 if plan.chunk_size.is_none() {
729 let schema = self.schema()?;
730 let bytes_per_event = 4_u64
731 .saturating_mul(schema.n_p4s() as u64)
732 .saturating_add(schema.n_scalars() as u64)
733 .saturating_add(u64::from(schema.has_weight()))
734 .saturating_mul(size_of::<f64>() as u64);
735 let copies = if self.ops.is_empty() { 1 } else { 2 };
736 let peak_per_event = bytes_per_event.saturating_mul(copies);
737 let state = MemoryState::current();
738 state.refresh();
739 let available = self
740 .memory_budget
741 .resolve(&state.host())
742 .map_err(|error| LadduDataError::Source(error.to_string()))?;
743 let event_limit = self
744 .source
745 .num_events()?
746 .and_then(|events| usize::try_from(events).ok())
747 .unwrap_or(usize::MAX);
748 let decision = MemoryDecision::fit(
749 "dataset read",
750 0,
751 peak_per_event,
752 available,
753 event_limit,
754 "memory-derived streaming",
755 )
756 .map_err(|error| LadduDataError::Source(error.to_string()))?;
757 plan.chunk_size = Some(decision.chunk_events.max(1));
758 *self
759 .last_memory_decision
760 .lock()
761 .unwrap_or_else(|error| error.into_inner()) = Some(decision);
762 }
763 self.source_traversals.fetch_add(1, Ordering::Relaxed);
764 let iter = self.source.batches(plan)?;
765 let ops = Arc::clone(&self.ops);
766
767 let transformed = Box::new(iter.scan(0_u64, move |offset, batch| {
768 let batch = match batch {
769 Ok(batch) => batch,
770 Err(err) => return Some(Err(err)),
771 };
772
773 let base = *offset;
774 *offset += batch.len() as u64;
775
776 Some(materialize_batch(&batch, &ops, base))
777 }));
778 Ok(Box::new(CoalescedBatches {
779 input: transformed,
780 target: plan.chunk_size.unwrap_or(usize::MAX).max(1),
781 pending: Vec::new(),
782 pending_len: 0,
783 deferred: None,
784 finished: false,
785 stats: (!plan.is_distributed()).then(|| Arc::clone(&self.stats)),
786 observed_events: 0,
787 observed_sum: 0.0,
788 observed_correction: 0.0,
789 }))
790 }
791
792 pub fn try_for_each_batch<F>(&self, mut f: F) -> LadduDataResult<()>
799 where
800 F: FnMut(EventBatch) -> LadduDataResult<()>,
801 {
802 for batch in self.batches()? {
803 f(batch?)?;
804 }
805
806 Ok(())
807 }
808
809 pub fn map_batches<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
815 where
816 F: FnMut(EventBatch) -> T,
817 {
818 let mut out = Vec::new();
819
820 self.try_for_each_batch(|batch| {
821 out.push(f(batch));
822 Ok(())
823 })?;
824
825 Ok(out)
826 }
827
828 pub fn write_to<S: EventSink>(&self, sink: &mut S) -> LadduDataResult<()> {
835 sink.begin(self.schema()?, WritePlan::from(self.plan))?;
836
837 for batch in self.batches()? {
838 sink.write_batch(&batch?)?;
839 }
840
841 sink.finish()
842 }
843
844 fn push_op(self, op: DatasetOp) -> Self {
845 let preserved_events = if matches!(&op, DatasetOp::Bootstrap { .. }) {
846 self.num_events().ok().flatten()
847 } else {
848 None
849 };
850 let mut ops = self.ops.to_vec();
851 ops.push(op);
852
853 Self {
854 identity: next_dataset_identity(),
855 source: self.source,
856 plan: self.plan,
857 ops: ops.into(),
858 cache_storage: self.cache_storage,
859 memory_policy: self.memory_policy,
860 memory_budget: self.memory_budget,
861 last_memory_decision: Default::default(),
862 stats: Arc::new(Mutex::new(DatasetStatsCache {
863 events: preserved_events,
864 sum_weights: None,
865 })),
866 source_traversals: Default::default(),
867 }
868 }
869}
870
871fn eval_batch<F>(batch: &EventBatch, ops: &[DatasetOp], base: u64, mut f: F) -> LadduDataResult<()>
872where
873 F: FnMut(Event<'_>) -> LadduDataResult<()>,
874{
875 'rows: for row in 0..batch.len() {
876 let event_id = base + row as u64;
877 let mut weight = batch.weights_at(row);
878
879 for op in ops {
880 match op {
881 DatasetOp::Filter(pred) => {
882 let ev = Event { batch, row, weight };
883 if !pred(ev) {
884 continue 'rows;
885 }
886 }
887 DatasetOp::Subsample { fraction, seed } => {
888 if uniform_hash_01(*seed, event_id) >= *fraction {
889 continue 'rows;
890 }
891 }
892 DatasetOp::Bootstrap { seed } => {
893 let k = poisson1_from_hash(*seed, event_id);
894 weight *= k as f64;
895 }
896 }
897 }
898
899 f(Event { batch, row, weight })?;
900 }
901 Ok(())
902}
903
904fn materialize_batch(
905 batch: &EventBatch,
906 ops: &[DatasetOp],
907 base: u64,
908) -> LadduDataResult<EventBatch> {
909 if ops.is_empty() {
910 return Ok(batch.clone());
911 }
912
913 let schema = Arc::clone(batch.schema());
914
915 let store_weights = batch.weights_column().is_some()
916 || ops
917 .iter()
918 .any(|op| matches!(op, DatasetOp::Bootstrap { .. }));
919
920 let mut p4s: Vec<Vec<RealVec4>> = (0..schema.n_p4s())
921 .map(|_| Vec::with_capacity(batch.len()))
922 .collect();
923 let mut scalars: Vec<Vec<f64>> = (0..schema.n_scalars())
924 .map(|_| Vec::with_capacity(batch.len()))
925 .collect();
926 let mut weights = if store_weights {
927 Some(Vec::with_capacity(batch.len()))
928 } else {
929 None
930 };
931
932 eval_batch(batch, ops, base, |ev| {
933 for (col, p4) in p4s.iter_mut().enumerate() {
934 p4.push(ev.p4(col));
935 }
936
937 for (col, scalar) in scalars.iter_mut().enumerate() {
938 scalar.push(ev.scalar(col));
939 }
940
941 if let Some(weights) = weights.as_mut() {
942 weights.push(ev.weight());
943 }
944
945 Ok(())
946 })?;
947
948 let p4s = p4s.into_iter().map(Arc::from).collect();
949 let scalars = scalars.into_iter().map(Arc::from).collect();
950 let weights = weights.map(Arc::from);
951
952 EventBatch::new(schema, p4s, scalars, weights)
953}
954
955fn uniform_hash_01(seed: u64, index: u64) -> f64 {
956 let x = splitmix64(seed ^ index);
957 ((x >> 11) as f64) / ((1_u64 << 53) as f64)
958}
959
960fn splitmix64(mut x: u64) -> u64 {
961 x = x.wrapping_add(0x9E3779B97F4A7C15);
962
963 let mut z = x;
964 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
965 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
966 z ^ (z >> 31)
967}
968
969fn poisson1_from_hash(seed: u64, index: u64) -> u32 {
970 let u = uniform_hash_01(seed, index);
971
972 let mut k = 0;
973 let mut p = (-1.0_f64).exp();
974 let mut cdf = p;
975
976 while u > cdf {
977 k += 1;
978 p /= k as f64;
979 cdf += p;
980 }
981
982 k
983}
984
985#[cfg(feature = "parallel")]
986pub mod accurate {
988 use accurate::{sum::Sum2, traits::*};
989 use num::complex::Complex64;
990
991 #[derive(Clone)]
993 pub struct AccurateF64 {
994 sum: Sum2<f64>,
995 }
996
997 impl AccurateF64 {
998 pub fn zero() -> Self {
1000 Self { sum: Sum2::zero() }
1001 }
1002
1003 pub fn push(&mut self, value: f64) {
1005 let sum = std::mem::replace(&mut self.sum, Sum2::zero());
1006 self.sum = sum + value;
1007 }
1008
1009 pub fn merge(&mut self, other: Self) {
1011 self.push(other.finish());
1012 }
1013
1014 pub fn finish(self) -> f64 {
1016 self.sum.sum()
1017 }
1018 }
1019
1020 #[derive(Clone)]
1022 pub struct AccurateComplex64 {
1023 re: AccurateF64,
1024 im: AccurateF64,
1025 }
1026
1027 impl AccurateComplex64 {
1028 pub fn zero() -> Self {
1030 Self {
1031 re: AccurateF64::zero(),
1032 im: AccurateF64::zero(),
1033 }
1034 }
1035
1036 pub fn push(&mut self, value: Complex64) {
1038 self.re.push(value.re);
1039 self.im.push(value.im);
1040 }
1041
1042 pub fn merge(&mut self, other: Self) {
1044 self.re.merge(other.re);
1045 self.im.merge(other.im);
1046 }
1047
1048 pub fn finish(self) -> Complex64 {
1050 Complex64::new(self.re.finish(), self.im.finish())
1051 }
1052 }
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use super::*;
1058 use crate::io::{EventSource, ReadPlan, memory::MemorySink};
1059 use std::sync::atomic::{AtomicUsize, Ordering};
1060
1061 #[derive(Clone)]
1062 struct CountingSource {
1063 batch: EventBatch,
1064 reads: Arc<AtomicUsize>,
1065 }
1066
1067 impl EventSource for CountingSource {
1068 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
1069 Ok(Arc::clone(self.batch.schema()))
1070 }
1071
1072 fn batches(
1073 &self,
1074 _plan: ReadPlan,
1075 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
1076 self.reads.fetch_add(1, Ordering::Relaxed);
1077 Ok(Box::new(std::iter::once(Ok(self.batch.clone()))))
1078 }
1079 }
1080
1081 fn v(x: f64) -> RealVec4 {
1082 RealVec4 {
1083 e: x + 0.3,
1084 px: x,
1085 py: x + 0.1,
1086 pz: x + 0.2,
1087 }
1088 }
1089
1090 fn schema_with_weight() -> Arc<Schema> {
1091 Arc::new(Schema::new(["p"], ["x"], true).unwrap())
1092 }
1093
1094 fn schema_without_weight() -> Arc<Schema> {
1095 Arc::new(Schema::new(["p"], ["x"], false).unwrap())
1096 }
1097
1098 fn weighted_batch(start: usize, len: usize) -> EventBatch {
1099 let schema = schema_with_weight();
1100
1101 let events = (start..start + len)
1102 .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
1103
1104 EventBatch::from_events(schema, events).unwrap()
1105 }
1106
1107 fn unweighted_batch(start: usize, len: usize) -> EventBatch {
1108 let schema = schema_without_weight();
1109
1110 let events =
1111 (start..start + len).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64]));
1112
1113 EventBatch::from_events(schema, events).unwrap()
1114 }
1115
1116 fn scalar_values(batch: &EventBatch) -> Vec<f64> {
1117 batch.scalar_column(0).to_vec()
1118 }
1119
1120 #[test]
1121 fn dataset_statistics_are_shared_per_view_and_invalidated_by_selection() {
1122 let reads = Arc::new(AtomicUsize::new(0));
1123 let dataset = Dataset::new(CountingSource {
1124 batch: weighted_batch(0, 5),
1125 reads: Arc::clone(&reads),
1126 });
1127 let clone = dataset.clone();
1128
1129 assert_eq!(dataset.num_events().unwrap(), None);
1130 assert_eq!(dataset.stats().unwrap().events(), 5);
1131 assert_eq!(clone.sum_weights().unwrap(), 60.0);
1132 assert_eq!(clone.num_events().unwrap(), Some(5));
1133 assert_eq!(reads.load(Ordering::Relaxed), 1);
1134
1135 let bootstrapped = dataset.clone().bootstrap(7);
1136 assert_eq!(bootstrapped.num_events().unwrap(), Some(5));
1137 assert_eq!(reads.load(Ordering::Relaxed), 1);
1138
1139 let filtered = dataset.filter(|event| event.scalar(0) >= 2.0);
1140 assert_eq!(filtered.num_events().unwrap(), None);
1141 assert_eq!(
1142 filtered.map_events(|event| event.scalar(0)).unwrap(),
1143 [2.0, 3.0, 4.0]
1144 );
1145 assert_eq!(reads.load(Ordering::Relaxed), 2);
1146 assert_eq!(filtered.stats().unwrap().events(), 3);
1147 assert_eq!(reads.load(Ordering::Relaxed), 2);
1148 }
1149
1150 #[test]
1151 fn transformed_fragments_are_coalesced_to_the_read_chunk_size() {
1152 let fragments = (0..10)
1153 .map(|index| weighted_batch(index, 1))
1154 .collect::<Vec<_>>();
1155 let dataset = Dataset::from_batches(fragments)
1156 .unwrap()
1157 .filter(|_| true)
1158 .chunked(4)
1159 .unwrap();
1160
1161 let batches = dataset
1162 .batches()
1163 .unwrap()
1164 .collect::<LadduDataResult<Vec<_>>>()
1165 .unwrap();
1166 assert_eq!(
1167 batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
1168 [4, 4, 2]
1169 );
1170 assert_eq!(
1171 batches
1172 .iter()
1173 .flat_map(|batch| batch.scalar_column(0).iter().copied())
1174 .collect::<Vec<_>>(),
1175 (0..10).map(|value| value as f64).collect::<Vec<_>>()
1176 );
1177 }
1178
1179 #[test]
1180 fn dataset_map_fold_accumulate_complex_sum_and_error_paths_use_transformed_events() {
1181 let dataset =
1182 Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) % 2.0 == 0.0);
1183
1184 let rows = dataset
1185 .map_events(|ev| (ev.row(), ev.scalar(0), ev.weight()))
1186 .unwrap();
1187
1188 assert_eq!(rows, vec![(0, 0.0, 10.0), (2, 2.0, 12.0), (4, 4.0, 14.0)]);
1189
1190 let folded = dataset
1191 .fold_events(String::new(), |mut out, ev| {
1192 out.push_str(&format!("{};", ev.scalar(0)));
1193 out
1194 })
1195 .unwrap();
1196
1197 assert_eq!(folded, "0;2;4;");
1198
1199 let accumulated = dataset
1200 .accumulate_events(Vec::<f64>::new(), |values, ev| values.push(ev.weight()))
1201 .unwrap();
1202
1203 assert_eq!(accumulated, vec![10.0, 12.0, 14.0]);
1204
1205 let weighted_sum = dataset.weighted_sum(|ev| ev.scalar(0)).unwrap();
1206 assert_eq!(weighted_sum, 0.0 * 10.0 + 2.0 * 12.0 + 4.0 * 14.0);
1207
1208 let complex_sum = dataset
1209 .weighted_complex_sum(|ev| Complex64::new(ev.scalar(0), 1.0))
1210 .unwrap();
1211
1212 assert_eq!(complex_sum.re, weighted_sum);
1213 assert_eq!(complex_sum.im, 10.0 + 12.0 + 14.0);
1214
1215 let err = dataset
1216 .try_map_events(|ev| {
1217 if ev.scalar(0) == 2.0 {
1218 Err(LadduDataError::Unsupported("stop"))
1219 } else {
1220 Ok(ev.scalar(0))
1221 }
1222 })
1223 .unwrap_err();
1224
1225 assert!(matches!(err, LadduDataError::Unsupported("stop")));
1226 }
1227
1228 #[test]
1229 fn deterministic_subsample_and_bootstrap_use_global_event_ids_across_batches() {
1230 let seed = 0x0BAD_5EED;
1231 let bootstrap_seed = 0xB007_57A9;
1232
1233 let dataset = Dataset::from_batches(vec![weighted_batch(0, 3), weighted_batch(3, 3)])
1234 .unwrap()
1235 .subsample(0.5, seed)
1236 .unwrap()
1237 .bootstrap(bootstrap_seed);
1238
1239 let observed = dataset
1240 .map_events(|ev| (ev.scalar(0) as u64, ev.weight()))
1241 .unwrap();
1242
1243 let expected: Vec<(u64, f64)> = (0_u64..6)
1244 .filter(|&event_id| uniform_hash_01(seed, event_id) < 0.5)
1245 .map(|event_id| {
1246 let original_weight = 10.0 + event_id as f64;
1247 let bootstrap_weight =
1248 poisson1_from_hash(bootstrap_seed, event_id) as f64 * original_weight;
1249 (event_id, bootstrap_weight)
1250 })
1251 .collect();
1252
1253 assert_eq!(observed, expected);
1254 }
1255
1256 #[test]
1257 fn materialized_batches_store_weights_only_when_needed() {
1258 let unweighted = unweighted_batch(0, 4);
1259
1260 let filtered = Dataset::from_batch(unweighted.clone())
1261 .filter(|ev| ev.scalar(0) >= 1.0)
1262 .subsample(1.0, 123)
1263 .unwrap();
1264
1265 let filtered_batch = filtered.batches().unwrap().next().unwrap().unwrap();
1266
1267 assert_eq!(scalar_values(&filtered_batch), vec![1.0, 2.0, 3.0]);
1268 assert!(filtered_batch.weights_column().is_none());
1269
1270 let bootstrapped = Dataset::from_batch(unweighted).bootstrap(999);
1271 let bootstrapped_batch = bootstrapped.batches().unwrap().next().unwrap().unwrap();
1272
1273 assert!(bootstrapped_batch.weights_column().is_some());
1274 }
1275
1276 #[test]
1277 fn write_to_memory_sink_captures_transformed_dataset() {
1278 let dataset = Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) >= 2.0);
1279
1280 let mut sink = MemorySink::new();
1281 dataset.write_to(&mut sink).unwrap();
1282
1283 let captured = sink.into_batch().unwrap();
1284
1285 assert_eq!(scalar_values(&captured), vec![2.0, 3.0, 4.0]);
1286 assert_eq!(captured.weights_column().unwrap(), &[12.0, 13.0, 14.0]);
1287 }
1288
1289 #[test]
1290 fn immutable_dataset_identity_tracks_semantic_views() {
1291 let dataset = Dataset::from_batch(weighted_batch(0, 3));
1292 assert_eq!(dataset.identity(), dataset.clone().identity());
1293 assert_eq!(dataset.identity(), dataset.clone().streaming().identity());
1294 assert_ne!(
1295 dataset.identity(),
1296 dataset.clone().subsample(1.0, 7).unwrap().identity()
1297 );
1298 assert_ne!(dataset.identity(), dataset.clone().bootstrap(7).identity());
1299 assert_ne!(
1300 dataset.identity(),
1301 dataset.clone().filter(|_| true).identity()
1302 );
1303 }
1304}