Skip to main content

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