1use std::{
2 path::{Path, PathBuf},
3 sync::{
4 Arc,
5 mpsc::{self, Receiver, Sender, SyncSender},
6 },
7 thread::{self, JoinHandle},
8};
9
10use laddu_physics::vectors::RealVec4;
11use oxyroot::{Branch, ReaderTree, RootFile, WriterTree};
12
13use crate::{
14 LadduDataError, LadduDataResult, Name,
15 data::{BatchAssembler, EventBatch},
16 io::{
17 DataFragment, EventSink, EventSource, FragmentedSource, OutputMode, OutputPath, ReadPlan,
18 SinkState, SourceBuild, SourceBuildOptions, SourceCapabilities, WritePlan, build_source,
19 fragmented_batches, sink_error, source_error,
20 },
21 schema::{
22 ColumnInfo, ColumnType, PhysicalColumnRole, PhysicalSchemaPlan, Precision, Schema,
23 SchemaColumnNames, SchemaInferenceOptions, SchemaWriteOptions, WriteWeightColumn,
24 },
25};
26
27mod encode;
28
29use encode::root_output_columns;
30
31#[derive(Clone, Debug)]
33pub struct RootSource {
34 files: Arc<[Arc<PathBuf>]>,
35 tree_name: Name,
36 schema: Arc<Schema>,
37 options: RootReadOptions,
38}
39
40#[derive(Clone, Debug)]
42pub struct RootReadOptions {
43 pub infer_schema: bool,
45 pub validate_all_files: bool,
47 pub sort_glob: bool,
49 pub tree: RootTreeSelection,
51 pub schema_inference: SchemaInferenceOptions,
53}
54
55impl Default for RootReadOptions {
56 fn default() -> Self {
57 Self {
58 infer_schema: true,
59 validate_all_files: true,
60 sort_glob: true,
61 tree: RootTreeSelection::First,
62 schema_inference: SchemaInferenceOptions::default(),
63 }
64 }
65}
66
67#[derive(Clone, Debug, Default)]
69pub enum RootTreeSelection {
70 #[default]
72 First,
73 Named(Name),
75}
76
77#[derive(Clone, Debug)]
79pub struct RootFragmentKey {
80 pub file: Arc<PathBuf>,
82 pub tree_name: Name,
84}
85
86#[derive(Clone, Debug)]
88pub struct RootColumnInfo {
89 pub name: Name,
91 pub item_type_name: String,
93 pub interpretation: String,
95 pub entries: i64,
97}
98
99impl RootSource {
100 pub fn open(pattern: impl AsRef<str>) -> LadduDataResult<Self> {
107 Self::builder(pattern).build()
108 }
109
110 pub fn builder(pattern: impl AsRef<str>) -> RootSourceBuilder {
112 RootSourceBuilder {
113 pattern: pattern.as_ref().to_owned(),
114 schema: None,
115 options: RootReadOptions::default(),
116 }
117 }
118
119 pub fn files(&self) -> &[Arc<PathBuf>] {
121 &self.files
122 }
123
124 pub fn tree_name(&self) -> &str {
126 self.tree_name.as_ref()
127 }
128
129 pub fn tree_names(path: impl AsRef<Path>) -> LadduDataResult<Vec<Name>> {
135 let path = path.as_ref();
136 let mut file = RootFile::open(path)
137 .map_err(|error| source_error("open ROOT file", path.display(), error))?;
138 let key_names: Vec<String> = file.keys_name().map(str::to_owned).collect();
139
140 let mut out = Vec::new();
141
142 for name in key_names {
143 if file.get_tree(&name).is_ok() {
144 out.push(Name::from(name));
145 }
146 }
147
148 Ok(out)
149 }
150
151 pub fn columns(
158 path: impl AsRef<Path>,
159 tree: Option<&str>,
160 ) -> LadduDataResult<Vec<RootColumnInfo>> {
161 let path = path.as_ref();
162 let mut file = RootFile::open(path)
163 .map_err(|error| source_error("open ROOT file", path.display(), error))?;
164
165 let tree_name = match tree {
166 Some(name) => Name::from(name),
167 None => first_tree_name(&mut file, path)?,
168 };
169
170 let tree = file.get_tree(tree_name.as_ref()).map_err(|error| {
171 source_error(
172 "read ROOT tree",
173 format!("{}::{tree_name}", path.display()),
174 error,
175 )
176 })?;
177
178 Ok(tree
179 .branches_r()
180 .into_iter()
181 .map(|branch| RootColumnInfo {
182 name: Name::from(branch.name()),
183 item_type_name: branch.item_type_name(),
184 interpretation: branch.interpretation(),
185 entries: branch.entries(),
186 })
187 .collect())
188 }
189}
190
191pub struct RootSourceBuilder {
193 pattern: String,
194 schema: Option<Arc<Schema>>,
195 options: RootReadOptions,
196}
197
198impl RootSourceBuilder {
199 pub fn schema(mut self, schema: Arc<Schema>) -> Self {
201 self.schema = Some(schema);
202 self.options.infer_schema = false;
203 self
204 }
205
206 pub fn infer_schema(mut self, value: bool) -> Self {
208 self.options.infer_schema = value;
209 self
210 }
211
212 pub fn tree(mut self, name: impl Into<Name>) -> Self {
214 self.options.tree = RootTreeSelection::Named(name.into());
215 self
216 }
217
218 pub fn first_tree(mut self) -> Self {
220 self.options.tree = RootTreeSelection::First;
221 self
222 }
223
224 pub fn require_weight(mut self, value: bool) -> Self {
226 self.options.schema_inference.require_weight = value;
227 self
228 }
229
230 pub fn validate_all_files(mut self, value: bool) -> Self {
232 self.options.validate_all_files = value;
233 self
234 }
235
236 pub fn sort_glob(mut self, value: bool) -> Self {
238 self.options.sort_glob = value;
239 self
240 }
241
242 pub fn schema_inference(mut self, options: SchemaInferenceOptions) -> Self {
244 self.options.schema_inference = options;
245 self
246 }
247
248 pub fn build(self) -> LadduDataResult<RootSource> {
255 let RootSourceBuilder {
256 pattern,
257 schema: explicit_schema,
258 options,
259 } = self;
260 let tree_selection = options.tree.clone();
261 let infer_options = options.schema_inference.clone();
262 let validate_options = options.schema_inference.clone();
263 let SourceBuild {
264 files,
265 context: tree_name,
266 schema,
267 } = build_source(
268 SourceBuildOptions {
269 pattern: &pattern,
270 sort: options.sort_glob,
271 format: "ROOT",
272 explicit_schema,
273 infer_schema: options.infer_schema,
274 validate_all_files: options.validate_all_files,
275 },
276 move |path| resolve_tree_name(path, &tree_selection),
277 move |path, tree_name| {
278 let columns = root_columns(path, tree_name.as_ref())?;
279 Schema::infer_from_columns(
280 columns.iter().map(OwnedColumnInfo::as_column_info),
281 &infer_options,
282 )
283 },
284 move |path, schema, tree_name| {
285 validate_root_file(path, tree_name.as_ref(), schema, &validate_options)
286 },
287 )?;
288
289 Ok(RootSource {
290 files,
291 tree_name,
292 schema,
293 options,
294 })
295 }
296}
297
298impl EventSource for RootSource {
299 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
300 Ok(Arc::clone(&self.schema))
301 }
302
303 fn capabilities(&self) -> SourceCapabilities {
304 SourceCapabilities {
305 exact_len: true,
306 exact_weighted_total: false,
307 random_access: false,
308 deterministic_partitioning: true,
309 predicate_pushdown: false,
310 projection_pushdown: true,
311 streaming: true,
312 }
313 }
314
315 fn num_events(&self) -> LadduDataResult<Option<u64>> {
316 Ok(Some(self.fragments()?.iter().map(|f| f.rows).sum()))
317 }
318
319 fn batches(
320 &self,
321 plan: ReadPlan,
322 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
323 fragmented_batches(Arc::new(self.clone()), plan)
324 }
325}
326
327impl FragmentedSource for RootSource {
328 type Key = RootFragmentKey;
329
330 fn fragments(&self) -> LadduDataResult<Vec<DataFragment<Self::Key>>> {
331 let mut fragments = Vec::new();
332 let mut global_start = 0_u64;
333
334 for path in self.files.iter() {
335 let resource = path.as_ref().display().to_string();
336 let mut file = RootFile::open(path.as_ref())
337 .map_err(|error| source_error("open ROOT file", &resource, error))?;
338 let tree = file.get_tree(self.tree_name.as_ref()).map_err(|error| {
339 source_error(
340 "read ROOT tree",
341 format!("{resource}::{}", self.tree_name),
342 error,
343 )
344 })?;
345
346 let tree_resource = format!("{resource}::{}", self.tree_name);
347 let rows = usize_from_i64(tree.entries(), "negative TTree entry count", &tree_resource)?
348 as u64;
349
350 fragments.push(DataFragment {
351 key: RootFragmentKey {
352 file: Arc::clone(path),
353 tree_name: self.tree_name.clone(),
354 },
355 global_start,
356 rows,
357 });
358
359 global_start += rows;
360 }
361
362 Ok(fragments)
363 }
364
365 fn read_fragment_range(
366 &self,
367 key: &Self::Key,
368 local_start: usize,
369 local_len: usize,
370 chunk_size: Option<usize>,
371 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
372 if matches!(chunk_size, Some(0)) {
373 return Err(LadduDataError::InvalidArgument(
374 "chunk_size must be nonzero",
375 ));
376 }
377
378 Ok(Box::new(RootBatchIter::spawn(
379 Arc::clone(&self.schema),
380 self.options.clone(),
381 key.clone(),
382 local_start,
383 local_len,
384 chunk_size,
385 )))
386 }
387}
388
389struct RootBatchIter {
390 rx: Receiver<LadduDataResult<EventBatch>>,
391 state: RootBatchState,
392}
393
394enum RootBatchState {
395 Receiving(JoinHandle<()>),
396 Done,
397}
398
399impl RootBatchIter {
400 fn spawn(
401 schema: Arc<Schema>,
402 options: RootReadOptions,
403 key: RootFragmentKey,
404 local_start: usize,
405 local_len: usize,
406 chunk_size: Option<usize>,
407 ) -> Self {
408 let (tx, rx) = mpsc::sync_channel(1);
411
412 let handle = thread::spawn(move || {
413 if let Err(err) = read_root_range_and_send_batches(
414 schema,
415 options,
416 key,
417 local_start,
418 local_len,
419 chunk_size,
420 tx.clone(),
421 ) {
422 let _ = tx.send(Err(err));
423 }
424 });
425
426 Self {
427 rx,
428 state: RootBatchState::Receiving(handle),
429 }
430 }
431
432 fn join_if_needed(&mut self) -> Option<LadduDataResult<EventBatch>> {
433 let state = std::mem::replace(&mut self.state, RootBatchState::Done);
434 match state {
435 RootBatchState::Done => None,
436 RootBatchState::Receiving(handle) => {
437 if handle.join().is_err() {
438 Some(Err(LadduDataError::Source(
439 "ROOT reader thread panicked".into(),
440 )))
441 } else {
442 None
443 }
444 }
445 }
446 }
447}
448
449impl Iterator for RootBatchIter {
450 type Item = LadduDataResult<EventBatch>;
451
452 fn next(&mut self) -> Option<Self::Item> {
453 if matches!(self.state, RootBatchState::Done) {
454 return None;
455 }
456
457 match self.rx.recv() {
458 Ok(item) => Some(item),
459 Err(_) => self.join_if_needed(),
460 }
461 }
462}
463
464fn read_root_range_and_send_batches(
465 schema: Arc<Schema>,
466 options: RootReadOptions,
467 key: RootFragmentKey,
468 local_start: usize,
469 local_len: usize,
470 chunk_size: Option<usize>,
471 tx: SyncSender<LadduDataResult<EventBatch>>,
472) -> LadduDataResult<()> {
473 let resource = format!("{}::{}", key.file.as_ref().display(), key.tree_name);
474 let mut file = RootFile::open(key.file.as_ref())
475 .map_err(|error| source_error("open ROOT file", &resource, error))?;
476 let tree = file
477 .get_tree(key.tree_name.as_ref())
478 .map_err(|error| source_error("read ROOT tree", &resource, error))?;
479
480 let mut readers =
481 RootColumnReaders::new(&tree, &schema, &options.schema_inference.column_names)?;
482
483 for _ in 0..local_start {
484 readers.skip_one()?;
485 }
486
487 let mut remaining = local_len;
488 let batch_size = chunk_size.unwrap_or(local_len.max(1));
489
490 while remaining > 0 {
491 let take = remaining.min(batch_size);
492 let batch = readers.read_batch(Arc::clone(&schema), take)?;
493
494 tx.send(Ok(batch))
495 .map_err(|error| source_error("send ROOT batch", &resource, error))?;
496
497 remaining -= take;
498 }
499
500 Ok(())
501}
502
503struct RootColumnReaders<'a> {
504 p4s: Vec<[RootFloatIter<'a>; 4]>,
505 scalars: Vec<RootFloatIter<'a>>,
506 weights: Option<RootFloatIter<'a>>,
507}
508
509impl<'a> RootColumnReaders<'a> {
510 fn new(
511 tree: &'a ReaderTree,
512 schema: &Schema,
513 column_names: &SchemaColumnNames,
514 ) -> LadduDataResult<Self> {
515 let plan = PhysicalSchemaPlan::for_read(schema, column_names);
516 let mut p4s: Vec<[Option<RootFloatIter<'a>>; 4]> = (0..schema.n_p4s())
517 .map(|_| std::array::from_fn(|_| None))
518 .collect();
519 let mut scalars: Vec<Option<RootFloatIter<'a>>> =
520 (0..schema.n_scalars()).map(|_| None).collect();
521 let mut weights = None;
522
523 for column in plan.columns() {
524 let reader = open_float_reader(tree, column.name().as_ref())?;
525 match column.role() {
526 PhysicalColumnRole::P4 { index, component } => {
527 p4s[index][component] = Some(reader);
528 }
529 PhysicalColumnRole::Scalar { index } => {
530 scalars[index] = Some(reader);
531 }
532 PhysicalColumnRole::Weight => {
533 weights = Some(reader);
534 }
535 }
536 }
537
538 let p4s = p4s
539 .into_iter()
540 .map(|parts| {
541 let [e, px, py, pz] = parts;
542 Ok([
543 e.ok_or_else(|| {
544 LadduDataError::Source(
545 "physical schema plan did not bind ROOT E branch".into(),
546 )
547 })?,
548 px.ok_or_else(|| {
549 LadduDataError::Source(
550 "physical schema plan did not bind ROOT px branch".into(),
551 )
552 })?,
553 py.ok_or_else(|| {
554 LadduDataError::Source(
555 "physical schema plan did not bind ROOT py branch".into(),
556 )
557 })?,
558 pz.ok_or_else(|| {
559 LadduDataError::Source(
560 "physical schema plan did not bind ROOT pz branch".into(),
561 )
562 })?,
563 ])
564 })
565 .collect::<LadduDataResult<Vec<_>>>()?;
566 let scalars = scalars
567 .into_iter()
568 .map(|reader| {
569 reader.ok_or_else(|| {
570 LadduDataError::Source(
571 "physical schema plan did not bind ROOT scalar branch".into(),
572 )
573 })
574 })
575 .collect::<LadduDataResult<Vec<_>>>()?;
576
577 Ok(Self {
578 p4s,
579 scalars,
580 weights,
581 })
582 }
583
584 fn skip_one(&mut self) -> LadduDataResult<()> {
585 for [e, px, py, pz] in self.p4s.iter_mut() {
586 e.next_f64()?;
587 px.next_f64()?;
588 py.next_f64()?;
589 pz.next_f64()?;
590 }
591
592 for scalar in self.scalars.iter_mut() {
593 scalar.next_f64()?;
594 }
595
596 if let Some(weights) = self.weights.as_mut() {
597 weights.next_f64()?;
598 }
599
600 Ok(())
601 }
602
603 fn read_batch(&mut self, schema: Arc<Schema>, len: usize) -> LadduDataResult<EventBatch> {
604 let mut p4s = Vec::with_capacity(schema.n_p4s());
605 let mut scalars = Vec::with_capacity(schema.n_scalars());
606
607 for [e, px, py, pz] in self.p4s.iter_mut() {
608 let mut col = Vec::with_capacity(len);
609 for _ in 0..len {
610 col.push(RealVec4 {
611 e: e.next_f64()?,
612 px: px.next_f64()?,
613 py: py.next_f64()?,
614 pz: pz.next_f64()?,
615 });
616 }
617 p4s.push(col);
618 }
619
620 for reader in self.scalars.iter_mut() {
621 let mut col = Vec::with_capacity(len);
622 for _ in 0..len {
623 col.push(reader.next_f64()?);
624 }
625 scalars.push(col);
626 }
627
628 let weights = if let Some(reader) = self.weights.as_mut() {
629 let mut col = Vec::with_capacity(len);
630 for _ in 0..len {
631 col.push(reader.next_f64()?);
632 }
633 Some(col)
634 } else {
635 None
636 };
637
638 BatchAssembler::from_columns(
639 schema,
640 p4s.into_iter().map(Arc::from).collect(),
641 scalars.into_iter().map(Arc::from).collect(),
642 weights.map(Arc::from),
643 )
644 }
645}
646
647enum RootFloatIter<'a> {
648 F64 {
649 name: Name,
650 iter: Box<dyn Iterator<Item = f64> + 'a>,
651 },
652 F32 {
653 name: Name,
654 iter: Box<dyn Iterator<Item = f32> + 'a>,
655 },
656}
657
658impl<'a> RootFloatIter<'a> {
659 fn next_f64(&mut self) -> LadduDataResult<f64> {
660 match self {
661 Self::F64 { name, iter } => iter
662 .next()
663 .ok_or_else(|| source_error("read ROOT branch", name, "ROOT branch ended early")),
664 Self::F32 { name, iter } => iter
665 .next()
666 .map(f64::from)
667 .ok_or_else(|| source_error("read ROOT branch", name, "ROOT branch ended early")),
668 }
669 }
670}
671
672fn open_float_reader<'a>(tree: &'a ReaderTree, name: &str) -> LadduDataResult<RootFloatIter<'a>> {
673 let branch =
674 find_branch(tree, name).ok_or_else(|| LadduDataError::MissingColumn(Name::from(name)))?;
675
676 match root_column_type(branch) {
677 ColumnType::F64 => Ok(RootFloatIter::F64 {
678 name: Name::from(name),
679 iter: Box::new(
680 branch
681 .as_iter::<f64>()
682 .map_err(|error| source_error("open ROOT branch", name, error))?,
683 ),
684 }),
685 ColumnType::F32 => Ok(RootFloatIter::F32 {
686 name: Name::from(name),
687 iter: Box::new(
688 branch
689 .as_iter::<f32>()
690 .map_err(|error| source_error("open ROOT branch", name, error))?,
691 ),
692 }),
693 ColumnType::Other => Err(source_error(
694 "decode ROOT branch",
695 name,
696 format!(
697 "column {name} has unsupported ROOT type {} interpreted as {}",
698 branch.item_type_name(),
699 branch.interpretation()
700 ),
701 )),
702 }
703}
704
705fn find_branch<'a>(tree: &'a ReaderTree, name: &str) -> Option<&'a Branch> {
706 tree.branch(name).or_else(|| {
707 tree.branches_r()
708 .into_iter()
709 .find(|branch| branch.name() == name)
710 })
711}
712
713#[derive(Clone, Debug)]
714struct OwnedColumnInfo {
715 name: Name,
716 dtype: ColumnType,
717}
718
719impl OwnedColumnInfo {
720 fn as_column_info(&self) -> ColumnInfo<'_> {
721 ColumnInfo {
722 name: self.name.as_ref(),
723 dtype: self.dtype,
724 }
725 }
726}
727
728fn root_columns(path: &Path, tree_name: &str) -> LadduDataResult<Vec<OwnedColumnInfo>> {
729 let resource = format!("{}::{tree_name}", path.display());
730 let mut file = RootFile::open(path)
731 .map_err(|error| source_error("open ROOT file", path.display(), error))?;
732 let tree = file
733 .get_tree(tree_name)
734 .map_err(|error| source_error("read ROOT tree", &resource, error))?;
735
736 Ok(tree
737 .branches_r()
738 .into_iter()
739 .map(|branch| OwnedColumnInfo {
740 name: Name::from(branch.name()),
741 dtype: root_column_type(branch),
742 })
743 .collect())
744}
745
746fn validate_root_file(
747 path: &Path,
748 tree_name: &str,
749 schema: &Schema,
750 options: &SchemaInferenceOptions,
751) -> LadduDataResult<()> {
752 let columns = root_columns(path, tree_name)?;
753
754 schema.validate_required_columns(columns.iter().map(OwnedColumnInfo::as_column_info), options)
755}
756
757fn root_column_type(branch: &Branch) -> ColumnType {
758 match branch.interpretation().as_str() {
759 "f64" => ColumnType::F64,
760 "f32" => ColumnType::F32,
761 _ => match branch.item_type_name().as_str() {
762 "double" | "Double_t" | "ROOT::Double_t" => ColumnType::F64,
763 "float" | "Float_t" | "ROOT::Float_t" => ColumnType::F32,
764 _ => ColumnType::Other,
765 },
766 }
767}
768
769fn resolve_tree_name(path: &Path, selection: &RootTreeSelection) -> LadduDataResult<Name> {
770 let mut file = RootFile::open(path)
771 .map_err(|error| source_error("open ROOT file", path.display(), error))?;
772
773 match selection {
774 RootTreeSelection::Named(name) => {
775 file.get_tree(name.as_ref()).map_err(|error| {
776 source_error(
777 "read ROOT tree",
778 format!("{}::{name}", path.display()),
779 error,
780 )
781 })?;
782 Ok(name.clone())
783 }
784 RootTreeSelection::First => first_tree_name(&mut file, path),
785 }
786}
787
788fn first_tree_name(file: &mut RootFile, path: &Path) -> LadduDataResult<Name> {
789 let key_names: Vec<String> = file.keys_name().map(str::to_owned).collect();
790
791 for name in key_names {
792 if file.get_tree(&name).is_ok() {
793 return Ok(Name::from(name));
794 }
795 }
796
797 Err(source_error(
798 "resolve first ROOT tree",
799 path.display(),
800 "no TTree found in ROOT file",
801 ))
802}
803
804fn usize_from_i64(value: i64, message: &'static str, resource: &str) -> LadduDataResult<usize> {
805 if value < 0 {
806 return Err(source_error("read ROOT entry count", resource, message));
807 }
808
809 usize::try_from(value).map_err(|_| {
810 source_error(
811 "read ROOT entry count",
812 resource,
813 "entry count overflows usize",
814 )
815 })
816}
817
818pub struct RootSink {
820 output: OutputPath,
821 options: RootWriteOptions,
822 resolved_path: Option<PathBuf>,
823 event_schema: Option<Arc<Schema>>,
824 senders: Option<RootColumnSenders>,
825 writer_thread: Option<JoinHandle<LadduDataResult<()>>>,
826 state: SinkState,
827}
828
829#[derive(Clone, Debug)]
831pub struct RootWriteOptions {
832 pub tree_name: Name,
834 pub schema_write: SchemaWriteOptions,
836}
837
838impl Default for RootWriteOptions {
839 fn default() -> Self {
840 Self {
841 tree_name: Name::from("tree"),
842 schema_write: SchemaWriteOptions::default(),
843 }
844 }
845}
846
847impl RootSink {
848 pub fn create(path: impl Into<PathBuf>) -> Self {
850 Self::builder(path).build()
851 }
852
853 pub fn builder(path: impl Into<PathBuf>) -> RootSinkBuilder {
855 RootSinkBuilder {
856 output: OutputPath::new(path),
857 options: RootWriteOptions::default(),
858 }
859 }
860
861 pub fn resolved_path(&self) -> Option<&Path> {
863 self.resolved_path.as_deref()
864 }
865}
866
867pub struct RootSinkBuilder {
869 output: OutputPath,
870 options: RootWriteOptions,
871}
872
873impl RootSinkBuilder {
874 pub fn output_mode(mut self, mode: OutputMode) -> Self {
876 self.output = self.output.with_mode(mode);
877 self
878 }
879
880 pub fn single_file(self) -> Self {
882 self.output_mode(OutputMode::SingleFile)
883 }
884
885 pub fn per_rank_files(self) -> Self {
887 self.output_mode(OutputMode::PerRankFiles)
888 }
889
890 pub fn auto_output(self) -> Self {
892 self.output_mode(OutputMode::Auto)
893 }
894
895 pub fn tree(mut self, name: impl Into<Name>) -> Self {
897 self.options.tree_name = name.into();
898 self
899 }
900
901 pub fn schema_write(mut self, options: SchemaWriteOptions) -> Self {
903 self.options.schema_write = options;
904 self
905 }
906
907 pub fn column_names(mut self, column_names: SchemaColumnNames) -> Self {
909 self.options.schema_write.column_names = column_names;
910 self
911 }
912
913 pub fn precision(mut self, precision: Precision) -> Self {
915 self.options.schema_write.precision = precision;
916 self
917 }
918
919 pub fn write_weight_column(mut self, value: WriteWeightColumn) -> Self {
921 self.options.schema_write.write_weight_column = value;
922 self
923 }
924
925 pub fn build(self) -> RootSink {
927 RootSink {
928 output: self.output,
929 options: self.options,
930 resolved_path: None,
931 event_schema: None,
932 senders: None,
933 writer_thread: None,
934 state: SinkState::Idle,
935 }
936 }
937}
938
939impl EventSink for RootSink {
940 fn begin(&mut self, schema: Arc<Schema>, plan: WritePlan) -> LadduDataResult<()> {
941 match self.state {
942 SinkState::Idle => {}
943 SinkState::Writing => {
944 return Err(LadduDataError::Sink("ROOT sink already initialized".into()));
945 }
946 SinkState::Failed => {
947 return Err(LadduDataError::Sink(
948 "ROOT sink requires abort after failure".into(),
949 ));
950 }
951 }
952
953 let path = self.output.resolve(plan, "root")?;
954 OutputPath::create_parent_dirs(&path)?;
955
956 let columns = root_output_columns(
957 &schema,
958 self.options.schema_write.write_weight_column,
959 &self.options.schema_write,
960 );
961
962 let (senders, receivers) = root_channels(&columns, self.options.schema_write.precision);
963
964 let writer_path = path.clone();
965 let tree_name = self.options.tree_name.clone();
966
967 let handle = thread::spawn(move || write_root_tree(writer_path, tree_name, receivers));
968
969 self.resolved_path = Some(path);
970 self.event_schema = Some(schema);
971 self.senders = Some(senders);
972 self.writer_thread = Some(handle);
973 self.state = SinkState::Writing;
974
975 Ok(())
976 }
977
978 fn write_batch(&mut self, batch: &EventBatch) -> LadduDataResult<()> {
979 if !matches!(self.state, SinkState::Writing) {
980 return Err(LadduDataError::Sink(
981 match self.state {
982 SinkState::Idle => "ROOT sink not initialized",
983 SinkState::Failed => "ROOT sink requires abort after failure",
984 SinkState::Writing => unreachable!(),
985 }
986 .into(),
987 ));
988 }
989
990 let event_schema = self
991 .event_schema
992 .as_ref()
993 .ok_or_else(|| LadduDataError::Sink("ROOT sink not initialized".into()))?;
994
995 if event_schema.as_ref() != batch.schema().as_ref() {
996 return Err(LadduDataError::Sink(
997 "batch schema does not match ROOT sink schema".into(),
998 ));
999 }
1000
1001 let senders = self
1002 .senders
1003 .as_ref()
1004 .ok_or_else(|| LadduDataError::Sink("ROOT sink not initialized".into()))?;
1005
1006 let plan = PhysicalSchemaPlan::for_write(
1007 batch.schema(),
1008 &self.options.schema_write,
1009 self.options.schema_write.write_weight_column,
1010 );
1011
1012 for row in 0..batch.len() {
1013 for (index, column) in plan.columns().iter().enumerate() {
1014 let value = match column.role() {
1015 PhysicalColumnRole::P4 { index, component } => {
1016 batch.p4_at(index, row).components()[component]
1017 }
1018 PhysicalColumnRole::Scalar { index } => batch.scalar_at(index, row),
1019 PhysicalColumnRole::Weight => batch.weights_at(row),
1020 };
1021 if let Err(error) = senders.send(index, value) {
1022 self.state = SinkState::Failed;
1023 return Err(sink_error("send ROOT column", column.name(), error));
1024 }
1025 }
1026 }
1027
1028 Ok(())
1029 }
1030
1031 fn finish(&mut self) -> LadduDataResult<()> {
1032 if matches!(self.state, SinkState::Idle) {
1033 return Ok(());
1034 }
1035 if matches!(self.state, SinkState::Failed) {
1036 return Err(LadduDataError::Sink(
1037 "ROOT sink requires abort after failure".into(),
1038 ));
1039 }
1040
1041 self.senders.take();
1042
1043 if let Some(handle) = self.writer_thread.take() {
1044 match handle.join() {
1045 Ok(Ok(())) => {}
1046 Ok(Err(error)) => {
1047 self.state = SinkState::Failed;
1048 return Err(error);
1049 }
1050 Err(_) => {
1051 self.state = SinkState::Failed;
1052 return Err(LadduDataError::Sink("ROOT writer thread panicked".into()));
1053 }
1054 }
1055 }
1056
1057 self.event_schema = None;
1058 self.state = SinkState::Idle;
1059 Ok(())
1060 }
1061
1062 fn abort(&mut self) -> LadduDataResult<()> {
1063 self.senders.take();
1067 let result = if let Some(handle) = self.writer_thread.take() {
1068 match handle.join() {
1069 Ok(result) => result,
1070 Err(_) => Err(LadduDataError::Sink("ROOT writer thread panicked".into())),
1071 }
1072 } else {
1073 Ok(())
1074 };
1075
1076 self.event_schema = None;
1077 self.state = SinkState::Idle;
1078 result
1079 }
1080}
1081
1082impl Drop for RootSink {
1083 fn drop(&mut self) {
1084 let _ = self.abort();
1085 }
1086}
1087
1088enum RootColumnSenders {
1089 F64(Vec<Sender<f64>>),
1090 F32(Vec<Sender<f32>>),
1091}
1092
1093impl RootColumnSenders {
1094 fn send(&self, index: usize, value: f64) -> LadduDataResult<()> {
1095 match self {
1096 Self::F64(senders) => senders[index]
1097 .send(value)
1098 .map_err(|e| LadduDataError::Sink(e.to_string())),
1099 Self::F32(senders) => senders[index]
1100 .send(value as f32)
1101 .map_err(|e| LadduDataError::Sink(e.to_string())),
1102 }
1103 }
1104}
1105
1106enum RootColumnReceivers {
1107 F64(Vec<(Name, Receiver<f64>)>),
1108 F32(Vec<(Name, Receiver<f32>)>),
1109}
1110
1111fn root_channels(
1112 columns: &[Name],
1113 precision: Precision,
1114) -> (RootColumnSenders, RootColumnReceivers) {
1115 match precision {
1116 Precision::F64 => {
1117 let mut senders = Vec::with_capacity(columns.len());
1118 let mut receivers = Vec::with_capacity(columns.len());
1119
1120 for name in columns {
1121 let (tx, rx) = mpsc::channel();
1122 senders.push(tx);
1123 receivers.push((name.clone(), rx));
1124 }
1125
1126 (
1127 RootColumnSenders::F64(senders),
1128 RootColumnReceivers::F64(receivers),
1129 )
1130 }
1131 Precision::F32 => {
1132 let mut senders = Vec::with_capacity(columns.len());
1133 let mut receivers = Vec::with_capacity(columns.len());
1134
1135 for name in columns {
1136 let (tx, rx) = mpsc::channel();
1137 senders.push(tx);
1138 receivers.push((name.clone(), rx));
1139 }
1140
1141 (
1142 RootColumnSenders::F32(senders),
1143 RootColumnReceivers::F32(receivers),
1144 )
1145 }
1146 }
1147}
1148
1149fn write_root_tree(
1150 path: PathBuf,
1151 tree_name: Name,
1152 receivers: RootColumnReceivers,
1153) -> LadduDataResult<()> {
1154 let resource = format!("{}::{tree_name}", path.display());
1155 let mut file = RootFile::create(&path)
1156 .map_err(|error| sink_error("create ROOT file", path.display(), error))?;
1157 let mut tree = WriterTree::new(tree_name.as_ref());
1158
1159 match receivers {
1160 RootColumnReceivers::F64(receivers) => {
1161 for (name, rx) in receivers {
1162 tree.new_branch(name.as_ref(), rx.into_iter());
1163 }
1164 }
1165 RootColumnReceivers::F32(receivers) => {
1166 for (name, rx) in receivers {
1167 tree.new_branch(name.as_ref(), rx.into_iter());
1168 }
1169 }
1170 }
1171
1172 tree.write(&mut file)
1173 .map_err(|error| sink_error("write ROOT tree", &resource, error))?;
1174 file.close()
1175 .map_err(|error| sink_error("close ROOT file", &resource, error))?;
1176
1177 Ok(())
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use std::sync::atomic::{AtomicU64, Ordering};
1183
1184 use super::*;
1185 use crate::data::{Dataset, EventBatchBuilder};
1186
1187 fn temp_path(ext: &str) -> PathBuf {
1188 static NEXT_TEMP_FILE_ID: AtomicU64 = AtomicU64::new(0);
1189
1190 let nanos = std::time::SystemTime::now()
1191 .duration_since(std::time::UNIX_EPOCH)
1192 .unwrap()
1193 .as_nanos();
1194 let id = NEXT_TEMP_FILE_ID.fetch_add(1, Ordering::Relaxed);
1195
1196 std::env::temp_dir().join(format!(
1197 "laddu-root-test-{}-{nanos}-{id}.{ext}",
1198 std::process::id()
1199 ))
1200 }
1201
1202 fn v(x: f64) -> RealVec4 {
1203 RealVec4 {
1204 e: x + 0.3,
1205 px: x,
1206 py: x + 0.1,
1207 pz: x + 0.2,
1208 }
1209 }
1210
1211 fn schema() -> Arc<Schema> {
1212 Arc::new(Schema::new(["p"], ["mass"], true).unwrap())
1213 }
1214
1215 fn batch() -> EventBatch {
1216 let schema = schema();
1217 let mut builder = EventBatchBuilder::new(schema);
1218
1219 for i in 0..4 {
1220 builder
1221 .push_weighted([v(i as f64)], [100.0 + i as f64], 10.0 + i as f64)
1222 .unwrap();
1223 }
1224
1225 builder.finish().unwrap()
1226 }
1227
1228 #[test]
1229 fn root_sink_and_source_roundtrip_named_tree_with_f32_precision() {
1230 let path = temp_path("root");
1231 let batch = batch();
1232
1233 let mut sink = RootSink::builder(path.clone())
1234 .tree("events")
1235 .precision(Precision::F32)
1236 .build();
1237
1238 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
1239 .unwrap();
1240 sink.write_batch(&batch).unwrap();
1241 sink.finish().unwrap();
1242
1243 let tree_names = RootSource::tree_names(&path).unwrap();
1244 assert!(tree_names.iter().any(|name| name.as_ref() == "events"));
1245
1246 let columns = RootSource::columns(&path, Some("events")).unwrap();
1247 let names = columns
1248 .iter()
1249 .map(|col| col.name.to_string())
1250 .collect::<Vec<_>>();
1251
1252 for expected in ["p_e", "p_px", "p_py", "p_pz", "mass", "weight"] {
1253 assert!(
1254 names.iter().any(|name| name == expected),
1255 "missing {expected}"
1256 );
1257 }
1258
1259 let source = RootSource::builder(path.to_str().unwrap())
1260 .tree("events")
1261 .build()
1262 .unwrap();
1263
1264 assert_eq!(source.tree_name(), "events");
1265 assert_eq!(source.num_events().unwrap(), Some(4));
1266
1267 let read_batches: Vec<EventBatch> = source
1268 .batches(ReadPlan {
1269 chunk_size: Some(2),
1270 #[cfg(feature = "mpi")]
1271 distribution: Default::default(),
1272 })
1273 .unwrap()
1274 .map(Result::unwrap)
1275 .collect();
1276
1277 assert_eq!(
1278 read_batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
1279 vec![2, 2]
1280 );
1281
1282 let read = EventBatch::concat(&read_batches).unwrap();
1283
1284 assert_eq!(read.scalar_column(0), &[100.0, 101.0, 102.0, 103.0]);
1285 assert_eq!(read.weights_column().unwrap(), &[10.0, 11.0, 12.0, 13.0]);
1286 assert!((read.p4_at(0, 2).e - 2.3).abs() < 1.0e-6);
1287
1288 let _ = std::fs::remove_file(path);
1289 }
1290
1291 #[test]
1292 fn root_source_infers_first_tree_when_no_tree_is_named() {
1293 let path = temp_path("root");
1294 let batch = batch();
1295
1296 let mut sink = RootSink::builder(path.clone()).tree("first_tree").build();
1297
1298 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
1299 .unwrap();
1300 sink.write_batch(&batch).unwrap();
1301 sink.finish().unwrap();
1302
1303 let source = RootSource::builder(path.to_str().unwrap())
1304 .first_tree()
1305 .build()
1306 .unwrap();
1307
1308 assert_eq!(source.tree_name(), "first_tree");
1309
1310 let read = EventBatch::concat(
1311 &source
1312 .batches(ReadPlan::default())
1313 .unwrap()
1314 .map(Result::unwrap)
1315 .collect::<Vec<_>>(),
1316 )
1317 .unwrap();
1318
1319 assert_eq!(read.scalar_column(0), &[100.0, 101.0, 102.0, 103.0]);
1320
1321 let _ = std::fs::remove_file(path);
1322 }
1323
1324 #[test]
1325 fn negative_root_entry_count_error_includes_operation_and_resource() {
1326 let error =
1327 usize_from_i64(-1, "negative TTree entry count", "events.root::events").unwrap_err();
1328 let message = error.to_string();
1329 assert!(message.contains("read ROOT entry count `events.root::events`"));
1330 assert!(message.contains("negative TTree entry count"));
1331 }
1332
1333 #[test]
1334 fn root_source_named_missing_tree_fails() {
1335 let path = temp_path("root");
1336 let batch = batch();
1337
1338 let mut sink = RootSink::builder(path.clone()).tree("events").build();
1339
1340 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
1341 .unwrap();
1342 sink.write_batch(&batch).unwrap();
1343 sink.finish().unwrap();
1344
1345 let err = RootSource::builder(path.to_str().unwrap())
1346 .tree("missing")
1347 .build()
1348 .unwrap_err();
1349
1350 assert!(matches!(err, LadduDataError::Source(_)));
1351
1352 let _ = std::fs::remove_file(path);
1353 }
1354
1355 #[test]
1356 fn root_sink_rejects_batches_with_different_schema() {
1357 let path = temp_path("root");
1358 let batch = batch();
1359
1360 let mut sink = RootSink::builder(path.clone()).tree("events").build();
1361
1362 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
1363 .unwrap();
1364
1365 let other_schema = Arc::new(Schema::new(["q"], ["mass"], true).unwrap());
1366 let mut builder = EventBatchBuilder::new(other_schema);
1367 builder.push_weighted([v(1.0)], [1.0], 1.0).unwrap();
1368 let other = builder.finish().unwrap();
1369
1370 let err = sink.write_batch(&other).unwrap_err();
1371 assert!(matches!(err, LadduDataError::Sink(msg) if msg.contains("schema")));
1372
1373 sink.finish().unwrap();
1374 let _ = std::fs::remove_file(path);
1375 }
1376
1377 #[test]
1378 fn dataset_write_to_root_applies_dataset_transformations_before_writing() {
1379 let path = temp_path("root");
1380
1381 let dataset = Dataset::from_batch(batch()).filter(|ev| ev.scalar(0) >= 102.0);
1382
1383 let mut sink = RootSink::builder(path.clone()).tree("events").build();
1384
1385 dataset.write_to(&mut sink).unwrap();
1386
1387 let source = RootSource::builder(path.to_str().unwrap())
1388 .tree("events")
1389 .build()
1390 .unwrap();
1391
1392 let read = EventBatch::concat(
1393 &source
1394 .batches(ReadPlan::default())
1395 .unwrap()
1396 .map(Result::unwrap)
1397 .collect::<Vec<_>>(),
1398 )
1399 .unwrap();
1400
1401 assert_eq!(read.scalar_column(0), &[102.0, 103.0]);
1402 assert_eq!(read.weights_column().unwrap(), &[12.0, 13.0]);
1403
1404 let _ = std::fs::remove_file(path);
1405 }
1406
1407 #[test]
1408 fn root_batch_iter_disconnect_is_terminal_after_joining_reader() {
1409 let (tx, rx) = mpsc::sync_channel(1);
1410 drop(tx);
1411 let handle = thread::spawn(|| {});
1412 let mut iter = RootBatchIter {
1413 rx,
1414 state: RootBatchState::Receiving(handle),
1415 };
1416
1417 assert!(iter.next().is_none());
1418 assert!(iter.next().is_none());
1419 }
1420}