Python bindings to oxyroot. Makes reading .root files blazing fast ๐Ÿš€

First attempt at a reader with few supported types

+4 -2
Cargo.toml
··· 5 5 6 6 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 7 [lib] 8 - name = "py_oxyroot" 8 + name = "oxyroot" 9 9 crate-type = ["cdylib"] 10 10 11 11 [dependencies] 12 - pyo3 = "0.25.0" 12 + numpy = "0.26.0" 13 + oxyroot = "0.1.25" 14 + pyo3 = { version = "0.26.0", features = ["abi3-py38"] }
+3 -1
pyproject.toml
··· 3 3 build-backend = "maturin" 4 4 5 5 [project] 6 - name = "py-oxyroot" 6 + name = "oxyroot" 7 7 requires-python = ">=3.8" 8 8 classifiers = [ 9 9 "Programming Language :: Rust", ··· 14 14 [project.optional-dependencies] 15 15 tests = [ 16 16 "pytest", 17 + "numpy", 18 + "uproot", 17 19 ] 18 20 [tool.maturin] 19 21 python-source = "python"
+7
python/oxyroot/__init__.py
··· 1 + from .oxyroot import * 2 + 3 + __doc__: str = oxyroot.__doc__ 4 + __version__: str = oxyroot.version() 5 + 6 + if hasattr(oxyroot, "__all__"): 7 + __all__ = oxyroot.__all__
-6
python/py_oxyroot/__init__.py
··· 1 - from .py_oxyroot import * 2 - 3 - 4 - __doc__ = py_oxyroot.__doc__ 5 - if hasattr(py_oxyroot, "__all__"): 6 - __all__ = py_oxyroot.__all__
+35
python/tests/read_ntuples.py
··· 1 + import oxyroot 2 + 3 + if __name__ == '__main__': 4 + import uproot 5 + import numpy as np 6 + import time 7 + 8 + file_name = "ntuples.root" 9 + tree_name = 'mu_mc' 10 + 11 + up_start_time = time.time() 12 + up_tree = uproot.open(file_name)[tree_name] 13 + for branch in up_tree.keys(): 14 + # print(branch, up_tree[branch].typename) 15 + if up_tree[branch].typename != "std::string": 16 + up_values = up_tree[branch].array(library="np") 17 + print(f"Uproot read {branch} into a {type(up_values)} and it has a mean of {np.nanmean(up_values):.2f}") 18 + up_end_time = time.time() 19 + 20 + print("\n") 21 + 22 + oxy_start_time = time.time() 23 + oxy_branches = oxyroot.read_root(file_name, tree_name=tree_name) 24 + for branch in oxy_branches: 25 + oxyroot.read_root(file_name, tree_name=tree_name, branch=branch) 26 + oxy_values = oxyroot.read_root(file_name, tree_name=tree_name, branch=branch) 27 + if type(oxy_values) is np.ndarray: 28 + print(f"Oxyroot read {branch} into a {type(oxy_values)} and it has a mean of {np.nanmean(oxy_values):.2f}") 29 + else: 30 + print(f"Oxyroot read {branch} into a {type(oxy_values)} and it has a length of {len(oxy_values)}") 31 + oxy_end_time = time.time() 32 + 33 + print("\n Total time") 34 + print(f"Uproot took: {up_end_time - up_start_time:.3}s") 35 + print(f"Oxyroot took: {oxy_end_time - oxy_start_time:.3}s")
-6
python/tests/test_all.py
··· 1 - import pytest 2 - import py_oxyroot 3 - 4 - 5 - def test_sum_as_string(): 6 - assert py_oxyroot.sum_as_string(1, 1) == "2"
+22
python/tests/test_read_from_uproot.py
··· 1 + import pytest 2 + import oxyroot 3 + import uproot 4 + import numpy as np 5 + import os 6 + 7 + def test_read_from_uproot(): 8 + # Create a dummy ROOT file for testing 9 + 10 + input = np.array([4.1, 5.2, 6.3]) 11 + file_name = "test.root" 12 + 13 + with uproot.recreate(file_name) as f: 14 + f.mktree("tree1", {"branch1": np.float64}) 15 + f["tree1"].extend({"branch1": input}) 16 + 17 + 18 + output = oxyroot.read_root(file_name, tree_name="tree1", branch="branch1") 19 + assert(type(output) is np.ndarray) 20 + assert(np.array_equal(input, output)) 21 + 22 + os.remove(file_name)
+96 -7
src/lib.rs
··· 1 - use pyo3::prelude::*; 1 + use ::oxyroot::{Named, RootFile}; 2 + use numpy::ToPyArray; 3 + use pyo3::{exceptions::PyValueError, prelude::*, types::PyModule, IntoPyObjectExt}; 4 + 5 + #[pyfunction] 6 + fn version() -> PyResult<String> { 7 + Ok(env!("CARGO_PKG_VERSION").to_string()) 8 + } 2 9 3 - /// Formats the sum of two numbers as string. 10 + /// Read a ROOT file and return the list of trees, the branches of a tree, or the values of a branch. 4 11 #[pyfunction] 5 - fn sum_as_string(a: usize, b: usize) -> PyResult<String> { 6 - Ok((a + b).to_string()) 12 + #[pyo3(signature = (path, tree_name = None, branch = None))] 13 + fn read_root( 14 + path: String, 15 + tree_name: Option<String>, 16 + branch: Option<String>, 17 + ) -> PyResult<Py<PyAny>> { 18 + let mut file = RootFile::open(&path).unwrap(); 19 + let keys: Vec<String> = file 20 + .keys() 21 + .into_iter() 22 + .map(|k| k.name().to_string()) 23 + .collect(); 24 + 25 + Python::attach(|py| -> PyResult<Py<PyAny>> { 26 + match tree_name { 27 + Some(name) => { 28 + if let Ok(tree) = file.get_tree(&name) { 29 + let branches_available: Vec<String> = 30 + tree.branches().map(|b| b.name().to_string()).collect(); 31 + 32 + match branch { 33 + Some(bs) => { 34 + if let Some(branch) = tree.branch(&bs) { 35 + match branch.item_type_name().as_str() { 36 + "f32" => { 37 + let data = 38 + branch.as_iter::<f32>().unwrap().collect::<Vec<_>>(); 39 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 40 + } 41 + "double" => { 42 + let data = 43 + branch.as_iter::<f64>().unwrap().collect::<Vec<_>>(); 44 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 45 + } 46 + "int32_t" => { 47 + let data = 48 + branch.as_iter::<i32>().unwrap().collect::<Vec<_>>(); 49 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 50 + } 51 + "int64_t" => { 52 + let data = 53 + branch.as_iter::<i64>().unwrap().collect::<Vec<_>>(); 54 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 55 + } 56 + "uint32_t" => { 57 + let data = 58 + branch.as_iter::<u32>().unwrap().collect::<Vec<_>>(); 59 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 60 + } 61 + "uint64_t" => { 62 + let data = 63 + branch.as_iter::<u64>().unwrap().collect::<Vec<_>>(); 64 + Ok(data.to_pyarray(py).into_py_any(py).unwrap()) 65 + } 66 + "string" => { 67 + let data = 68 + branch.as_iter::<String>().unwrap().collect::<Vec<_>>(); 69 + Ok(data.into_py_any(py).unwrap()) 70 + } 71 + other => Err(PyValueError::new_err(format!( 72 + "Unsupported branch type: {}", 73 + other 74 + ))), 75 + } 76 + } else { 77 + Err(PyValueError::new_err(format!( 78 + "Branch '{}' not found. Available branches are: {:?}", 79 + bs, branches_available 80 + ))) 81 + } 82 + } 83 + None => Ok(branches_available.into_py_any(py).unwrap()), 84 + } 85 + } else { 86 + Err(PyValueError::new_err(format!( 87 + "Tree '{}' not found. Available trees are: {:?}", 88 + name, keys 89 + ))) 90 + } 91 + } 92 + None => Ok(keys.into_py_any(py).unwrap()), 93 + } 94 + }) 7 95 } 8 96 9 - /// A Python module implemented in Rust. 97 + /// A Python module to read root files implemented in Rust. 10 98 #[pymodule] 11 - fn py_oxyroot(m: &Bound<'_, PyModule>) -> PyResult<()> { 12 - m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; 99 + fn oxyroot(m: &Bound<'_, PyModule>) -> PyResult<()> { 100 + m.add_function(wrap_pyfunction!(version, m)?)?; 101 + m.add_function(wrap_pyfunction!(read_root, m)?)?; 13 102 Ok(()) 14 103 }