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#[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 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 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 pub fn schema(&self) -> &Arc<Schema> {
303 &self.schema
304 }
305
306 pub fn len(&self) -> usize {
308 self.len
309 }
310
311 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 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 pub fn is_empty(&self) -> bool {
335 self.len == 0
336 }
337
338 pub fn vec4_column(&self, index: usize) -> &[RealVec4] {
340 &self.parts.p4s[index]
341 }
342
343 pub fn scalar_column(&self, index: usize) -> &[f64] {
345 &self.parts.scalars[index]
346 }
347
348 pub fn weights_column(&self) -> Option<&[f64]> {
350 self.parts.weights.as_slice()
351 }
352
353 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 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 pub fn p4_at(&self, col: usize, row: usize) -> RealVec4 {
367 self.parts.p4s[col][row]
368 }
369
370 pub fn scalar_at(&self, col: usize, row: usize) -> f64 {
372 self.parts.scalars[col][row]
373 }
374
375 pub fn weights_at(&self, row: usize) -> f64 {
377 self.parts.weights.at(row)
378 }
379
380 pub fn event(&self, row: usize) -> BatchEvent<'_> {
382 BatchEvent { batch: self, row }
383 }
384
385 pub fn iter(&self) -> impl Iterator<Item = BatchEvent<'_>> {
387 (0..self.len()).map(|i| self.event(i))
388 }
389
390 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 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 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 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 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#[derive(Copy, Clone, Debug)]
520pub struct BatchEvent<'a> {
521 batch: &'a EventBatch,
522 row: usize,
523}
524
525impl<'a> BatchEvent<'a> {
526 pub fn row(&self) -> usize {
528 self.row
529 }
530
531 pub fn batch(&self) -> &'a EventBatch {
533 self.batch
534 }
535
536 pub fn p4(&self, col: usize) -> RealVec4 {
538 self.batch.p4_at(col, self.row)
539 }
540
541 pub fn scalar(&self, col: usize) -> f64 {
543 self.batch.scalar_at(col, self.row)
544 }
545
546 pub fn weight(&self) -> f64 {
548 self.batch.weights_at(self.row)
549 }
550
551 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 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#[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 pub fn row(&self) -> usize {
575 self.row
576 }
577
578 pub fn p4(&self, col: usize) -> RealVec4 {
580 self.batch.p4_at(col, self.row)
581 }
582
583 pub fn scalar(&self, col: usize) -> f64 {
585 self.batch.scalar_at(col, self.row)
586 }
587
588 pub fn weight(&self) -> f64 {
590 self.weight
591 }
592
593 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 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#[derive(Clone, Debug)]
608pub struct OwnedEvent {
609 pub p4s: Vec<RealVec4>,
611 pub scalars: Vec<f64>,
613 pub weight: Option<f64>,
615}
616
617impl OwnedEvent {
618 pub fn new(p4s: Vec<RealVec4>, scalars: Vec<f64>) -> Self {
620 Self {
621 p4s,
622 scalars,
623 weight: None,
624 }
625 }
626
627 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
637pub(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
755pub struct EventBatchBuilder {
757 assembler: BatchAssembler,
758}
759
760impl EventBatchBuilder {
761 pub fn new(schema: Arc<Schema>) -> Self {
763 Self::with_capacity(schema, 0)
764 }
765
766 pub fn with_capacity(schema: Arc<Schema>, capacity: usize) -> Self {
768 Self {
769 assembler: BatchAssembler::new(schema, capacity),
770 }
771 }
772
773 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 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 pub fn push_event(&mut self, event: OwnedEvent) -> LadduDataResult<&mut Self> {
820 self.assembler.push_owned(event)?;
821 Ok(self)
822 }
823
824 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 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}