go-hep.org/x/hep@v0.38.1/cmd/root2csv/main.go (about)

     1  // Copyright ©2017 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // root2csv converts the content of a ROOT TTree to a CSV file.
     6  //
     7  //	Usage of root2csv:
     8  //	  -f string
     9  //	    	path to input ROOT file name
    10  //	  -o string
    11  //	    	path to output CSV file name (default "output.csv")
    12  //	  -t string
    13  //	    	name of the tree or graph to convert (default "tree")
    14  //
    15  // By default, root2csv will write out a CSV file with ';' as a column delimiter.
    16  // root2csv ignores the branches of the TTree that are not supported by CSV:
    17  //   - slices/arrays
    18  //   - C++ objects
    19  //
    20  // Example:
    21  //
    22  //	$> root2csv -o out.csv -t tree -f testdata/small-flat-tree.root
    23  //	$> head out.csv
    24  //	## Automatically generated from "testdata/small-flat-tree.root"
    25  //	Int32;Int64;UInt32;UInt64;Float32;Float64;Str;N
    26  //	0;0;0;0;0;0;evt-000;0
    27  //	1;1;1;1;1;1;evt-001;1
    28  //	2;2;2;2;2;2;evt-002;2
    29  //	3;3;3;3;3;3;evt-003;3
    30  //	4;4;4;4;4;4;evt-004;4
    31  //	5;5;5;5;5;5;evt-005;5
    32  //	6;6;6;6;6;6;evt-006;6
    33  //	7;7;7;7;7;7;evt-007;7
    34  package main
    35  
    36  import (
    37  	"flag"
    38  	"fmt"
    39  	"log"
    40  	"reflect"
    41  	"strings"
    42  
    43  	"go-hep.org/x/hep/csvutil"
    44  	"go-hep.org/x/hep/groot"
    45  	"go-hep.org/x/hep/groot/rhist"
    46  	"go-hep.org/x/hep/groot/riofs"
    47  	_ "go-hep.org/x/hep/groot/riofs/plugin/http"
    48  	_ "go-hep.org/x/hep/groot/riofs/plugin/xrootd"
    49  	"go-hep.org/x/hep/groot/rtree"
    50  )
    51  
    52  func main() {
    53  	log.SetPrefix("root2csv: ")
    54  	log.SetFlags(0)
    55  
    56  	fname := flag.String("f", "", "path to input ROOT file name")
    57  	oname := flag.String("o", "output.csv", "path to output CSV file name")
    58  	tname := flag.String("t", "tree", "name of the tree or graph to convert")
    59  
    60  	flag.Parse()
    61  
    62  	if *fname == "" {
    63  		flag.Usage()
    64  		log.Fatalf("missing input ROOT filename argument")
    65  	}
    66  
    67  	err := process(*oname, *fname, *tname)
    68  	if err != nil {
    69  		log.Fatal(err)
    70  	}
    71  }
    72  
    73  func process(oname, fname, tname string) error {
    74  
    75  	f, err := groot.Open(fname)
    76  	if err != nil {
    77  		return fmt.Errorf("could not open ROOT file: %w", err)
    78  	}
    79  	defer f.Close()
    80  
    81  	obj, err := riofs.Dir(f).Get(tname)
    82  	if err != nil {
    83  		return fmt.Errorf("could not get ROOT object: %w", err)
    84  	}
    85  
    86  	switch obj := obj.(type) {
    87  	case rtree.Tree:
    88  		return processTree(oname, fname, obj)
    89  	case rhist.GraphErrors: // Note: test rhist.GraphErrors before rhist.Graph
    90  		return processGraphErrors(oname, fname, obj)
    91  	case rhist.Graph:
    92  		return processGraph(oname, fname, obj)
    93  	default:
    94  		return fmt.Errorf("object %q in file %q is not a rtree.Tree nor a rhist.Graph", tname, fname)
    95  	}
    96  }
    97  
    98  func processTree(oname, fname string, tree rtree.Tree) error {
    99  	var nt = ntuple{n: tree.Entries()}
   100  	log.Printf("scanning leaves...")
   101  	for _, leaf := range tree.Leaves() {
   102  		kind := leaf.Type().Kind()
   103  		switch kind {
   104  		case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
   105  			log.Printf(">>> %q %v not supported (%v)", leaf.Name(), leaf.Class(), kind)
   106  			continue
   107  		case reflect.String:
   108  			// ok
   109  		case reflect.Bool,
   110  			reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   111  			reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   112  			reflect.Float32, reflect.Float64:
   113  			if leaf.LeafCount() != nil {
   114  				log.Printf(">>> %q %v not supported (slice)", leaf.Name(), leaf.Class())
   115  				continue
   116  			}
   117  			if leaf.Len() > 1 {
   118  				log.Printf(">>> %q %v not supported (array)", leaf.Name(), leaf.Class())
   119  				continue
   120  			}
   121  		default:
   122  			log.Printf(">>> %q %v not supported (%v) (unknown!)", leaf.Name(), leaf.Class(), kind)
   123  			continue
   124  		}
   125  
   126  		nt.add(leaf.Name(), leaf)
   127  	}
   128  	log.Printf("scanning leaves... [done]")
   129  
   130  	r, err := rtree.NewReader(tree, nt.args)
   131  	if err != nil {
   132  		return fmt.Errorf("could not create tree reader: %w", err)
   133  	}
   134  	defer r.Close()
   135  
   136  	nrows := 0
   137  	err = r.Read(func(ctx rtree.RCtx) error {
   138  		nt.fill()
   139  		nrows++
   140  		return nil
   141  	})
   142  	if err != nil {
   143  		return fmt.Errorf("could not read tree: %w", err)
   144  	}
   145  
   146  	tbl, err := csvutil.Create(oname)
   147  	if err != nil {
   148  		return fmt.Errorf("could not create output CSV file: %w", err)
   149  	}
   150  	defer tbl.Close()
   151  	tbl.Writer.Comma = ';'
   152  
   153  	names := make([]string, len(nt.cols))
   154  	for i, col := range nt.cols {
   155  		names[i] = col.name
   156  	}
   157  	err = tbl.WriteHeader(fmt.Sprintf(
   158  		"## Automatically generated from %q\n%s\n",
   159  		fname,
   160  		strings.Join(names, string(tbl.Writer.Comma)),
   161  	))
   162  	if err != nil {
   163  		return fmt.Errorf("could not write CSV header: %w", err)
   164  	}
   165  
   166  	row := make([]any, len(nt.cols))
   167  	for irow := range nrows {
   168  		for i, col := range nt.cols {
   169  			row[i] = col.slice.Index(irow).Interface()
   170  		}
   171  		err = tbl.WriteRow(row...)
   172  		if err != nil {
   173  			return fmt.Errorf("could not write row %d to CSV file: %w", irow, err)
   174  		}
   175  	}
   176  
   177  	err = tbl.Close()
   178  	if err != nil {
   179  		return fmt.Errorf("could not close CSV output file: %w", err)
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  type ntuple struct {
   186  	n    int64
   187  	cols []column
   188  	args []rtree.ReadVar
   189  	vars []any
   190  }
   191  
   192  func (nt *ntuple) add(name string, leaf rtree.Leaf) {
   193  	n := len(nt.cols)
   194  	nt.cols = append(nt.cols, newColumn(name, leaf, nt.n))
   195  	col := &nt.cols[n]
   196  	nt.args = append(nt.args, rtree.ReadVar{
   197  		Name:  name,
   198  		Leaf:  leaf.Name(),
   199  		Value: col.data.Addr().Interface(),
   200  	})
   201  	nt.vars = append(nt.vars, col.data.Addr().Interface())
   202  }
   203  
   204  func (nt *ntuple) fill() {
   205  	for i := range nt.cols {
   206  		col := &nt.cols[i]
   207  		col.fill()
   208  	}
   209  }
   210  
   211  type column struct {
   212  	name  string
   213  	i     int64
   214  	leaf  rtree.Leaf
   215  	etype reflect.Type
   216  	shape []int
   217  	data  reflect.Value
   218  	slice reflect.Value
   219  }
   220  
   221  func newColumn(name string, leaf rtree.Leaf, n int64) column {
   222  	etype := leaf.Type()
   223  	shape := []int{int(n)}
   224  	if leaf.Len() > 1 && leaf.Kind() != reflect.String {
   225  		etype = reflect.ArrayOf(leaf.Len(), etype)
   226  		shape = append(shape, leaf.Len())
   227  	}
   228  	rtype := reflect.SliceOf(etype)
   229  	return column{
   230  		name:  name,
   231  		i:     0,
   232  		leaf:  leaf,
   233  		etype: etype,
   234  		shape: shape,
   235  		data:  reflect.New(etype).Elem(),
   236  		slice: reflect.MakeSlice(rtype, int(n), int(n)),
   237  	}
   238  }
   239  
   240  func (col *column) fill() {
   241  	col.slice.Index(int(col.i)).Set(col.data)
   242  	col.i++
   243  }
   244  
   245  func processGraph(oname, fname string, g rhist.Graph) error {
   246  	names := []string{"x", "y"}
   247  
   248  	tbl, err := csvutil.Create(oname)
   249  	if err != nil {
   250  		return fmt.Errorf("could not create output CSV file: %w", err)
   251  	}
   252  	defer tbl.Close()
   253  	tbl.Writer.Comma = ';'
   254  
   255  	err = tbl.WriteHeader(fmt.Sprintf(
   256  		"## Automatically generated from %q\n%s\n",
   257  		fname,
   258  		strings.Join(names, string(tbl.Writer.Comma)),
   259  	))
   260  	if err != nil {
   261  		return fmt.Errorf("could not write CSV header: %w", err)
   262  	}
   263  
   264  	n := g.Len()
   265  	for i := range n {
   266  		var (
   267  			x, y = g.XY(i)
   268  		)
   269  		err = tbl.WriteRow(x, y)
   270  		if err != nil {
   271  			return fmt.Errorf("could not write row %d to CSV file: %w", i, err)
   272  		}
   273  	}
   274  
   275  	err = tbl.Close()
   276  	if err != nil {
   277  		return fmt.Errorf("could not close CSV output file: %w", err)
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  func processGraphErrors(oname, fname string, g rhist.GraphErrors) error {
   284  	names := []string{"x", "y", "ex-lo", "ex-hi", "ey-lo", "ey-hi"}
   285  
   286  	tbl, err := csvutil.Create(oname)
   287  	if err != nil {
   288  		return fmt.Errorf("could not create output CSV file: %w", err)
   289  	}
   290  	defer tbl.Close()
   291  	tbl.Writer.Comma = ';'
   292  
   293  	err = tbl.WriteHeader(fmt.Sprintf(
   294  		"## Automatically generated from %q\n%s\n",
   295  		fname,
   296  		strings.Join(names, string(tbl.Writer.Comma)),
   297  	))
   298  	if err != nil {
   299  		return fmt.Errorf("could not write CSV header: %w", err)
   300  	}
   301  
   302  	n := g.Len()
   303  	for i := range n {
   304  		var (
   305  			x, y     = g.XY(i)
   306  			xlo, xhi = g.XError(i)
   307  			ylo, yhi = g.YError(i)
   308  		)
   309  		err = tbl.WriteRow(x, y, xlo, xhi, ylo, yhi)
   310  		if err != nil {
   311  			return fmt.Errorf("could not write row %d to CSV file: %w", i, err)
   312  		}
   313  	}
   314  
   315  	err = tbl.Close()
   316  	if err != nil {
   317  		return fmt.Errorf("could not close CSV output file: %w", err)
   318  	}
   319  
   320  	return nil
   321  }