Skip to main content

laddu_data/data/
dataset.rs

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/// Lazy event dataset combining a source, read plan, and row transformations.
21#[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/// Memory policy for compiled event-dependent model caches.
33#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
34pub enum CacheStorage {
35    /// Materialize all event-dependent cache values once and retain them for repeated evaluations.
36    #[default]
37    Resident,
38    /// Retain only dataset statistics and rebuild each batch cache during every evaluation.
39    Streaming,
40}
41
42/// Strategy used to trade retained memory for execution speed.
43#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
44pub enum MemoryPolicy {
45    /// Select the fastest supported resident or streaming strategy that fits.
46    #[default]
47    Fastest,
48    /// Require the complete compiled event cache to remain resident.
49    Resident,
50    /// Retain no compiled event cache between traversals.
51    Streaming,
52}
53
54impl Dataset {
55    /// Creates a dataset from an event source.
56    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    /// Creates a dataset from a shared dynamically dispatched source.
72    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    /// Build a derived dataset while preserving this dataset's read and cache policy.
85    #[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    /// Creates an in-memory dataset from one batch.
102    pub fn from_batch(batch: EventBatch) -> Self {
103        Self::new(MemorySource::new(batch))
104    }
105
106    /// Creates an in-memory dataset from schema-compatible batches.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`LadduDataError`] when batch schemas are incompatible.
111    pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
112        Ok(Self::new(MemorySource::from_batches(batches)?))
113    }
114
115    /// Collects owned events into an in-memory dataset.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`LadduDataError`] when an event does not match `schema`.
120    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    /// Returns the source schema.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`LadduDataError`] when the underlying source cannot determine
132    /// or load its schema.
133    pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
134        self.source.schema()
135    }
136
137    /// Returns source planning capabilities.
138    pub fn capabilities(&self) -> SourceCapabilities {
139        self.source.capabilities()
140    }
141
142    /// Returns the source event count when cheaply available.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error when source metadata cannot be read.
147    pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
148        self.source.num_events()
149    }
150
151    /// Returns the current read plan.
152    pub fn read_plan(&self) -> ReadPlan {
153        self.plan
154    }
155
156    /// Returns the compiled-cache memory policy.
157    pub fn cache_storage(&self) -> CacheStorage {
158        self.cache_storage
159    }
160
161    /// Returns the memory-first cache selection policy.
162    pub fn memory_policy(&self) -> MemoryPolicy {
163        self.memory_policy
164    }
165
166    /// Returns the dataset's host-memory budget.
167    pub fn memory_budget(&self) -> MemoryBudget {
168        self.memory_budget
169    }
170
171    /// Returns the most recent memory-derived read decision.
172    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    /// Returns this dataset with a host-memory budget.
180    pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
181        self.memory_budget = budget;
182        self
183    }
184
185    /// Select the fastest strategy allowed by the active memory budget.
186    pub fn fastest(mut self) -> Self {
187        self.memory_policy = MemoryPolicy::Fastest;
188        self.cache_storage = CacheStorage::Resident;
189        self
190    }
191
192    /// Retain compiled event-dependent values for every local event.
193    ///
194    /// This strict policy is intended for repeatedly evaluating a likelihood
195    /// and returns an error if the resident cache cannot fit. [`Dataset::fastest`]
196    /// is the default.
197    pub fn resident(mut self) -> Self {
198        self.memory_policy = MemoryPolicy::Resident;
199        self.cache_storage = CacheStorage::Resident;
200        self
201    }
202
203    /// Re-read the source and rebuild one batch cache during each parameter evaluation.
204    ///
205    /// Only fixed dataset statistics are retained. This minimizes memory use but requires a
206    /// repeatable source and is expected to be slower than [`Dataset::resident`].
207    pub fn streaming(mut self) -> Self {
208        self.memory_policy = MemoryPolicy::Streaming;
209        self.cache_storage = CacheStorage::Streaming;
210        self
211    }
212
213    /// Returns this dataset with a low-level nonzero maximum event count.
214    ///
215    /// Prefer [`Dataset::with_memory_budget`] for portable application code.
216    /// This explicit read-plan override remains available for source debugging
217    /// and reproducibility and is always capped by execution memory planning.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`LadduDataError::InvalidArgument`] when `chunk_size` is zero.
222    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    /// Returns this dataset with source-native batch sizes.
233    pub fn unchunked(mut self) -> Self {
234        self.plan.chunk_size = None;
235        self
236    }
237
238    /// Lazily retains events satisfying `f`.
239    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    /// Lazily retains a deterministic fraction of events.
247    ///
248    /// # Errors
249    ///
250    /// Returns [`LadduDataError::InvalidArgument`] when `fraction` is outside
251    /// `[0, 1]` or is NaN.
252    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    /// Applies deterministic Poisson bootstrap multiplicities to event weights.
263    pub fn bootstrap(self, seed: u64) -> Self {
264        self.push_op(DatasetOp::Bootstrap { seed })
265    }
266
267    /// Visits each transformed event.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`LadduDataError`] when reading or transforming the source
272    /// fails.
273    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    /// Visits each transformed event and stops at the first error.
284    ///
285    /// # Errors
286    ///
287    /// Returns the first [`LadduDataError`] produced by the source,
288    /// transformations, or callback.
289    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    /// Fallibly maps transformed events into a vector.
306    ///
307    /// # Errors
308    ///
309    /// Returns the first [`LadduDataError`] produced while reading,
310    /// transforming, or mapping an event.
311    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    /// Maps transformed events into a vector.
325    ///
326    /// # Errors
327    ///
328    /// Returns [`LadduDataError`] when reading or transforming the source
329    /// fails.
330    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    /// Fallibly folds transformed events into an owned accumulator.
344    ///
345    /// # Errors
346    ///
347    /// Returns the first [`LadduDataError`] produced while reading,
348    /// transforming, or folding an event.
349    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    /// Folds transformed events into an owned accumulator.
367    ///
368    /// # Errors
369    ///
370    /// Returns [`LadduDataError`] when reading or transforming the source
371    /// fails.
372    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    /// Fallibly updates a mutable accumulator for every transformed event.
380    ///
381    /// # Errors
382    ///
383    /// Returns the first [`LadduDataError`] produced while reading,
384    /// transforming, or accumulating an event.
385    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    /// Updates a mutable accumulator for every transformed event.
394    ///
395    /// # Errors
396    ///
397    /// Returns [`LadduDataError`] when reading or transforming the source
398    /// fails.
399    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    /// Sums effective event weights.
410    ///
411    /// # Errors
412    ///
413    /// Returns [`LadduDataError`] when reading or transforming the source
414    /// fails.
415    pub fn sum_weights(&self) -> LadduDataResult<f64> {
416        self.fold_events(0.0, |sum, ev| sum + ev.weight())
417    }
418
419    /// Sums `weight * f(event)` over transformed events.
420    ///
421    /// # Errors
422    ///
423    /// Returns [`LadduDataError`] when reading or transforming the source
424    /// fails.
425    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    /// Sums complex `weight * f(event)` contributions.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`LadduDataError`] when reading or transforming the source
437    /// fails.
438    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    /// Opens an iterator of fully transformed event batches.
446    ///
447    /// # Errors
448    ///
449    /// Returns [`LadduDataError`] when the underlying source cannot initialize
450    /// a batch stream for the current plan.
451    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    /// Opens transformed batches using an explicit read plan.
459    ///
460    /// # Errors
461    ///
462    /// Returns [`LadduDataError`] when the underlying source cannot initialize
463    /// the requested batch stream.
464    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    /// Visits each transformed batch and stops at the first error.
520    ///
521    /// # Errors
522    ///
523    /// Returns the first [`LadduDataError`] produced by the source,
524    /// transformations, or callback.
525    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    /// Maps transformed batches into a vector.
537    ///
538    /// # Errors
539    ///
540    /// Returns [`LadduDataError`] when reading or transforming a batch fails.
541    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    /// Streams the transformed dataset into an event sink.
556    ///
557    /// # Errors
558    ///
559    /// Returns [`LadduDataError`] when reading, transforming, or writing a
560    /// batch fails, or the sink cannot begin or finish the stream.
561    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")]
702/// Numerically accurate accumulators used by parallel reductions.
703pub mod accurate {
704    use accurate::{sum::Sum2, traits::*};
705    use num::complex::Complex64;
706
707    /// Compensated accumulator for real values.
708    #[derive(Clone)]
709    pub struct AccurateF64 {
710        sum: Sum2<f64>,
711    }
712
713    impl AccurateF64 {
714        /// Creates a zero accumulator.
715        pub fn zero() -> Self {
716            Self { sum: Sum2::zero() }
717        }
718
719        /// Adds one value.
720        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        /// Merges another accumulator.
726        pub fn merge(&mut self, other: Self) {
727            self.push(other.finish());
728        }
729
730        /// Returns the accumulated sum.
731        pub fn finish(self) -> f64 {
732            self.sum.sum()
733        }
734    }
735
736    /// Pair of compensated accumulators for complex values.
737    #[derive(Clone)]
738    pub struct AccurateComplex64 {
739        re: AccurateF64,
740        im: AccurateF64,
741    }
742
743    impl AccurateComplex64 {
744        /// Creates a zero accumulator.
745        pub fn zero() -> Self {
746            Self {
747                re: AccurateF64::zero(),
748                im: AccurateF64::zero(),
749            }
750        }
751
752        /// Adds one complex value.
753        pub fn push(&mut self, value: Complex64) {
754            self.re.push(value.re);
755            self.im.push(value.im);
756        }
757
758        /// Merges another accumulator.
759        pub fn merge(&mut self, other: Self) {
760            self.re.merge(other.re);
761            self.im.merge(other.im);
762        }
763
764        /// Returns the accumulated complex sum.
765        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}