Skip to main content

laddu_data/data/
event.rs

1use std::fmt;
2use std::sync::Arc;
3
4use laddu_physics::vectors::RealVec4;
5
6use crate::{
7    BatchLayout, LadduDataError, LadduDataResult,
8    schema::{Precision, Schema},
9};
10
11#[derive(Clone, Debug)]
12struct BatchParts {
13    p4s: Arc<[Arc<[RealVec4]>]>,
14    scalars: Arc<[Arc<[f64]>]>,
15    weights: Weights,
16}
17
18#[derive(Clone, Debug)]
19enum Weights {
20    ImplicitUnit,
21    Explicit(Arc<[f64]>),
22}
23
24impl Weights {
25    fn from_option(weights: Option<Arc<[f64]>>) -> Self {
26        match weights {
27            Some(weights) => Self::Explicit(weights),
28            None => Self::ImplicitUnit,
29        }
30    }
31
32    fn as_slice(&self) -> Option<&[f64]> {
33        match self {
34            Self::ImplicitUnit => None,
35            Self::Explicit(weights) => Some(weights),
36        }
37    }
38
39    fn at(&self, row: usize) -> f64 {
40        self.as_slice().map_or(1.0, |weights| weights[row])
41    }
42
43    fn is_explicit(&self) -> bool {
44        matches!(self, Self::Explicit(_))
45    }
46
47    fn select(&self, rows: &[usize]) -> Self {
48        match self {
49            Self::ImplicitUnit => Self::ImplicitUnit,
50            Self::Explicit(weights) => {
51                let selected: Arc<[f64]> = rows.iter().map(|&row| weights[row]).collect();
52                Self::Explicit(selected)
53            }
54        }
55    }
56
57    fn slice(&self, start: usize, end: usize) -> Self {
58        match self {
59            Self::ImplicitUnit => Self::ImplicitUnit,
60            Self::Explicit(weights) => Self::Explicit(Arc::from(&weights[start..end])),
61        }
62    }
63
64    fn reweight<F>(&self, len: usize, f: F) -> Self
65    where
66        F: Fn(usize, f64) -> f64,
67    {
68        let weights: Arc<[f64]> = (0..len).map(|i| f(i, self.at(i))).collect();
69        Self::Explicit(weights)
70    }
71}
72
73impl BatchParts {
74    fn from_columns(p4s: Vec<Arc<[RealVec4]>>, scalars: Vec<Arc<[f64]>>, weights: Weights) -> Self {
75        Self {
76            p4s: p4s.into(),
77            scalars: scalars.into(),
78            weights,
79        }
80    }
81
82    fn validate(&self, schema: &Schema, expected_len: Option<usize>) -> LadduDataResult<usize> {
83        if self.p4s.len() != schema.n_p4s() {
84            return Err(LadduDataError::Schema(
85                "wrong number of vec4 columns".into(),
86            ));
87        }
88
89        if self.scalars.len() != schema.n_scalars() {
90            return Err(LadduDataError::Schema(
91                "wrong number of scalar columns".into(),
92            ));
93        }
94
95        let len = infer_len(&self.p4s, &self.scalars, self.weights.as_slice())?;
96        if let Some(expected_len) = expected_len {
97            let has_columns =
98                !self.p4s.is_empty() || !self.scalars.is_empty() || self.weights.is_explicit();
99            if has_columns && len != expected_len {
100                return Err(LadduDataError::Schema("inconsistent batch length".into()));
101            }
102            return Ok(expected_len);
103        }
104        Ok(len)
105    }
106
107    fn select(&self, rows: &[usize]) -> Self {
108        let p4s = self
109            .p4s
110            .iter()
111            .map(|col| rows.iter().map(|&i| col[i]).collect())
112            .collect();
113        let scalars = self
114            .scalars
115            .iter()
116            .map(|col| rows.iter().map(|&i| col[i]).collect())
117            .collect();
118
119        Self {
120            p4s,
121            scalars,
122            weights: self.weights.select(rows),
123        }
124    }
125
126    fn slice(&self, start: usize, end: usize) -> Self {
127        let p4s = self
128            .p4s
129            .iter()
130            .map(|col| Arc::<[RealVec4]>::from(&col[start..end]))
131            .collect();
132        let scalars = self
133            .scalars
134            .iter()
135            .map(|col| Arc::<[f64]>::from(&col[start..end]))
136            .collect();
137
138        Self {
139            p4s,
140            scalars,
141            weights: self.weights.slice(start, end),
142        }
143    }
144
145    fn reweight<F>(&self, len: usize, f: F) -> Self
146    where
147        F: Fn(usize, f64) -> f64,
148    {
149        Self {
150            p4s: Arc::clone(&self.p4s),
151            scalars: Arc::clone(&self.scalars),
152            weights: self.weights.reweight(len, f),
153        }
154    }
155
156    fn concat(batches: &[(&Self, usize)]) -> Self {
157        let len: usize = batches.iter().map(|(_, len)| *len).sum();
158        let n_p4s = batches.first().map_or(0, |(batch, _)| batch.p4s.len());
159        let n_scalars = batches.first().map_or(0, |(batch, _)| batch.scalars.len());
160
161        let mut p4s = Vec::with_capacity(n_p4s);
162        for col in 0..n_p4s {
163            let mut out = Vec::with_capacity(len);
164            for (batch, _) in batches {
165                out.extend_from_slice(&batch.p4s[col]);
166            }
167            p4s.push(Arc::from(out));
168        }
169
170        let mut scalars = Vec::with_capacity(n_scalars);
171        for col in 0..n_scalars {
172            let mut out = Vec::with_capacity(len);
173            for (batch, _) in batches {
174                out.extend_from_slice(&batch.scalars[col]);
175            }
176            scalars.push(Arc::from(out));
177        }
178
179        let weights = if batches.iter().any(|(batch, _)| batch.weights.is_explicit()) {
180            let mut out = Vec::with_capacity(len);
181            for (batch, batch_len) in batches {
182                for row in 0..*batch_len {
183                    out.push(batch.weights.at(row));
184                }
185            }
186            Weights::Explicit(Arc::from(out))
187        } else {
188            Weights::ImplicitUnit
189        };
190
191        Self::from_columns(p4s, scalars, weights)
192    }
193}
194
195#[derive(Default)]
196enum WeightAssembler {
197    #[default]
198    ImplicitUnit,
199    Explicit(Vec<f64>),
200}
201
202impl WeightAssembler {
203    fn push(&mut self, weight: Option<f64>, len: usize) -> LadduDataResult<()> {
204        match self {
205            Self::Explicit(weights) => match weight {
206                Some(weight) => weights.push(weight),
207                None => {
208                    return Err(LadduDataError::InvalidArgument(
209                        "cannot mix weighted and unweighted events in one batch",
210                    ));
211                }
212            },
213            Self::ImplicitUnit => match weight {
214                Some(weight) if len == 0 => *self = Self::Explicit(vec![weight]),
215                Some(_) => {
216                    return Err(LadduDataError::InvalidArgument(
217                        "cannot mix unweighted and weighted events in one batch",
218                    ));
219                }
220                None => {}
221            },
222        }
223
224        Ok(())
225    }
226
227    fn finish(self) -> Weights {
228        match self {
229            Self::ImplicitUnit => Weights::ImplicitUnit,
230            Self::Explicit(weights) => Weights::Explicit(Arc::from(weights)),
231        }
232    }
233}
234
235/// Immutable columnar batch of events sharing one schema.
236#[derive(Clone)]
237pub struct EventBatch {
238    schema: Arc<Schema>,
239    len: usize,
240    parts: BatchParts,
241}
242
243impl fmt::Debug for EventBatch {
244    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245        formatter
246            .debug_struct("EventBatch")
247            .field("schema", &self.schema)
248            .field("len", &self.len)
249            .field("p4s", &self.parts.p4s)
250            .field("scalars", &self.parts.scalars)
251            .field("weights", &self.parts.weights.as_slice())
252            .finish()
253    }
254}
255
256impl EventBatch {
257    /// Validates column counts and lengths and constructs a batch.
258    ///
259    /// # Errors
260    ///
261    /// Returns [`LadduDataError`] when column counts do not match `schema` or
262    /// column and weight lengths are inconsistent.
263    pub fn new(
264        schema: Arc<Schema>,
265        p4s: Vec<Arc<[RealVec4]>>,
266        scalars: Vec<Arc<[f64]>>,
267        weights: Option<Arc<[f64]>>,
268    ) -> LadduDataResult<Self> {
269        BatchAssembler::from_columns(schema, p4s, scalars, weights)
270    }
271
272    fn from_parts(schema: Arc<Schema>, parts: BatchParts) -> LadduDataResult<Self> {
273        let len = parts.validate(&schema, None)?;
274        Ok(Self { schema, len, parts })
275    }
276
277    fn from_parts_with_len(
278        schema: Arc<Schema>,
279        parts: BatchParts,
280        expected_len: usize,
281    ) -> LadduDataResult<Self> {
282        let len = parts.validate(&schema, Some(expected_len))?;
283        Ok(Self { schema, len, parts })
284    }
285
286    /// Collects owned row events into a columnar batch.
287    ///
288    /// # Errors
289    ///
290    /// Returns [`LadduDataError`] when an event has the wrong number of values
291    /// or weighted and unweighted events are mixed.
292    pub fn from_events<I>(schema: Arc<Schema>, events: I) -> LadduDataResult<Self>
293    where
294        I: IntoIterator<Item = OwnedEvent>,
295    {
296        let mut builder = EventBatchBuilder::new(schema);
297        builder.extend(events)?;
298        builder.finish()
299    }
300
301    /// Returns the shared logical schema.
302    pub fn schema(&self) -> &Arc<Schema> {
303        &self.schema
304    }
305
306    /// Returns the number of rows.
307    pub fn len(&self) -> usize {
308        self.len
309    }
310
311    /// Returns the logical payload bytes per event represented by this batch.
312    pub fn bytes_per_event(&self) -> usize {
313        BatchLayout::from_batch(self)
314            .bytes_per_event(Precision::F64)
315            .ok()
316            .and_then(|bytes| usize::try_from(bytes).ok())
317            .unwrap_or(usize::MAX)
318    }
319
320    /// Returns the retained column payload size in bytes.
321    ///
322    /// Shared schema metadata, allocation headers, and other owners of shared
323    /// columns are not included.
324    pub fn resident_bytes(&self) -> usize {
325        BatchLayout::from_batch(self)
326            .footprint(Precision::F64)
327            .and_then(|footprint| footprint.checked_peak_bytes(self.len))
328            .ok()
329            .and_then(|bytes| usize::try_from(bytes).ok())
330            .unwrap_or(usize::MAX)
331    }
332
333    /// Returns whether the batch contains no rows.
334    pub fn is_empty(&self) -> bool {
335        self.len == 0
336    }
337
338    /// Returns a four-momentum column by index.
339    pub fn vec4_column(&self, index: usize) -> &[RealVec4] {
340        &self.parts.p4s[index]
341    }
342
343    /// Returns a scalar column by index.
344    pub fn scalar_column(&self, index: usize) -> &[f64] {
345        &self.parts.scalars[index]
346    }
347
348    /// Returns the optional explicit weight column.
349    pub fn weights_column(&self) -> Option<&[f64]> {
350        self.parts.weights.as_slice()
351    }
352
353    /// Returns a four-momentum column by logical name.
354    pub fn vec4_column_named(&self, name: &str) -> Option<&[RealVec4]> {
355        let i = self.schema.p4_index(name)?;
356        Some(self.vec4_column(i))
357    }
358
359    /// Returns a scalar column by logical name.
360    pub fn scalar_column_named(&self, name: &str) -> Option<&[f64]> {
361        let i = self.schema.scalar_index(name)?;
362        Some(self.scalar_column(i))
363    }
364
365    /// Returns one four-momentum cell.
366    pub fn p4_at(&self, col: usize, row: usize) -> RealVec4 {
367        self.parts.p4s[col][row]
368    }
369
370    /// Returns one scalar cell.
371    pub fn scalar_at(&self, col: usize, row: usize) -> f64 {
372        self.parts.scalars[col][row]
373    }
374
375    /// Returns the explicit row weight, or one when weights are absent.
376    pub fn weights_at(&self, row: usize) -> f64 {
377        self.parts.weights.at(row)
378    }
379
380    /// Returns a borrowed view of one row.
381    pub fn event(&self, row: usize) -> BatchEvent<'_> {
382        BatchEvent { batch: self, row }
383    }
384
385    /// Iterates over borrowed event views.
386    pub fn iter(&self) -> impl Iterator<Item = BatchEvent<'_>> {
387        (0..self.len()).map(|i| self.event(i))
388    }
389
390    /// Copies selected rows into a new batch in the requested order.
391    ///
392    /// # Panics
393    ///
394    /// Panics when a selected row is outside the batch.
395    pub fn select(&self, rows: &[usize]) -> Self {
396        Self::from_parts_with_len(
397            Arc::clone(&self.schema),
398            self.parts.select(rows),
399            rows.len(),
400        )
401        .expect("select preserves EventBatch invariants")
402    }
403
404    /// Copies rows satisfying `keep` into a new batch.
405    pub fn filter<F>(&self, keep: F) -> Self
406    where
407        F: Fn(BatchEvent<'_>) -> bool,
408    {
409        let rows: Vec<usize> = (0..self.len).filter(|&i| keep(self.event(i))).collect();
410
411        self.select(&rows)
412    }
413
414    /// Returns a batch sharing value columns with newly computed weights.
415    ///
416    /// # Panics
417    ///
418    /// Panics if an internal batch invariant is violated while rebuilding the
419    /// batch.
420    pub fn reweight<F>(&self, f: F) -> Self
421    where
422        F: Fn(usize, f64) -> f64,
423    {
424        Self::from_parts(Arc::clone(&self.schema), self.parts.reweight(self.len, f))
425            .expect("reweight preserves EventBatch invariants")
426    }
427
428    /// Copies the half-open row range `start..end` into a new batch.
429    ///
430    /// # Panics
431    ///
432    /// Panics when `start > end` or `end` exceeds the batch length.
433    pub fn slice(&self, start: usize, end: usize) -> Self {
434        assert!(start <= end);
435        assert!(end <= self.len);
436
437        if start == 0 && end == self.len {
438            return self.clone();
439        }
440
441        Self::from_parts_with_len(
442            Arc::clone(&self.schema),
443            self.parts.slice(start, end),
444            end - start,
445        )
446        .expect("slice preserves EventBatch invariants")
447    }
448
449    /// Concatenates schema-compatible batches.
450    ///
451    /// # Errors
452    ///
453    /// Returns [`LadduDataError`] when `batches` is empty or contains
454    /// incompatible schemas.
455    pub fn concat(batches: &[Self]) -> LadduDataResult<Self> {
456        if batches.is_empty() {
457            return Err(LadduDataError::InvalidArgument(
458                "cannot concatenate zero batches",
459            ));
460        }
461
462        let schema = Arc::clone(&batches[0].schema);
463
464        for batch in batches {
465            if schema != batch.schema {
466                return Err(LadduDataError::Schema(
467                    "cannot concatenate batches with different schemas".into(),
468                ));
469            }
470        }
471
472        let parts = batches
473            .iter()
474            .map(|batch| (&batch.parts, batch.len))
475            .collect::<Vec<_>>();
476        let len = batches.iter().map(|batch| batch.len).sum();
477        Self::from_parts_with_len(schema, BatchParts::concat(&parts), len)
478    }
479}
480
481fn infer_len(
482    vec4s: &[Arc<[RealVec4]>],
483    scalars: &[Arc<[f64]>],
484    weight: Option<&[f64]>,
485) -> LadduDataResult<usize> {
486    let len = vec4s
487        .first()
488        .map(|c| c.len())
489        .or_else(|| scalars.first().map(|c| c.len()))
490        .or_else(|| weight.map(|w| w.len()))
491        .unwrap_or(0);
492
493    for col in vec4s {
494        if col.len() != len {
495            return Err(LadduDataError::Schema(
496                "inconsistent vec4 column length".into(),
497            ));
498        }
499    }
500
501    for col in scalars {
502        if col.len() != len {
503            return Err(LadduDataError::Schema(
504                "inconsistent scalar column length".into(),
505            ));
506        }
507    }
508
509    if let Some(w) = weight
510        && w.len() != len
511    {
512        return Err(LadduDataError::Schema("inconsistent weight length".into()));
513    }
514
515    Ok(len)
516}
517
518/// Borrowed view of one row in an [`EventBatch`].
519#[derive(Copy, Clone, Debug)]
520pub struct BatchEvent<'a> {
521    batch: &'a EventBatch,
522    row: usize,
523}
524
525impl<'a> BatchEvent<'a> {
526    /// Returns the row index.
527    pub fn row(&self) -> usize {
528        self.row
529    }
530
531    /// Returns the backing batch.
532    pub fn batch(&self) -> &'a EventBatch {
533        self.batch
534    }
535
536    /// Returns a four-momentum value by column index.
537    pub fn p4(&self, col: usize) -> RealVec4 {
538        self.batch.p4_at(col, self.row)
539    }
540
541    /// Returns a scalar value by column index.
542    pub fn scalar(&self, col: usize) -> f64 {
543        self.batch.scalar_at(col, self.row)
544    }
545
546    /// Returns the row weight, defaulting to one.
547    pub fn weight(&self) -> f64 {
548        self.batch.weights_at(self.row)
549    }
550
551    /// Returns a four-momentum value by logical name.
552    pub fn p4_named(&self, name: &str) -> Option<RealVec4> {
553        let col = self.batch.schema.p4_index(name)?;
554        Some(self.p4(col))
555    }
556
557    /// Returns a scalar value by logical name.
558    pub fn scalar_named(&self, name: &str) -> Option<f64> {
559        let col = self.batch.schema.scalar_index(name)?;
560        Some(self.scalar(col))
561    }
562}
563
564/// Borrowed event view with a possibly transformed weight.
565#[derive(Copy, Clone, Debug)]
566pub struct Event<'a> {
567    pub(super) batch: &'a EventBatch,
568    pub(super) row: usize,
569    pub(super) weight: f64,
570}
571
572impl<'a> Event<'a> {
573    /// Returns the row index in the backing batch.
574    pub fn row(&self) -> usize {
575        self.row
576    }
577
578    /// Returns a four-momentum value by column index.
579    pub fn p4(&self, col: usize) -> RealVec4 {
580        self.batch.p4_at(col, self.row)
581    }
582
583    /// Returns a scalar value by column index.
584    pub fn scalar(&self, col: usize) -> f64 {
585        self.batch.scalar_at(col, self.row)
586    }
587
588    /// Returns this view's effective weight.
589    pub fn weight(&self) -> f64 {
590        self.weight
591    }
592
593    /// Returns a four-momentum value by logical name.
594    pub fn p4_named(&self, name: &str) -> Option<RealVec4> {
595        let col = self.batch.schema.p4_index(name)?;
596        Some(self.p4(col))
597    }
598
599    /// Returns a scalar value by logical name.
600    pub fn scalar_named(&self, name: &str) -> Option<f64> {
601        let col = self.batch.schema.scalar_index(name)?;
602        Some(self.scalar(col))
603    }
604}
605
606/// Owned row-oriented event used while constructing batches.
607#[derive(Clone, Debug)]
608pub struct OwnedEvent {
609    /// Four-momentum values in schema order.
610    pub p4s: Vec<RealVec4>,
611    /// Scalar values in schema order.
612    pub scalars: Vec<f64>,
613    /// Optional explicit event weight.
614    pub weight: Option<f64>,
615}
616
617impl OwnedEvent {
618    /// Creates an unweighted owned event.
619    pub fn new(p4s: Vec<RealVec4>, scalars: Vec<f64>) -> Self {
620        Self {
621            p4s,
622            scalars,
623            weight: None,
624        }
625    }
626
627    /// Creates an owned event with an explicit weight.
628    pub fn weighted(p4s: Vec<RealVec4>, scalars: Vec<f64>, weight: f64) -> Self {
629        Self {
630            p4s,
631            scalars,
632            weight: Some(weight),
633        }
634    }
635}
636
637/// Shared checked assembly for row-oriented batch producers.
638pub(crate) struct BatchAssembler {
639    schema: Arc<Schema>,
640    p4s: Vec<Vec<RealVec4>>,
641    scalars: Vec<Vec<f64>>,
642    weights: WeightAssembler,
643    len: usize,
644}
645
646impl BatchAssembler {
647    pub(crate) fn new(schema: Arc<Schema>, capacity: usize) -> Self {
648        let p4s = (0..schema.n_p4s())
649            .map(|_| Vec::with_capacity(capacity))
650            .collect();
651        let scalars = (0..schema.n_scalars())
652            .map(|_| Vec::with_capacity(capacity))
653            .collect();
654
655        Self {
656            schema,
657            p4s,
658            scalars,
659            weights: WeightAssembler::default(),
660            len: 0,
661        }
662    }
663
664    pub(crate) fn with_weight_mode(
665        schema: Arc<Schema>,
666        capacity: usize,
667        explicit_weights: bool,
668    ) -> Self {
669        let mut assembler = Self::new(schema, capacity);
670        if explicit_weights {
671            assembler.weights = WeightAssembler::Explicit(Vec::with_capacity(capacity));
672        }
673        assembler
674    }
675
676    pub(crate) fn from_columns(
677        schema: Arc<Schema>,
678        p4s: Vec<Arc<[RealVec4]>>,
679        scalars: Vec<Arc<[f64]>>,
680        weights: Option<Arc<[f64]>>,
681    ) -> LadduDataResult<EventBatch> {
682        EventBatch::from_parts(
683            schema,
684            BatchParts::from_columns(p4s, scalars, Weights::from_option(weights)),
685        )
686    }
687
688    fn push_owned(&mut self, event: OwnedEvent) -> LadduDataResult<()> {
689        if event.p4s.len() != self.schema.n_p4s() {
690            return Err(LadduDataError::Schema(
691                "wrong number of event vec4 values".into(),
692            ));
693        }
694
695        if event.scalars.len() != self.schema.n_scalars() {
696            return Err(LadduDataError::Schema(
697                "wrong number of event scalar values".into(),
698            ));
699        }
700
701        self.weights.push(event.weight, self.len)?;
702
703        for (col, value) in event.p4s.into_iter().enumerate() {
704            self.p4s[col].push(value);
705        }
706        for (col, value) in event.scalars.into_iter().enumerate() {
707            self.scalars[col].push(value);
708        }
709
710        self.len += 1;
711        Ok(())
712    }
713
714    pub(crate) fn push_borrowed(
715        &mut self,
716        event: Event<'_>,
717        explicit_weight: bool,
718    ) -> LadduDataResult<()> {
719        if event.batch.schema().n_p4s() != self.schema.n_p4s() {
720            return Err(LadduDataError::Schema(
721                "wrong number of event vec4 values".into(),
722            ));
723        }
724
725        if event.batch.schema().n_scalars() != self.schema.n_scalars() {
726            return Err(LadduDataError::Schema(
727                "wrong number of event scalar values".into(),
728            ));
729        }
730
731        self.weights
732            .push(explicit_weight.then_some(event.weight()), self.len)?;
733
734        for col in 0..self.schema.n_p4s() {
735            self.p4s[col].push(event.p4(col));
736        }
737        for col in 0..self.schema.n_scalars() {
738            self.scalars[col].push(event.scalar(col));
739        }
740
741        self.len += 1;
742        Ok(())
743    }
744
745    pub(crate) fn finish(self) -> LadduDataResult<EventBatch> {
746        let parts = BatchParts::from_columns(
747            self.p4s.into_iter().map(Arc::from).collect(),
748            self.scalars.into_iter().map(Arc::from).collect(),
749            self.weights.finish(),
750        );
751        EventBatch::from_parts_with_len(self.schema, parts, self.len)
752    }
753}
754
755/// Incremental builder for a columnar [`EventBatch`].
756pub struct EventBatchBuilder {
757    assembler: BatchAssembler,
758}
759
760impl EventBatchBuilder {
761    /// Creates an empty builder.
762    pub fn new(schema: Arc<Schema>) -> Self {
763        Self::with_capacity(schema, 0)
764    }
765
766    /// Creates an empty builder with per-column capacity.
767    pub fn with_capacity(schema: Arc<Schema>, capacity: usize) -> Self {
768        Self {
769            assembler: BatchAssembler::new(schema, capacity),
770        }
771    }
772
773    /// Appends an unweighted event from ordered values.
774    ///
775    /// # Errors
776    ///
777    /// Returns [`LadduDataError`] when value counts do not match the schema or
778    /// the builder already contains weighted events.
779    pub fn push<P, S>(&mut self, p4s: P, scalars: S) -> LadduDataResult<&mut Self>
780    where
781        P: IntoIterator<Item = RealVec4>,
782        S: IntoIterator<Item = f64>,
783    {
784        self.push_event(OwnedEvent::new(
785            p4s.into_iter().collect(),
786            scalars.into_iter().collect(),
787        ))
788    }
789
790    /// Appends a weighted event from ordered values.
791    ///
792    /// # Errors
793    ///
794    /// Returns [`LadduDataError`] when value counts do not match the schema or
795    /// the builder already contains unweighted events.
796    pub fn push_weighted<P, S>(
797        &mut self,
798        p4s: P,
799        scalars: S,
800        weight: f64,
801    ) -> LadduDataResult<&mut Self>
802    where
803        P: IntoIterator<Item = RealVec4>,
804        S: IntoIterator<Item = f64>,
805    {
806        self.push_event(OwnedEvent::weighted(
807            p4s.into_iter().collect(),
808            scalars.into_iter().collect(),
809            weight,
810        ))
811    }
812
813    /// Validates and appends one owned event.
814    ///
815    /// # Errors
816    ///
817    /// Returns [`LadduDataError`] when the event shape does not match the
818    /// schema or its weight presence differs from prior events.
819    pub fn push_event(&mut self, event: OwnedEvent) -> LadduDataResult<&mut Self> {
820        self.assembler.push_owned(event)?;
821        Ok(self)
822    }
823
824    /// Appends all owned events from an iterator.
825    ///
826    /// # Errors
827    ///
828    /// Returns the first [`LadduDataError`] produced by an event whose shape or
829    /// weight presence is incompatible with the builder.
830    pub fn extend<I>(&mut self, events: I) -> LadduDataResult<&mut Self>
831    where
832        I: IntoIterator<Item = OwnedEvent>,
833    {
834        for event in events {
835            self.push_event(event)?;
836        }
837
838        Ok(self)
839    }
840
841    /// Finalizes the builder into an immutable batch.
842    ///
843    /// # Errors
844    ///
845    /// Returns [`LadduDataError`] if the accumulated columns or weights have
846    /// inconsistent lengths.
847    pub fn finish(self) -> LadduDataResult<EventBatch> {
848        self.assembler.finish()
849    }
850}
851
852#[cfg(test)]
853mod tests {
854    use super::*;
855
856    fn v(x: f64) -> RealVec4 {
857        RealVec4 {
858            e: x + 0.3,
859            px: x,
860            py: x + 0.1,
861            pz: x + 0.2,
862        }
863    }
864
865    fn schema_with_weight() -> Arc<Schema> {
866        Arc::new(Schema::new(["p"], ["x"], true).unwrap())
867    }
868
869    fn weighted_batch(start: usize, len: usize) -> EventBatch {
870        let schema = schema_with_weight();
871
872        let events = (start..start + len)
873            .map(|i| OwnedEvent::weighted(vec![v(i as f64)], vec![i as f64], 10.0 + i as f64));
874
875        EventBatch::from_events(schema, events).unwrap()
876    }
877
878    fn scalar_values(batch: &EventBatch) -> Vec<f64> {
879        batch.scalar_column(0).to_vec()
880    }
881
882    #[test]
883    fn event_batch_rejects_shape_mismatches_and_builder_rejects_mixed_weights() {
884        let schema = schema_with_weight();
885
886        let bad_vec4_count = EventBatch::new(
887            Arc::clone(&schema),
888            vec![],
889            vec![Arc::from([1.0, 2.0])],
890            Some(Arc::from([1.0, 2.0])),
891        );
892
893        assert!(matches!(bad_vec4_count, Err(LadduDataError::Schema(_))));
894
895        let bad_lengths = EventBatch::new(
896            Arc::clone(&schema),
897            vec![Arc::from([v(1.0), v(2.0)])],
898            vec![Arc::from([1.0])],
899            Some(Arc::from([1.0, 2.0])),
900        );
901
902        assert!(matches!(bad_lengths, Err(LadduDataError::Schema(_))));
903
904        let mut builder = EventBatchBuilder::new(schema);
905        builder.push([v(1.0)], [1.0]).unwrap();
906
907        let mixed = builder.push_weighted([v(2.0)], [2.0], 2.0);
908        assert!(matches!(mixed, Err(LadduDataError::InvalidArgument(_))));
909    }
910
911    #[test]
912    fn select_slice_filter_reweight_and_concat_preserve_columns_and_weight_semantics() {
913        let weighted = weighted_batch(0, 4);
914        let selected = weighted.select(&[3, 1]);
915
916        assert_eq!(scalar_values(&selected), vec![3.0, 1.0]);
917        assert_eq!(selected.weights_column().unwrap(), &[13.0, 11.0]);
918        assert_eq!(selected.p4_at(0, 0).px, 3.0);
919        assert_eq!(selected.p4_at(0, 1).e, 1.3);
920
921        let sliced = weighted.slice(1, 3);
922        assert_eq!(scalar_values(&sliced), vec![1.0, 2.0]);
923        assert_eq!(sliced.weights_column().unwrap(), &[11.0, 12.0]);
924
925        let filtered = weighted.filter(|ev| ev.scalar(0) >= 2.0);
926        assert_eq!(scalar_values(&filtered), vec![2.0, 3.0]);
927
928        let reweighted = filtered.reweight(|i, w| w + 100.0 + i as f64);
929        assert_eq!(reweighted.weights_column().unwrap(), &[112.0, 114.0]);
930
931        let schema = schema_with_weight();
932
933        let unweighted_with_weight_schema = EventBatch::from_events(
934            Arc::clone(&schema),
935            [
936                OwnedEvent::new(vec![v(100.0)], vec![100.0]),
937                OwnedEvent::new(vec![v(101.0)], vec![101.0]),
938            ],
939        )
940        .unwrap();
941
942        let weighted_tail = EventBatch::from_events(
943            schema,
944            [
945                OwnedEvent::weighted(vec![v(200.0)], vec![200.0], 5.0),
946                OwnedEvent::weighted(vec![v(201.0)], vec![201.0], 6.0),
947            ],
948        )
949        .unwrap();
950
951        let concatenated =
952            EventBatch::concat(&[unweighted_with_weight_schema, weighted_tail]).unwrap();
953
954        assert_eq!(
955            scalar_values(&concatenated),
956            vec![100.0, 101.0, 200.0, 201.0]
957        );
958        assert_eq!(
959            concatenated.weights_column().unwrap(),
960            &[1.0, 1.0, 5.0, 6.0]
961        );
962    }
963
964    #[test]
965    fn implicit_unit_weights_survive_assembly_and_row_transforms() {
966        let schema = Arc::new(Schema::new(["p"], ["x"], false).unwrap());
967        let batch = EventBatch::from_events(
968            Arc::clone(&schema),
969            (0..3).map(|i| OwnedEvent::new(vec![v(i as f64)], vec![i as f64])),
970        )
971        .unwrap();
972
973        assert!(batch.weights_column().is_none());
974        assert_eq!(batch.weights_at(2), 1.0);
975
976        let selected = batch.select(&[2, 0]);
977        let sliced = batch.slice(1, 3);
978        let filtered = batch.filter(|event| event.scalar(0) > 0.0);
979        let concatenated = EventBatch::concat(&[selected, sliced]).unwrap();
980
981        assert!(filtered.weights_column().is_none());
982        assert!(concatenated.weights_column().is_none());
983        assert_eq!(concatenated.weights_at(3), 1.0);
984
985        let reweighted = batch.reweight(|row, weight| weight + row as f64);
986        assert_eq!(reweighted.weights_column().unwrap(), &[1.0, 2.0, 3.0]);
987    }
988
989    #[test]
990    fn shared_assembler_preserves_weight_mode_and_rejects_transitions() {
991        let schema = Arc::new(Schema::new(["p"], ["x"], true).unwrap());
992        let source = EventBatch::from_events(
993            Arc::clone(&schema),
994            [
995                OwnedEvent::weighted(vec![v(1.0)], vec![1.0], 2.0),
996                OwnedEvent::weighted(vec![v(2.0)], vec![2.0], 3.0),
997            ],
998        )
999        .unwrap();
1000
1001        let mut explicit = BatchAssembler::new(Arc::clone(&schema), 2);
1002        let first = Event {
1003            batch: &source,
1004            row: 0,
1005            weight: source.weights_at(0),
1006        };
1007        explicit.push_borrowed(first, true).unwrap();
1008        let second = Event {
1009            batch: &source,
1010            row: 1,
1011            weight: source.weights_at(1),
1012        };
1013        let transition = explicit.push_borrowed(second, false);
1014        assert!(matches!(
1015            transition,
1016            Err(LadduDataError::InvalidArgument(_))
1017        ));
1018        let explicit = explicit.finish().unwrap();
1019        assert_eq!(explicit.len(), 1);
1020        assert_eq!(explicit.weights_column().unwrap(), &[2.0]);
1021
1022        let unweighted = EventBatch::from_events(
1023            Arc::clone(&schema),
1024            [OwnedEvent::new(vec![v(3.0)], vec![3.0])],
1025        )
1026        .unwrap();
1027        let mut implicit = BatchAssembler::new(schema, 1);
1028        let event = Event {
1029            batch: &unweighted,
1030            row: 0,
1031            weight: unweighted.weights_at(0),
1032        };
1033        implicit.push_borrowed(event, false).unwrap();
1034        let implicit = implicit.finish().unwrap();
1035        assert!(implicit.weights_column().is_none());
1036        assert_eq!(implicit.weights_at(0), 1.0);
1037    }
1038
1039    #[test]
1040    fn assembly_table_covers_column_shapes_and_observable_sharing() {
1041        let cases = [
1042            (
1043                Arc::new(Schema::new(Vec::<&str>::new(), Vec::<&str>::new(), false).unwrap()),
1044                false,
1045            ),
1046            (
1047                Arc::new(Schema::new(["p"], Vec::<&str>::new(), false).unwrap()),
1048                false,
1049            ),
1050            (
1051                Arc::new(Schema::new(Vec::<&str>::new(), ["x"], false).unwrap()),
1052                false,
1053            ),
1054            (
1055                Arc::new(Schema::new(Vec::<&str>::new(), Vec::<&str>::new(), true).unwrap()),
1056                true,
1057            ),
1058            (Arc::new(Schema::new(["p"], ["x"], true).unwrap()), true),
1059        ];
1060
1061        for (schema, weighted) in cases {
1062            let events = (0..2).map(|i| {
1063                let p4s = if schema.n_p4s() == 0 {
1064                    Vec::new()
1065                } else {
1066                    vec![v(i as f64)]
1067                };
1068                let scalars = if schema.n_scalars() == 0 {
1069                    Vec::new()
1070                } else {
1071                    vec![i as f64]
1072                };
1073                if weighted {
1074                    OwnedEvent::weighted(p4s, scalars, 2.0 + i as f64)
1075                } else {
1076                    OwnedEvent::new(p4s, scalars)
1077                }
1078            });
1079            let batch = EventBatch::from_events(Arc::clone(&schema), events).unwrap();
1080            assert_eq!(batch.len(), 2);
1081            assert_eq!(batch.weights_column().is_some(), weighted);
1082
1083            let selected = batch.select(&(0..batch.len()).collect::<Vec<_>>());
1084            assert_eq!(selected.len(), batch.len());
1085            for col in 0..schema.n_p4s() {
1086                assert_eq!(selected.vec4_column(col), batch.vec4_column(col));
1087            }
1088            for col in 0..schema.n_scalars() {
1089                assert_eq!(selected.scalar_column(col), batch.scalar_column(col));
1090            }
1091            assert_eq!(selected.weights_column(), batch.weights_column());
1092
1093            let concatenated = EventBatch::concat(&[batch.slice(0, 1), batch.slice(1, 2)]).unwrap();
1094            assert_eq!(concatenated.len(), batch.len());
1095            for col in 0..schema.n_p4s() {
1096                assert_eq!(concatenated.vec4_column(col), batch.vec4_column(col));
1097            }
1098            for col in 0..schema.n_scalars() {
1099                assert_eq!(concatenated.scalar_column(col), batch.scalar_column(col));
1100            }
1101            assert_eq!(concatenated.weights_column(), batch.weights_column());
1102
1103            if schema.n_p4s() > 0 || schema.n_scalars() > 0 {
1104                let reweighted = batch.reweight(|_, weight| weight + 1.0);
1105                if schema.n_p4s() > 0 {
1106                    assert_eq!(
1107                        reweighted.vec4_column(0).as_ptr(),
1108                        batch.vec4_column(0).as_ptr()
1109                    );
1110                }
1111                if schema.n_scalars() > 0 {
1112                    assert_eq!(
1113                        reweighted.scalar_column(0).as_ptr(),
1114                        batch.scalar_column(0).as_ptr()
1115                    );
1116                }
1117            }
1118        }
1119    }
1120}