Skip to main content

laddu_data/data/dataset/
mod.rs

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/// Cached statistics for an immutable dataset view.
32#[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    /// Returns the number of transformed events.
43    pub fn events(&self) -> u64 {
44        self.events
45    }
46
47    /// Returns the accurately accumulated event-weight sum.
48    pub fn sum_weights(&self) -> f64 {
49        self.sum_weights
50    }
51
52    /// Returns the accurately accumulated sum of squared event weights.
53    pub fn sum_squared_weights(&self) -> f64 {
54        self.sum_squared_weights
55    }
56
57    /// Returns the effective number of entries, when the squared-weight sum is positive.
58    ///
59    /// Empty, all-zero-weight, and other datasets with a non-positive squared-weight sum have
60    /// no defined effective entries and return `None`.
61    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    /// Returns the sum of positive event weights.
67    pub fn positive_weights(&self) -> f64 {
68        self.positive_weights
69    }
70
71    /// Returns the signed sum of negative event weights.
72    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/// Lazy event dataset combining a source, read plan, and row transformations.
87#[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/// Memory policy for compiled event-dependent model caches.
103#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
104pub enum CacheStorage {
105    /// Materialize all event-dependent cache values once and retain them for repeated evaluations.
106    #[default]
107    Resident,
108    /// Retain only dataset statistics and rebuild each batch cache during every evaluation.
109    Streaming,
110}
111
112/// Strategy used to trade retained memory for execution speed.
113#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
114pub enum MemoryPolicy {
115    /// Select the fastest supported resident or streaming strategy that fits.
116    #[default]
117    Fastest,
118    /// Require the complete compiled event cache to remain resident.
119    Resident,
120    /// Retain no compiled event cache between traversals.
121    Streaming,
122}
123
124impl Dataset {
125    /// Creates a dataset from an event source.
126    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    /// Creates a dataset from a shared dynamically dispatched source.
146    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    /// Builds a derived dataset while preserving this dataset's read and cache policy.
163    ///
164    /// The derived source owns its row lifetime; this method only carries over
165    /// execution policy and schema-independent dataset settings.
166    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    /// Builds an empty derived dataset with this dataset's schema and policy.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`LadduDataError`] when the source schema cannot be read.
190    pub fn empty_derived(&self) -> LadduDataResult<Self> {
191        Ok(self.with_derived_source(MemorySource::empty(self.schema()?)))
192    }
193
194    /// Creates an in-memory dataset from one batch.
195    pub fn from_batch(batch: EventBatch) -> Self {
196        Self::new(MemorySource::new(batch))
197    }
198
199    /// Creates an in-memory dataset from schema-compatible batches.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`LadduDataError`] when batch schemas are incompatible.
204    pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
205        Ok(Self::new(MemorySource::from_batches(batches)?))
206    }
207
208    /// Collects owned events into an in-memory dataset.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`LadduDataError`] when an event does not match `schema`.
213    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    /// Returns the source schema.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`LadduDataError`] when the underlying source cannot determine
225    /// or load its schema.
226    pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
227        self.source.schema()
228    }
229
230    /// Materializes an exact column in this view's row order.
231    ///
232    /// # Errors
233    /// Returns an error for an unknown column, source failure, or dtype mismatch.
234    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    /// Returns source planning capabilities.
247    pub fn capabilities(&self) -> SourceCapabilities {
248        self.source.capabilities()
249    }
250
251    /// Returns the source event count when cheaply available.
252    ///
253    /// # Errors
254    ///
255    /// Returns an error when source metadata cannot be read.
256    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    /// Returns cached event-count and weight-sum statistics, computing them once if needed.
283    ///
284    /// Clones of a dataset view share this cache. Failed traversals are not cached and may be
285    /// retried.
286    ///
287    /// # Errors
288    ///
289    /// Returns [`LadduDataError`] when reading or transforming the dataset fails.
290    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    /// Returns the current read plan.
325    pub fn read_plan(&self) -> ReadPlan {
326        self.plan
327    }
328
329    /// Returns the compiled-cache memory policy.
330    pub fn cache_storage(&self) -> CacheStorage {
331        self.cache_storage
332    }
333
334    /// Returns the memory-first cache selection policy.
335    pub fn memory_policy(&self) -> MemoryPolicy {
336        self.memory_policy
337    }
338
339    /// Returns the dataset's host-memory budget.
340    pub fn memory_budget(&self) -> MemoryBudget {
341        self.memory_budget
342    }
343
344    /// Returns the most recent memory-derived read decision.
345    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    /// Returns the number of transformed source iterators opened by this dataset view.
353    pub fn source_traversals(&self) -> u64 {
354        self.source_traversals.load(Ordering::Relaxed)
355    }
356
357    /// Returns the immutable identity of this dataset view.
358    ///
359    /// Clones retain identity, while row, weight, and source transformations
360    /// create a new identity. This is intended for execution-scoped caches.
361    #[doc(hidden)]
362    pub fn identity(&self) -> u64 {
363        self.identity
364    }
365
366    /// Returns this dataset with a host-memory budget.
367    pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
368        self.memory_budget = budget;
369        self
370    }
371
372    /// Select the fastest strategy allowed by the active memory budget.
373    pub fn fastest(mut self) -> Self {
374        self.memory_policy = MemoryPolicy::Fastest;
375        self.cache_storage = CacheStorage::Resident;
376        self
377    }
378
379    /// Retain compiled event-dependent values for every local event.
380    ///
381    /// This strict policy is intended for repeatedly evaluating a likelihood
382    /// and returns an error if the resident cache cannot fit. [`Dataset::fastest`]
383    /// is the default.
384    pub fn resident(mut self) -> Self {
385        self.memory_policy = MemoryPolicy::Resident;
386        self.cache_storage = CacheStorage::Resident;
387        self
388    }
389
390    /// Re-read the source and rebuild one batch cache during each parameter evaluation.
391    ///
392    /// Only fixed dataset statistics are retained. This minimizes memory use but requires a
393    /// repeatable source and is expected to be slower than [`Dataset::resident`].
394    pub fn streaming(mut self) -> Self {
395        self.memory_policy = MemoryPolicy::Streaming;
396        self.cache_storage = CacheStorage::Streaming;
397        self
398    }
399
400    /// Returns this dataset with a low-level nonzero maximum event count.
401    ///
402    /// Prefer [`Dataset::with_memory_budget`] for portable application code.
403    /// This explicit read-plan override remains available for source debugging
404    /// and reproducibility and is always capped by execution memory planning.
405    ///
406    /// # Errors
407    ///
408    /// Returns [`LadduDataError::InvalidArgument`] when `chunk_size` is zero.
409    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    /// Returns this dataset with source-native batch sizes.
420    pub fn unchunked(mut self) -> Self {
421        self.plan.chunk_size = None;
422        self
423    }
424
425    /// Lazily retains events satisfying `f`.
426    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    /// Lazily retains a deterministic fraction of events.
434    ///
435    /// # Errors
436    ///
437    /// Returns [`LadduDataError::InvalidArgument`] when `fraction` is outside
438    /// `[0, 1]` or is NaN.
439    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    /// Selects exact integer columns whose equal tuples share a bootstrap multiplier.
450    ///
451    /// The namespace separates independent samples while keeping group draws stable
452    /// across filters, row order, batch boundaries, models and likelihood term order.
453    /// This configures future bootstraps; it does not reweight the central dataset.
454    /// Existing bootstrap operations retain their original grouping configuration.
455    ///
456    /// # Errors
457    /// Returns an error for an empty/repeated group list or missing integer column.
458    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    /// Whether bootstraps use explicit group namespaces rather than likelihood term offsets.
490    pub fn has_bootstrap_groups(&self) -> bool {
491        self.bootstrap_grouping.is_some()
492    }
493
494    /// Applies deterministic Poisson bootstrap multiplicities to event weights.
495    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    /// Visits each transformed event.
501    ///
502    /// # Errors
503    ///
504    /// Returns [`LadduDataError`] when reading or transforming the source
505    /// fails.
506    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    /// Visits each transformed event and stops at the first error.
517    ///
518    /// # Errors
519    ///
520    /// Returns the first [`LadduDataError`] produced by the source,
521    /// transformations, or callback.
522    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    /// Fallibly maps transformed events into a vector.
534    ///
535    /// # Errors
536    ///
537    /// Returns the first [`LadduDataError`] produced while reading,
538    /// transforming, or mapping an event.
539    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    /// Maps transformed events into a vector.
553    ///
554    /// # Errors
555    ///
556    /// Returns [`LadduDataError`] when reading or transforming the source
557    /// fails.
558    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    /// Fallibly folds transformed events into an owned accumulator.
572    ///
573    /// # Errors
574    ///
575    /// Returns the first [`LadduDataError`] produced while reading,
576    /// transforming, or folding an event.
577    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    /// Folds transformed events into an owned accumulator.
595    ///
596    /// # Errors
597    ///
598    /// Returns [`LadduDataError`] when reading or transforming the source
599    /// fails.
600    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    /// Fallibly folds transformed event batches into an owned accumulator.
608    ///
609    /// The callback receives each batch in source order and may stop the
610    /// traversal by returning a data error.
611    ///
612    /// # Errors
613    ///
614    /// Returns the first source or callback error.
615    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    /// Fallibly updates a mutable accumulator for every transformed event.
627    ///
628    /// # Errors
629    ///
630    /// Returns the first [`LadduDataError`] produced while reading,
631    /// transforming, or accumulating an event.
632    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    /// Updates a mutable accumulator for every transformed event.
641    ///
642    /// # Errors
643    ///
644    /// Returns [`LadduDataError`] when reading or transforming the source
645    /// fails.
646    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    /// Sums effective event weights.
657    ///
658    /// # Errors
659    ///
660    /// Returns [`LadduDataError`] when reading or transforming the source
661    /// fails.
662    pub fn sum_weights(&self) -> LadduDataResult<f64> {
663        Ok(self.stats()?.sum_weights())
664    }
665
666    /// Sums `weight * f(event)` over transformed events.
667    ///
668    /// # Errors
669    ///
670    /// Returns [`LadduDataError`] when reading or transforming the source
671    /// fails.
672    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    /// Sums complex `weight * f(event)` contributions.
680    ///
681    /// # Errors
682    ///
683    /// Returns [`LadduDataError`] when reading or transforming the source
684    /// fails.
685    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    /// Opens an iterator of fully transformed event batches.
693    ///
694    /// # Errors
695    ///
696    /// Returns [`LadduDataError`] when the underlying source cannot initialize
697    /// a batch stream for the current plan.
698    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    /// Opens the shared transformed batch stream using an explicit read plan.
706    ///
707    /// # Errors
708    ///
709    /// Returns [`LadduDataError`] when the underlying source cannot initialize
710    /// the requested batch stream.
711    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    /// Compatibility alias for the shared transformed batch stream.
720    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    /// Visits each transformed batch and stops at the first error.
732    ///
733    /// # Errors
734    ///
735    /// Returns the first [`LadduDataError`] produced by the source,
736    /// transformations, or callback.
737    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    /// Maps transformed batches into a vector.
749    ///
750    /// # Errors
751    ///
752    /// Returns [`LadduDataError`] when reading or transforming a batch fails.
753    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    /// Streams the transformed dataset into an event sink.
768    ///
769    /// # Errors
770    ///
771    /// Returns [`LadduDataError`] when reading, transforming, or writing a
772    /// batch fails, or the sink cannot begin or finish the stream.
773    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            // Preserve the operation error; abort is best-effort cleanup and
786            // may itself report a backend failure.
787            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}