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::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}
37
38impl DatasetStats {
39    /// Returns the number of transformed events.
40    pub fn events(&self) -> u64 {
41        self.events
42    }
43
44    /// Returns the accurately accumulated event-weight sum.
45    pub fn sum_weights(&self) -> f64 {
46        self.sum_weights
47    }
48}
49
50#[derive(Default)]
51struct DatasetStatsCache {
52    events: Option<u64>,
53    sum_weights: Option<f64>,
54}
55
56/// Lazy event dataset combining a source, read plan, and row transformations.
57#[derive(Clone)]
58pub struct Dataset {
59    identity: u64,
60    source: Arc<dyn EventSource>,
61    plan: ReadPlan,
62    ops: Arc<[DatasetOp]>,
63    cache_storage: CacheStorage,
64    memory_policy: MemoryPolicy,
65    memory_budget: MemoryBudget,
66    last_memory_decision: Arc<Mutex<Option<MemoryDecision>>>,
67    stats: Arc<Mutex<DatasetStatsCache>>,
68    source_traversals: Arc<AtomicU64>,
69}
70
71/// Memory policy for compiled event-dependent model caches.
72#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
73pub enum CacheStorage {
74    /// Materialize all event-dependent cache values once and retain them for repeated evaluations.
75    #[default]
76    Resident,
77    /// Retain only dataset statistics and rebuild each batch cache during every evaluation.
78    Streaming,
79}
80
81/// Strategy used to trade retained memory for execution speed.
82#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
83pub enum MemoryPolicy {
84    /// Select the fastest supported resident or streaming strategy that fits.
85    #[default]
86    Fastest,
87    /// Require the complete compiled event cache to remain resident.
88    Resident,
89    /// Retain no compiled event cache between traversals.
90    Streaming,
91}
92
93impl Dataset {
94    /// Creates a dataset from an event source.
95    pub fn new<S>(source: S) -> Self
96    where
97        S: EventSource + 'static,
98    {
99        Self {
100            identity: next_dataset_identity(),
101            source: Arc::new(source),
102            plan: ReadPlan::default(),
103            ops: Arc::from([]),
104            cache_storage: CacheStorage::Resident,
105            memory_policy: MemoryPolicy::Fastest,
106            memory_budget: MemoryBudget::Auto,
107            last_memory_decision: Default::default(),
108            stats: Default::default(),
109            source_traversals: Default::default(),
110        }
111    }
112
113    /// Creates a dataset from a shared dynamically dispatched source.
114    pub fn from_arc(source: Arc<dyn EventSource>) -> Self {
115        Self {
116            identity: next_dataset_identity(),
117            source,
118            plan: ReadPlan::default(),
119            ops: Arc::from([]),
120            cache_storage: CacheStorage::Resident,
121            memory_policy: MemoryPolicy::Fastest,
122            memory_budget: MemoryBudget::Auto,
123            last_memory_decision: Default::default(),
124            stats: Default::default(),
125            source_traversals: Default::default(),
126        }
127    }
128
129    /// Builds a derived dataset while preserving this dataset's read and cache policy.
130    ///
131    /// The derived source owns its row lifetime; this method only carries over
132    /// execution policy and schema-independent dataset settings.
133    pub fn with_derived_source<S>(&self, source: S) -> Self
134    where
135        S: EventSource + 'static,
136    {
137        Self {
138            identity: next_dataset_identity(),
139            source: Arc::new(source),
140            plan: self.plan,
141            ops: Arc::from([]),
142            cache_storage: self.cache_storage,
143            memory_policy: self.memory_policy,
144            memory_budget: self.memory_budget,
145            last_memory_decision: Default::default(),
146            stats: Default::default(),
147            source_traversals: Default::default(),
148        }
149    }
150
151    /// Builds an empty derived dataset with this dataset's schema and policy.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`LadduDataError`] when the source schema cannot be read.
156    pub fn empty_derived(&self) -> LadduDataResult<Self> {
157        Ok(self.with_derived_source(MemorySource::empty(self.schema()?)))
158    }
159
160    /// Creates an in-memory dataset from one batch.
161    pub fn from_batch(batch: EventBatch) -> Self {
162        Self::new(MemorySource::new(batch))
163    }
164
165    /// Creates an in-memory dataset from schema-compatible batches.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`LadduDataError`] when batch schemas are incompatible.
170    pub fn from_batches(batches: Vec<EventBatch>) -> LadduDataResult<Self> {
171        Ok(Self::new(MemorySource::from_batches(batches)?))
172    }
173
174    /// Collects owned events into an in-memory dataset.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`LadduDataError`] when an event does not match `schema`.
179    pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
180    where
181        I: IntoIterator<Item = OwnedEvent>,
182    {
183        Ok(Self::new(MemorySource::from_events(schema, events)?))
184    }
185
186    /// Returns the source schema.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`LadduDataError`] when the underlying source cannot determine
191    /// or load its schema.
192    pub fn schema(&self) -> LadduDataResult<Arc<Schema>> {
193        self.source.schema()
194    }
195
196    /// Returns source planning capabilities.
197    pub fn capabilities(&self) -> SourceCapabilities {
198        self.source.capabilities()
199    }
200
201    /// Returns the source event count when cheaply available.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error when source metadata cannot be read.
206    pub fn num_events(&self) -> LadduDataResult<Option<u64>> {
207        {
208            let stats = self.stats.lock().unwrap_or_else(|error| error.into_inner());
209            if let Some(events) = stats.events {
210                return Ok(Some(events));
211            }
212        }
213
214        if self
215            .ops
216            .iter()
217            .any(|op| !matches!(op, DatasetOp::Bootstrap { .. }))
218        {
219            return Ok(None);
220        }
221
222        let events = self.source.num_events()?;
223        if let Some(events) = events {
224            self.stats
225                .lock()
226                .unwrap_or_else(|error| error.into_inner())
227                .events = Some(events);
228        }
229        Ok(events)
230    }
231
232    /// Returns cached event-count and weight-sum statistics, computing them once if needed.
233    ///
234    /// Clones of a dataset view share this cache. Failed traversals are not cached and may be
235    /// retried.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`LadduDataError`] when reading or transforming the dataset fails.
240    pub fn stats(&self) -> LadduDataResult<DatasetStats> {
241        {
242            let cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
243            if let (Some(events), Some(sum_weights)) = (cache.events, cache.sum_weights) {
244                return Ok(DatasetStats {
245                    events,
246                    sum_weights,
247                });
248            }
249        }
250
251        if self.ops.is_empty()
252            && let (Some(events), Some(sum_weights)) =
253                (self.source.num_events()?, self.source.weighted_total()?)
254        {
255            let stats = DatasetStats {
256                events,
257                sum_weights,
258            };
259            let mut cache = self.stats.lock().unwrap_or_else(|error| error.into_inner());
260            cache.events = Some(events);
261            cache.sum_weights = Some(sum_weights);
262            return Ok(stats);
263        }
264
265        let mut executor = self.executor_with_plan(self.plan)?;
266        for batch in &mut executor {
267            batch?;
268        }
269
270        Ok(executor.stats())
271    }
272
273    /// Returns the current read plan.
274    pub fn read_plan(&self) -> ReadPlan {
275        self.plan
276    }
277
278    /// Returns the compiled-cache memory policy.
279    pub fn cache_storage(&self) -> CacheStorage {
280        self.cache_storage
281    }
282
283    /// Returns the memory-first cache selection policy.
284    pub fn memory_policy(&self) -> MemoryPolicy {
285        self.memory_policy
286    }
287
288    /// Returns the dataset's host-memory budget.
289    pub fn memory_budget(&self) -> MemoryBudget {
290        self.memory_budget
291    }
292
293    /// Returns the most recent memory-derived read decision.
294    pub fn last_memory_decision(&self) -> Option<MemoryDecision> {
295        self.last_memory_decision
296            .lock()
297            .unwrap_or_else(|error| error.into_inner())
298            .clone()
299    }
300
301    /// Returns the number of transformed source iterators opened by this dataset view.
302    pub fn source_traversals(&self) -> u64 {
303        self.source_traversals.load(Ordering::Relaxed)
304    }
305
306    /// Returns the immutable identity of this dataset view.
307    ///
308    /// Clones retain identity, while row, weight, and source transformations
309    /// create a new identity. This is intended for execution-scoped caches.
310    #[doc(hidden)]
311    pub fn identity(&self) -> u64 {
312        self.identity
313    }
314
315    /// Returns this dataset with a host-memory budget.
316    pub fn with_memory_budget(mut self, budget: MemoryBudget) -> Self {
317        self.memory_budget = budget;
318        self
319    }
320
321    /// Select the fastest strategy allowed by the active memory budget.
322    pub fn fastest(mut self) -> Self {
323        self.memory_policy = MemoryPolicy::Fastest;
324        self.cache_storage = CacheStorage::Resident;
325        self
326    }
327
328    /// Retain compiled event-dependent values for every local event.
329    ///
330    /// This strict policy is intended for repeatedly evaluating a likelihood
331    /// and returns an error if the resident cache cannot fit. [`Dataset::fastest`]
332    /// is the default.
333    pub fn resident(mut self) -> Self {
334        self.memory_policy = MemoryPolicy::Resident;
335        self.cache_storage = CacheStorage::Resident;
336        self
337    }
338
339    /// Re-read the source and rebuild one batch cache during each parameter evaluation.
340    ///
341    /// Only fixed dataset statistics are retained. This minimizes memory use but requires a
342    /// repeatable source and is expected to be slower than [`Dataset::resident`].
343    pub fn streaming(mut self) -> Self {
344        self.memory_policy = MemoryPolicy::Streaming;
345        self.cache_storage = CacheStorage::Streaming;
346        self
347    }
348
349    /// Returns this dataset with a low-level nonzero maximum event count.
350    ///
351    /// Prefer [`Dataset::with_memory_budget`] for portable application code.
352    /// This explicit read-plan override remains available for source debugging
353    /// and reproducibility and is always capped by execution memory planning.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`LadduDataError::InvalidArgument`] when `chunk_size` is zero.
358    pub fn chunked(mut self, chunk_size: usize) -> LadduDataResult<Self> {
359        if chunk_size == 0 {
360            return Err(LadduDataError::InvalidArgument(
361                "chunk_size must be nonzero",
362            ));
363        }
364        self.plan.chunk_size = Some(chunk_size);
365        Ok(self)
366    }
367
368    /// Returns this dataset with source-native batch sizes.
369    pub fn unchunked(mut self) -> Self {
370        self.plan.chunk_size = None;
371        self
372    }
373
374    /// Lazily retains events satisfying `f`.
375    pub fn filter<F>(self, f: F) -> Self
376    where
377        F: Fn(Event<'_>) -> bool + Send + Sync + 'static,
378    {
379        self.push_op(DatasetOp::Filter(Arc::new(f)))
380    }
381
382    /// Lazily retains a deterministic fraction of events.
383    ///
384    /// # Errors
385    ///
386    /// Returns [`LadduDataError::InvalidArgument`] when `fraction` is outside
387    /// `[0, 1]` or is NaN.
388    pub fn subsample(self, fraction: f64, seed: u64) -> LadduDataResult<Self> {
389        if !(0.0..=1.0).contains(&fraction) {
390            return Err(LadduDataError::InvalidArgument(
391                "fraction must be in [0, 1]",
392            ));
393        }
394
395        Ok(self.push_op(DatasetOp::Subsample { fraction, seed }))
396    }
397
398    /// Applies deterministic Poisson bootstrap multiplicities to event weights.
399    pub fn bootstrap(self, seed: u64) -> Self {
400        self.push_op(DatasetOp::Bootstrap { seed })
401    }
402
403    /// Visits each transformed event.
404    ///
405    /// # Errors
406    ///
407    /// Returns [`LadduDataError`] when reading or transforming the source
408    /// fails.
409    pub fn for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
410    where
411        F: FnMut(Event<'_>),
412    {
413        self.try_for_each_event(|ev| {
414            f(ev);
415            Ok(())
416        })
417    }
418
419    /// Visits each transformed event and stops at the first error.
420    ///
421    /// # Errors
422    ///
423    /// Returns the first [`LadduDataError`] produced by the source,
424    /// transformations, or callback.
425    pub fn try_for_each_event<F>(&self, mut f: F) -> LadduDataResult<()>
426    where
427        F: FnMut(Event<'_>) -> LadduDataResult<()>,
428    {
429        visit_events(
430            self,
431            DatasetExecutionPlan::resolve(self, self.plan)?,
432            &mut f,
433        )
434    }
435
436    /// Fallibly maps transformed events into a vector.
437    ///
438    /// # Errors
439    ///
440    /// Returns the first [`LadduDataError`] produced while reading,
441    /// transforming, or mapping an event.
442    pub fn try_map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
443    where
444        F: FnMut(Event<'_>) -> LadduDataResult<T>,
445    {
446        let mut out = Vec::new();
447        self.try_for_each_event(|ev| {
448            out.push(f(ev)?);
449            Ok(())
450        })?;
451
452        Ok(out)
453    }
454
455    /// Maps transformed events into a vector.
456    ///
457    /// # Errors
458    ///
459    /// Returns [`LadduDataError`] when reading or transforming the source
460    /// fails.
461    pub fn map_events<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
462    where
463        F: FnMut(Event<'_>) -> T,
464    {
465        let mut out = Vec::new();
466        self.try_for_each_event(|ev| {
467            out.push(f(ev));
468            Ok(())
469        })?;
470
471        Ok(out)
472    }
473
474    /// Fallibly folds transformed events into an owned accumulator.
475    ///
476    /// # Errors
477    ///
478    /// Returns the first [`LadduDataError`] produced while reading,
479    /// transforming, or folding an event.
480    pub fn try_fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
481    where
482        F: FnMut(T, Event<'_>) -> LadduDataResult<T>,
483    {
484        let mut acc = Some(init);
485
486        self.try_for_each_event(|ev| {
487            let current = acc.take().ok_or_else(|| {
488                LadduDataError::Source("dataset fold accumulator was consumed".into())
489            })?;
490            acc = Some(f(current, ev)?);
491            Ok(())
492        })?;
493
494        acc.ok_or_else(|| LadduDataError::Source("dataset fold produced no accumulator".into()))
495    }
496
497    /// Folds transformed events into an owned accumulator.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`LadduDataError`] when reading or transforming the source
502    /// fails.
503    pub fn fold_events<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
504    where
505        F: FnMut(T, Event<'_>) -> T,
506    {
507        self.try_fold_events(init, |acc, ev| Ok(f(acc, ev)))
508    }
509
510    /// Fallibly folds transformed event batches into an owned accumulator.
511    ///
512    /// The callback receives each batch in source order and may stop the
513    /// traversal by returning a data error.
514    ///
515    /// # Errors
516    ///
517    /// Returns the first source or callback error.
518    pub fn try_fold_batches<T, F>(&self, init: T, mut f: F) -> LadduDataResult<T>
519    where
520        F: FnMut(T, EventBatch) -> LadduDataResult<T>,
521    {
522        let mut acc = init;
523        for batch in self.batches()? {
524            acc = f(acc, batch?)?;
525        }
526        Ok(acc)
527    }
528
529    /// Fallibly updates a mutable accumulator for every transformed event.
530    ///
531    /// # Errors
532    ///
533    /// Returns the first [`LadduDataError`] produced while reading,
534    /// transforming, or accumulating an event.
535    pub fn try_accumulate_events<T, F>(&self, mut acc: T, mut f: F) -> LadduDataResult<T>
536    where
537        F: FnMut(&mut T, Event<'_>) -> LadduDataResult<()>,
538    {
539        self.try_for_each_event(|ev| f(&mut acc, ev))?;
540        Ok(acc)
541    }
542
543    /// Updates a mutable accumulator for every transformed event.
544    ///
545    /// # Errors
546    ///
547    /// Returns [`LadduDataError`] when reading or transforming the source
548    /// fails.
549    pub fn accumulate_events<T, F>(&self, acc: T, mut f: F) -> LadduDataResult<T>
550    where
551        F: FnMut(&mut T, Event<'_>),
552    {
553        self.try_accumulate_events(acc, |acc, ev| {
554            f(acc, ev);
555            Ok(())
556        })
557    }
558
559    /// Sums effective event weights.
560    ///
561    /// # Errors
562    ///
563    /// Returns [`LadduDataError`] when reading or transforming the source
564    /// fails.
565    pub fn sum_weights(&self) -> LadduDataResult<f64> {
566        Ok(self.stats()?.sum_weights())
567    }
568
569    /// Sums `weight * f(event)` over transformed events.
570    ///
571    /// # Errors
572    ///
573    /// Returns [`LadduDataError`] when reading or transforming the source
574    /// fails.
575    pub fn weighted_sum<F>(&self, mut f: F) -> LadduDataResult<f64>
576    where
577        F: FnMut(Event<'_>) -> f64,
578    {
579        self.fold_events(0.0, |sum, ev| sum + ev.weight() * f(ev))
580    }
581
582    /// Sums complex `weight * f(event)` contributions.
583    ///
584    /// # Errors
585    ///
586    /// Returns [`LadduDataError`] when reading or transforming the source
587    /// fails.
588    pub fn weighted_complex_sum<F>(&self, mut f: F) -> LadduDataResult<Complex64>
589    where
590        F: FnMut(Event<'_>) -> Complex64,
591    {
592        self.fold_events(0.0.into(), |sum, ev| sum + ev.weight() * f(ev))
593    }
594
595    /// Opens an iterator of fully transformed event batches.
596    ///
597    /// # Errors
598    ///
599    /// Returns [`LadduDataError`] when the underlying source cannot initialize
600    /// a batch stream for the current plan.
601    pub fn batches(
602        &self,
603    ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
604        self.stream_with_plan(self.plan)
605    }
606
607    #[doc(hidden)]
608    /// Opens the shared transformed batch stream using an explicit read plan.
609    ///
610    /// # Errors
611    ///
612    /// Returns [`LadduDataError`] when the underlying source cannot initialize
613    /// the requested batch stream.
614    pub fn stream_with_plan(
615        &self,
616        plan: ReadPlan,
617    ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
618        Ok(Box::new(self.executor_with_plan(plan)?))
619    }
620
621    #[doc(hidden)]
622    /// Compatibility alias for the shared transformed batch stream.
623    pub fn batches_with_plan(
624        &self,
625        plan: ReadPlan,
626    ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
627        self.stream_with_plan(plan)
628    }
629
630    fn executor_with_plan(&self, plan: ReadPlan) -> LadduDataResult<DatasetExecutor> {
631        DatasetExecutor::new(self, DatasetExecutionPlan::resolve(self, plan)?)
632    }
633
634    /// Visits each transformed batch and stops at the first error.
635    ///
636    /// # Errors
637    ///
638    /// Returns the first [`LadduDataError`] produced by the source,
639    /// transformations, or callback.
640    pub fn try_for_each_batch<F>(&self, mut f: F) -> LadduDataResult<()>
641    where
642        F: FnMut(EventBatch) -> LadduDataResult<()>,
643    {
644        for batch in self.batches()? {
645            f(batch?)?;
646        }
647
648        Ok(())
649    }
650
651    /// Maps transformed batches into a vector.
652    ///
653    /// # Errors
654    ///
655    /// Returns [`LadduDataError`] when reading or transforming a batch fails.
656    pub fn map_batches<T, F>(&self, mut f: F) -> LadduDataResult<Vec<T>>
657    where
658        F: FnMut(EventBatch) -> T,
659    {
660        let mut out = Vec::new();
661
662        self.try_for_each_batch(|batch| {
663            out.push(f(batch));
664            Ok(())
665        })?;
666
667        Ok(out)
668    }
669
670    /// Streams the transformed dataset into an event sink.
671    ///
672    /// # Errors
673    ///
674    /// Returns [`LadduDataError`] when reading, transforming, or writing a
675    /// batch fails, or the sink cannot begin or finish the stream.
676    pub fn write_to<S: EventSink>(&self, sink: &mut S) -> LadduDataResult<()> {
677        sink.begin(self.schema()?, WritePlan::from(self.plan))?;
678
679        let result = (|| {
680            for batch in self.batches()? {
681                sink.write_batch(&batch?)?;
682            }
683
684            sink.finish()
685        })();
686
687        if result.is_err() {
688            // Preserve the operation error; abort is best-effort cleanup and
689            // may itself report a backend failure.
690            let _ = sink.abort();
691        }
692
693        result
694    }
695
696    fn push_op(self, op: DatasetOp) -> Self {
697        let preserved_events = if matches!(&op, DatasetOp::Bootstrap { .. }) {
698            self.num_events().ok().flatten()
699        } else {
700            None
701        };
702        let mut ops = self.ops.to_vec();
703        ops.push(op);
704
705        Self {
706            identity: next_dataset_identity(),
707            source: self.source,
708            plan: self.plan,
709            ops: ops.into(),
710            cache_storage: self.cache_storage,
711            memory_policy: self.memory_policy,
712            memory_budget: self.memory_budget,
713            last_memory_decision: Default::default(),
714            stats: Arc::new(Mutex::new(DatasetStatsCache {
715                events: preserved_events,
716                sum_weights: None,
717            })),
718            source_traversals: Default::default(),
719        }
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::ops::materialize_batch;
726    use super::*;
727    use crate::io::{EventBatchIter, EventSource, ReadPlan, memory::MemorySink};
728    use laddu_physics::vectors::RealVec4;
729    use std::sync::atomic::{AtomicUsize, Ordering};
730
731    #[derive(Clone)]
732    struct CountingSource {
733        batch: EventBatch,
734        reads: Arc<AtomicUsize>,
735    }
736
737    impl EventSource for CountingSource {
738        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
739            Ok(Arc::clone(self.batch.schema()))
740        }
741
742        fn batches(
743            &self,
744            _plan: ReadPlan,
745        ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
746            self.reads.fetch_add(1, Ordering::Relaxed);
747            Ok(Box::new(std::iter::once(Ok(self.batch.clone()))))
748        }
749    }
750
751    #[derive(Clone)]
752    struct ErrorSource {
753        schema: Arc<Schema>,
754        items: Arc<[LadduDataResult<EventBatch>]>,
755    }
756
757    impl EventSource for ErrorSource {
758        fn schema(&self) -> LadduDataResult<Arc<Schema>> {
759            Ok(Arc::clone(&self.schema))
760        }
761
762        fn batches(&self, _plan: ReadPlan) -> LadduDataResult<EventBatchIter> {
763            let items = Arc::clone(&self.items);
764            Ok(Box::new(
765                (0..items.len()).map(move |index| items[index].clone()),
766            ))
767        }
768    }
769
770    fn v(x: f64) -> RealVec4 {
771        RealVec4 {
772            e: x + 0.3,
773            px: x,
774            py: x + 0.1,
775            pz: x + 0.2,
776        }
777    }
778
779    fn schema_with_weight() -> Arc<Schema> {
780        Arc::new(Schema::new(["p"], ["x"], true).unwrap())
781    }
782
783    fn schema_without_weight() -> Arc<Schema> {
784        Arc::new(Schema::new(["p"], ["x"], false).unwrap())
785    }
786
787    fn weighted_batch(start: usize, len: usize) -> EventBatch {
788        let schema = schema_with_weight();
789
790        let events = (start..start + len)
791            .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
792
793        EventBatch::from_events(schema, events).unwrap()
794    }
795
796    fn unweighted_batch(start: usize, len: usize) -> EventBatch {
797        let schema = schema_without_weight();
798
799        let events =
800            (start..start + len).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64]));
801
802        EventBatch::from_events(schema, events).unwrap()
803    }
804
805    fn error_source(error_after: Option<EventBatch>) -> ErrorSource {
806        let schema = schema_with_weight();
807        let mut items = Vec::new();
808        if let Some(batch) = error_after {
809            items.push(Ok(batch));
810        }
811        items.push(Err(LadduDataError::Unsupported("source")));
812        ErrorSource {
813            schema,
814            items: items.into(),
815        }
816    }
817
818    fn scalar_values(batch: &EventBatch) -> Vec<f64> {
819        batch.scalar_column(0).to_vec()
820    }
821
822    #[test]
823    fn dataset_statistics_are_shared_per_view_and_invalidated_by_selection() {
824        let reads = Arc::new(AtomicUsize::new(0));
825        let dataset = Dataset::new(CountingSource {
826            batch: weighted_batch(0, 5),
827            reads: Arc::clone(&reads),
828        });
829        let clone = dataset.clone();
830
831        assert_eq!(dataset.num_events().unwrap(), None);
832        assert_eq!(dataset.stats().unwrap().events(), 5);
833        assert_eq!(clone.sum_weights().unwrap(), 60.0);
834        assert_eq!(clone.num_events().unwrap(), Some(5));
835        assert_eq!(reads.load(Ordering::Relaxed), 1);
836
837        let bootstrapped = dataset.clone().bootstrap(7);
838        assert_eq!(bootstrapped.num_events().unwrap(), Some(5));
839        assert_eq!(reads.load(Ordering::Relaxed), 1);
840
841        let filtered = dataset.filter(|event| event.scalar(0) >= 2.0);
842        assert_eq!(filtered.num_events().unwrap(), None);
843        assert_eq!(
844            filtered.map_events(|event| event.scalar(0)).unwrap(),
845            [2.0, 3.0, 4.0]
846        );
847        assert_eq!(reads.load(Ordering::Relaxed), 2);
848        assert_eq!(filtered.stats().unwrap().events(), 3);
849        assert_eq!(reads.load(Ordering::Relaxed), 2);
850    }
851
852    #[test]
853    fn transformed_fragments_are_coalesced_to_the_read_chunk_size() {
854        let fragments = (0..10)
855            .map(|index| weighted_batch(index, 1))
856            .collect::<Vec<_>>();
857        let dataset = Dataset::from_batches(fragments)
858            .unwrap()
859            .filter(|_| true)
860            .chunked(4)
861            .unwrap();
862
863        let batches = dataset
864            .batches()
865            .unwrap()
866            .collect::<LadduDataResult<Vec<_>>>()
867            .unwrap();
868        assert_eq!(
869            batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
870            [4, 4, 2]
871        );
872        assert_eq!(
873            batches
874                .iter()
875                .flat_map(|batch| batch.scalar_column(0).iter().copied())
876                .collect::<Vec<_>>(),
877            (0..10).map(|value| value as f64).collect::<Vec<_>>()
878        );
879    }
880
881    #[test]
882    fn shared_stream_preserves_pending_batches_before_source_errors() {
883        let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
884            .chunked(4)
885            .unwrap();
886        let mut batches = dataset.batches().unwrap();
887
888        assert_eq!(batches.next().unwrap().unwrap().len(), 2);
889        assert!(matches!(
890            batches.next().unwrap(),
891            Err(LadduDataError::Unsupported("source"))
892        ));
893        assert!(batches.next().is_none());
894
895        assert!(matches!(
896            dataset.stats(),
897            Err(LadduDataError::Unsupported("source"))
898        ));
899        assert_eq!(dataset.source_traversals(), 2);
900    }
901
902    #[test]
903    fn event_visitors_share_the_execution_plan_without_changing_source_rows() {
904        let dataset = Dataset::new(error_source(Some(weighted_batch(0, 2))))
905            .chunked(4)
906            .unwrap()
907            .filter(|event| event.scalar(0) >= 0.0);
908        let mut rows = Vec::new();
909
910        let error = dataset
911            .try_for_each_event(|event| {
912                rows.push(event.row());
913                Ok(())
914            })
915            .unwrap_err();
916
917        assert!(matches!(error, LadduDataError::Unsupported("source")));
918        assert_eq!(rows, [0, 1]);
919    }
920
921    #[test]
922    fn dataset_map_fold_accumulate_complex_sum_and_error_paths_use_transformed_events() {
923        let dataset =
924            Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) % 2.0 == 0.0);
925
926        let rows = dataset
927            .map_events(|ev| (ev.row(), ev.scalar(0), ev.weight()))
928            .unwrap();
929
930        assert_eq!(rows, vec![(0, 0.0, 10.0), (2, 2.0, 12.0), (4, 4.0, 14.0)]);
931
932        let folded = dataset
933            .fold_events(String::new(), |mut out, ev| {
934                out.push_str(&format!("{};", ev.scalar(0)));
935                out
936            })
937            .unwrap();
938
939        assert_eq!(folded, "0;2;4;");
940
941        let accumulated = dataset
942            .accumulate_events(Vec::<f64>::new(), |values, ev| values.push(ev.weight()))
943            .unwrap();
944
945        assert_eq!(accumulated, vec![10.0, 12.0, 14.0]);
946
947        let weighted_sum = dataset.weighted_sum(|ev| ev.scalar(0)).unwrap();
948        assert_eq!(weighted_sum, 0.0 * 10.0 + 2.0 * 12.0 + 4.0 * 14.0);
949
950        let complex_sum = dataset
951            .weighted_complex_sum(|ev| Complex64::new(ev.scalar(0), 1.0))
952            .unwrap();
953
954        assert_eq!(complex_sum.re, weighted_sum);
955        assert_eq!(complex_sum.im, 10.0 + 12.0 + 14.0);
956
957        let err = dataset
958            .try_map_events(|ev| {
959                if ev.scalar(0) == 2.0 {
960                    Err(LadduDataError::Unsupported("stop"))
961                } else {
962                    Ok(ev.scalar(0))
963                }
964            })
965            .unwrap_err();
966
967        assert!(matches!(err, LadduDataError::Unsupported("stop")));
968    }
969
970    #[test]
971    fn batch_folds_and_empty_derived_sources_preserve_schema_and_errors() {
972        let dataset = Dataset::from_batches(vec![weighted_batch(0, 2), weighted_batch(2, 2)])
973            .unwrap()
974            .chunked(2)
975            .unwrap();
976        let event_count = dataset
977            .try_fold_batches(0usize, |count, batch| Ok(count + batch.len()))
978            .unwrap();
979        assert_eq!(event_count, 4);
980        let rows = dataset
981            .try_fold_batches(Vec::new(), |mut rows, batch| {
982                rows.extend((0..batch.len()).map(|row| batch.scalar_at(0, row)));
983                Ok(rows)
984            })
985            .unwrap();
986        assert_eq!(rows, [0.0, 1.0, 2.0, 3.0]);
987
988        let error = dataset
989            .try_fold_batches(0usize, |_count, _batch| {
990                Err(LadduDataError::Unsupported("stop"))
991            })
992            .unwrap_err();
993        assert!(matches!(error, LadduDataError::Unsupported("stop")));
994
995        let empty = dataset.empty_derived().unwrap();
996        assert_eq!(
997            empty.schema().unwrap().as_ref(),
998            dataset.schema().unwrap().as_ref()
999        );
1000        assert_eq!(empty.num_events().unwrap(), Some(0));
1001        assert!(empty.batches().unwrap().next().is_none());
1002    }
1003
1004    #[test]
1005    fn deterministic_subsample_and_bootstrap_use_global_event_ids_across_batches() {
1006        let seed = 0x0BAD_5EED;
1007        let bootstrap_seed = 0xB007_57A9;
1008
1009        let dataset = Dataset::from_batches(vec![weighted_batch(0, 3), weighted_batch(3, 3)])
1010            .unwrap()
1011            .subsample(0.5, seed)
1012            .unwrap()
1013            .bootstrap(bootstrap_seed);
1014
1015        let observed = dataset
1016            .map_events(|ev| (ev.scalar(0) as u64, ev.weight()))
1017            .unwrap();
1018
1019        let expected: Vec<(u64, f64)> = (0_u64..6)
1020            .filter(|&event_id| uniform_hash_01(seed, event_id) < 0.5)
1021            .map(|event_id| {
1022                let original_weight = 10.0 + event_id as f64;
1023                let bootstrap_weight =
1024                    poisson1_from_hash(bootstrap_seed, event_id) as f64 * original_weight;
1025                (event_id, bootstrap_weight)
1026            })
1027            .collect();
1028
1029        assert_eq!(observed, expected);
1030    }
1031
1032    #[test]
1033    fn materialized_batches_store_weights_only_when_needed() {
1034        let unweighted = unweighted_batch(0, 4);
1035
1036        let filtered = Dataset::from_batch(unweighted.clone())
1037            .filter(|ev| ev.scalar(0) >= 1.0)
1038            .subsample(1.0, 123)
1039            .unwrap();
1040
1041        let filtered_batch = filtered.batches().unwrap().next().unwrap().unwrap();
1042
1043        assert_eq!(scalar_values(&filtered_batch), vec![1.0, 2.0, 3.0]);
1044        assert!(filtered_batch.weights_column().is_none());
1045
1046        let bootstrapped = Dataset::from_batch(unweighted).bootstrap(999);
1047        let bootstrapped_batch = bootstrapped.batches().unwrap().next().unwrap().unwrap();
1048
1049        assert!(bootstrapped_batch.weights_column().is_some());
1050
1051        let source = weighted_batch(0, 2);
1052        let empty_weighted =
1053            materialize_batch(&source, &[DatasetOp::Filter(Arc::new(|_| false))], 0).unwrap();
1054        assert!(empty_weighted.is_empty());
1055        assert_eq!(empty_weighted.weights_column(), Some([].as_slice()));
1056    }
1057
1058    #[test]
1059    fn write_to_memory_sink_captures_transformed_dataset() {
1060        let dataset = Dataset::from_batch(weighted_batch(0, 5)).filter(|ev| ev.scalar(0) >= 2.0);
1061
1062        let mut sink = MemorySink::new();
1063        dataset.write_to(&mut sink).unwrap();
1064
1065        let captured = sink.into_batch().unwrap();
1066
1067        assert_eq!(scalar_values(&captured), vec![2.0, 3.0, 4.0]);
1068        assert_eq!(captured.weights_column().unwrap(), &[12.0, 13.0, 14.0]);
1069    }
1070
1071    #[test]
1072    fn immutable_dataset_identity_tracks_semantic_views() {
1073        let dataset = Dataset::from_batch(weighted_batch(0, 3));
1074        assert_eq!(dataset.identity(), dataset.clone().identity());
1075        assert_eq!(dataset.identity(), dataset.clone().streaming().identity());
1076        assert_ne!(
1077            dataset.identity(),
1078            dataset.clone().subsample(1.0, 7).unwrap().identity()
1079        );
1080        assert_ne!(dataset.identity(), dataset.clone().bootstrap(7).identity());
1081        assert_ne!(
1082            dataset.identity(),
1083            dataset.clone().filter(|_| true).identity()
1084        );
1085    }
1086}