Skip to content

mathieuouillon/oxiroot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

236 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

oxiroot

License: MIT Rust 1.95+ No libROOT

Pure-Rust IO for the CERN ROOT file format — read and write RNTuple, TTree, the classic histogram family, and graphs in the ROOT (TFile) container, with no C++/libROOT or Python dependency. Files written by oxiroot open in official ROOT and uproot, and oxiroot reads files they write.

The name is ROOT + oxide — Rust is oxidized iron.

Highlights

  • 🦀 Pure Rust — the on-disk format reimplemented from the official specs. No libROOT, no Python; builds and runs anywhere Rust does, depending only on a handful of small pure-Rust crates (compression codecs and a hasher).
  • 🔄 Two-way interop — every reader and writer is validated against both official ROOT (C++) and uproot, in both directions.
  • 🔍 Inspect any classRFile::get_value decodes any object from its TStreamerInfo into a dynamic Value tree (rootls / rootprint-style), even classes with no typed model — and oxroot dump prints it. Undecodable members degrade to Unsupported, never a crash.
  • 📊 Histograms & profilesTH1/TH2/TH3 (every precision, uniform or irregular bins), TProfile/TProfile2D/TProfile3D, TEfficiency, N-dimensional THnSparse, and polygon-binned TH2Poly — all read and write.
  • 🎲 Sampling & smoothing — draw from a histogram's or function's distribution (get_random/fill_random, ROOT's GetRandom/FillRandom) and smooth with ROOT's 353QH (smooth), via a small seedable built-in Rng (no rand dependency).
  • 📈 GraphsTGraph, TGraphErrors, TGraphAsymmErrors, plus TGraph2D and TGraphMultiErrors — read and write, including a graph's display frame (fHistogram) and attached fitted functions (fFunctions, faithful TF1).
  • 🧮 Functions — standalone TF1/TF2/TF3 keys backed by a real TFormula expression engine (arbitrary formulas, ROOT gaus/expo/pol shortcuts), with pure-Rust eval/integral/derivative; any formula is also fittable via Model::from_formula.
  • 📐 Statistics (oxiroot::stat) — a dependency-free, scipy.stats-verified core: special functions (erf, gammainc, betainc, ndtri), the Normal/StudentT/ChiSquared/FisherF distributions (pdf/cdf/sf/ppf) plus Poisson/Binomial, descriptive stats (describe, skew, kurtosis, sem, iqr, median_abs_deviation, entropy, …), pearsonr/spearmanr, and tests (chisquare, ks_1samp/ks_2samp, ttest_ind, normaltest, mannwhitneyu/wilcoxon) plus a seeded bootstrap_ci. A physics module adds HEP conventions: significance ↔ p-value ("nσ"), inverse-variance measurement combination, Clopper–Pearson / Garwood / Wilson / Agresti–Coull efficiency intervals, and the Feldman–Cousins unified interval. A lineshapes module has the HEP fit shapes — Crystal Ball (and double-sided), Breit–Wigner / relativistic BW, the Voigt profile, Novosibirsk, ARGUS, bifurcated Gaussian, Moyal, and Landau — each also a ready-to-fit Model (Model::crystal_ball, voigtian, …).
  • 🌳 TTree — read and write scalar, fixed/variable-length array, string, std::vector<T>, and split std::vector<MyStruct> branches; read nested structs, std::vector<std::vector<T>>, TClonesArray, split single objects, old unsplit object branches (TBranchObject), std::set/std::map branches, TNtuple/TNtupleD, friend trees (AddFriend, read entry-aligned), tree aliases (SetAlias), and TEntryList selections; multi-basket via a bounded-memory streaming writer.
  • 🧱 RNTuple — read and write ROOT's columnar format (scalars, strings, vectors, nested vectors and vectors of records, fixed-size std::array/std::bitset, user classes, std::set/std::map, the nullable std::optional/std::unique_ptr and std::atomic), read unsplit streamer fields, compressed, multi-cluster via a streaming writer, several RNTuples per file / inside a TDirectory, and schema late extensions (read and write fields added via the footer's schema-extension record). A bounded prefix read (RNTuple::read_field_prefix(name, n)) decodes only the clusters covering the first n entries, so previewing a huge file (oxroot dump -n 10) never touches the whole field.
  • 🗜 Compression — decode and encode Zstd / zlib / LZ4 / LZMA — all pure Rust, all read back by ROOT and uproot.
  • 🌐 Remote reads — open a file over HTTP(S) (http feature) or CERN's XRootD root:// (xrootd feature) with RFile::open_url and read only the byte ranges each object touches, never downloading the file whole — the way ROOT and uproot read remote data. Verified against real public data on root://eospublic.cern.ch. A large local file reads the same lazy way with RFile::open_ranged.
  • 🧵 Multithreaded fillThreadedHist, the pure-std analog of ROOT's TThreadedObject<TH1>; optional one-call rayon parallel fill.
  • hadd — a pure-Rust file merger: histograms summed, TTree / RNTuple entries concatenated, other objects copied — verified against ROOT's own hadd (oxiroot::hadd::merge_files).
  • 🔎 Command-line inspectoroxroot ls / show / dump / stat looks into any ROOT file (keys, TTree/RNTuple structure, entries, histogram bins) from the shell — no ROOT, no Python (the oxiroot-cli crate).
  • 🎨 Plotting (optional) — render TH1/TH2/TGraph/TProfile to SVG, PNG, and PDF with a matplotlib-like API and an mplhep histogram style — grids, ratio plots, LaTeX ($…$) math labels — all pure Rust, no matplotlib, no system fonts.
  • 🛡 Robust by construction — readers never panic on malformed input (fuzz-tested); writers cross the 2 GiB mark by switching to ROOT's 64-bit container form (never silently truncating a 32-bit seek pointer).

Quick start

Not yet on crates.io — depend on it via git. Pull in everything through the facade, or just the part you need: the histogram, tree, and RNTuple crates are independent, so a histogram-only project never compiles the others.

[dependencies]
# Everything — histograms, graphs, TTree, RNTuple, fitting, plotting — through
# the facade. The optional capabilities (fit, plot, rayon, mmap, argmin) are ON
# BY DEFAULT, so nothing extra to enable:
oxiroot = { git = "https://github.com/mathieuouillon/oxiroot" }

# …leaner, just the format core (drops the fitting/plotting/rayon/mmap deps):
# oxiroot = { git = "https://github.com/mathieuouillon/oxiroot", default-features = false }

# …or depend on just one crate from the same repo:
oxiroot-hist    = { git = "https://github.com/mathieuouillon/oxiroot" }  # histograms + graphs
oxiroot-tree    = { git = "https://github.com/mathieuouillon/oxiroot" }  # TTree
oxiroot-rntuple = { git = "https://github.com/mathieuouillon/oxiroot" }  # RNTuple
use oxiroot::prelude::*;

// Fill and save one histogram — the Hist builder + WriteRoot / ReadRoot traits.
let mut h = Hist::reg(50, 0.0, 100.0).name("pt").title("p_{T}").weight();
h.fill_weight(42.0, 1.5);
h.write_root("hist.root", Compression::Zstd(5))?;            // any single writable object
let same = TH1::read_root(&RFile::open("hist.root")?, "pt")?; // any readable object

// Several objects, subdirectories, or appending — the RootFile builder.
let prof = Hist::reg(5, 0.0, 5.0).name("prof").title("<pt> per region").profile();
RootFile::create("out.root")
    .add(&h)                              // any &dyn WriteRoot: hist, profile, graph…
    .dir("by_region", |d| d.add(&prof))   // a TDirectory
    .write(Compression::Zstd(5))?;
let g = RFile::open("out.root")?;
let p = TProfile::read_root_in(&g, "by_region", "prof")?;   // read from a subdirectory

// Write a TTree, then read a branch back.
let branches = vec![
    Branch::i32("n", vec![1, 2, 3]),
    Branch::f64("pt", vec![10.5, 20.1, 33.7]),
];
Tree::new("Events", branches).write_root("tree.root", Compression::None)?;
let f = RFile::open("tree.root")?;
let t = TTree::open(&f, "Events")?;
let BranchValues::F64(pt) = t.read_branch(&f, "pt")? else { panic!() };

// Write a columnar RNTuple, then read it back.
let fields = vec![Field::f64("mass", vec![91.2, 125.0])];
Ntuple::new("events", fields).write_root("data.root", Compression::None)?;
let n = RNTuple::open(&RFile::open("data.root")?, "events")?.num_entries();

The analysis example is an end-to-end mini analysis — weighted/variable-bin histograms → scale/merge/normalize → per-region subdirectories → a columnar event dataset → read-back:

cargo run -p oxiroot --example analysis

Features

Histograms & profiles (oxiroot::hist)

  • Read & write TH1/TH2/TH3 in every precision (D/F/I/S/C/L), TProfile/TProfile2D/TProfile3D, TEfficiency, N-dimensional THnSparse, and polygon-binned TH2Poly (arbitrary-shape bins, with a builder API).
  • One way per operation, not a function per type. Write one object with h.write_root(path, compression)? and read one with TH1::read_root(&file, name)? (the WriteRoot/ReadRoot traits; also h.to_root_bytes() and TH1::read_root_in(&file, dir, name)? for a subdirectory). A TH1/TH2/TH3's on-disk precision is a typed Precision chosen by the builder's storage finalizer (.float() writes a TH1F; see below), or changed on a histogram you already built or read with .with_precision(Precision::Float); h.class_name() reconstructs the ROOT class. Profiles carry a typed ErrorMode.
  • No forced names, no global registry. A histogram is just data: construct it with the Hist builder (Hist::reg(nbins, lo, hi).double()) and name it only when you persist it — .name("pt") sets the file key (.title(...) the title). Unlike ROOT there is no gROOT/gDirectory, so any number of same-named histograms coexist in memory; and writing two objects under the same key name in one directory is a loud DuplicateName error, never ROOT's silent shadow-on-read.
  • Then fill/fill_weight with ROOT's exact Fill semantics; sumw2() (chains: h.sumw2().fill(x)) enables weighted per-bin errors (bin_error) on a histogram not already built with .weight().
  • The one way to build a histogram is the scikit-hep hist-style Hist builder, mapped onto ROOT so the result is an ordinary TH1/TH2/TH3. Hist::reg(50, 0.0, 100.0).name("pt").label("$p_T$ [GeV]").weight() chains a regular (reg) axis; for irregular bin edges give them explicitly with varHist::var(&[0.0, 1.0, 2.0, 5.0, 10.0, 100.0]).double() (a real ROOT variable-binned TH1D). Either kind chains a storage finalizer picks the ROOT class — double()TH1D, float()TH1F, int64()TH1L, int32()TH1I, int16()TH1S, int8()TH1C, weight()TH1D+Sumw2, profile()TProfile. Chain .reg/.var again for TH2/TH3 (and .profile() on those for TProfile2D/TProfile3D). Axis labels (label() / with_x_label() / x_label()) live in ROOT's fXaxis.fTitle and round-trip. The hist accessor family is there too: values(), variances() (Sumw2), errors() (√variance), counts() (effective entries), density(), and at(x) (content at a coordinate); plus batch fill_many(), UHI-style integral_range(a, b) / slice(a, b), and a Hist::reg(...).profile() finalizer for a TProfile (hist's Mean storage).
  • Arithmetic with Sumw2 error propagation: scale (also h *= c / h * c), add (the bin-by-bin merge used to combine job outputs), multiply, divide, integral. Bins read by cell index (h[bin]) or iterator (for &c in &h); a shared Histogram trait (contents/entries/sum) abstracts over TH1/2/3, and every type implements Display for a one-line summary.
  • Statistics & shape accessors: mean/std_dev, maximum/minimum/ maximum_bin, find_bin, bin_center/bin_width/bin_low_edge, effective_entries, reset, interpolate, quantiles; derived histograms rebin/rebin2d/rebin3d, cumulative, projections (TH2TH1; TH3TH1/TH2), and profile_x/profile_y — all carrying the statistical moment sums so the results' mean/std_dev stay correct.
  • Sampling & smoothing: get_random / fill_random draw from a histogram's (or, via fill_random_fn / TF1::get_random, a function's) distribution (inverse-CDF, ROOT's GetRandom/FillRandom); smooth is ROOT's 353QH smoother. A small seedable Rng means no rand dependency and reproducible draws.
  • Compatibility tests: chi2_test/chi2_test_with (Pearson χ², all three UU/UW/WW weighting schemes) and kolmogorov_test, returning ROOT-matched p-values. Alphanumeric (labelled) axes round-trip through TAxis::labels (read and write, with set_label).
  • Fitting (the fit feature, on by default) — fit a parametric model to any 1-D data: a histogram, a TGraph, or your own (x, y, σ) points. The standalone oxiroot::fit crate provides the Model (built-in gaussian/exponential/polynomial, the HEP peaks crystal_ball/voigtian/…, an arbitrary formula, or a custom closure) and a pure-Rust Minuit2 minimizer; anything implementing the FitData trait gets .fit(&model) (Neyman/Pearson χ² or binned Poisson likelihood), returning parameters, parabolic + MINOS errors, covariance, and chi2/ndf. A robust Loss (Huber/SoftL1/Cauchy/Arctan — scipy's least_squares loss) down-weights outliers.
  • Multithreaded fillThreadedHist, the pure-Rust analog of ROOT's TThreadedObject<TH1>: share &hist, call hist.fill(x) from any thread — each thread transparently gets its own copy — then hist.merge() combines them exactly (contents + Sumw2 + every moment sum), identical to a serial fill:
    let hist = ThreadedHist::new(Hist::reg(100, 0.0, 100.0).double().named("h"));
    std::thread::scope(|s| for chunk in data.chunks(n) {
        let hist = &hist;
        s.spawn(move || for &x in chunk { hist.fill(x); });
    });
    let merged = hist.merge()?;
    No Arc, no manual slots; with_local(|h| …) batches fills or reaches any method, and the rayon feature (on by default) adds a one-call fill_par(&template, &data, |h, &x| h.fill(x)). See examples/threaded.rs.
  • Write one object with h.write_root(path, compression). For several objects, subdirectories, or appending, use the RootFile builder — one entry point for all file composition:
    RootFile::create("out.root")
        .add(&h)                            // any &dyn WriteRoot: hist, profile, graph…
        .add(&prof)
        .dir("by_region", |d| d.add(&sig))  // a TDirectory per region
        .write(Compression::Zstd(5))?;
    RootFile::open("out.root")?.add(&extra).write(Compression::None)?; // append
    Written files embed a TStreamerInfo list, so they are self-describing for any ROOT reader.

Fitting (oxiroot::fit, fit feature)

Fitting lives in its own crate (oxiroot-fit), so it works on any 1-D data, not just histograms. A dataset implements the FitData trait (yielding (x, y, σ) points) and the blanket FitExt gives it .fit(&model). TH1 and TGraph implement FitData out of the box, and Points (or your own FitData impl) covers everything else. fit is χ² by default; fit_opts picks the cost (Neyman or Pearson chi-square, or a binned Poisson likelihood), a fit range, a robust loss, and opt-in MINOS errors. A Model is a built-in shape (gaussian, exponential, polynomial), any formula string (Model::from_formula), or a closure f(x, params), with per-parameter limits, fixing, and a data-driven seed. The fit returns the parameters, parabolic errors, optional asymmetric MINOS errors and covariance matrix, and chi2/ndf (with chi2_per_ndf() and a goodness-of-fit p_value()). The χ² survival function it shares with the comparison tests lives in the dependency-free oxiroot-stat crate.

Two minimizer backends are selectable via FitOptions::minimizer(...): the default Minimizer::Minuit2 (the pure-Rust Minuit2 MIGRAD — ROOT's algorithm, with parabolic + MINOS errors and the covariance), and, behind the argmin feature, Minimizer::NelderMead — a gradient-free simplex from the argmin crate (parameter errors from a numerical Hessian; no MINOS), a handy independent cross-check.

let fit = data.fit_opts(&model, &FitOptions::new().minimizer(Minimizer::NelderMead));
use oxiroot::prelude::*; // the `fit` feature is on by default

let mut h = Hist::reg(60, 80.0, 100.0).double().named("mass").titled("di-muon mass");
h.sumw2();
// … fill h with events …

// Gaussian peak fit (chi-square). `estimate_from` seeds (constant, mean, sigma)
// from the bins — no manual moment loop, and it works for set_bin_content too.
let model = Model::gaussian("z").estimate_from(&h);
let fit = h.fit(&model);
println!("mean = {:.3} ± {:.3}", fit.params[1], fit.errors[1]);
println!("chi2/ndf = {:.2}, p = {:.3}", fit.chi2_per_ndf(), fit.p_value());

// binned Poisson likelihood, via FitOptions:
let ml = h.fit_opts(&model, &FitOptions::new().method(FitMethod::Likelihood));

// Full control: fit the core ±window, keep sigma positive, and ask for MINOS.
let opts = FitOptions::new().range(85.0, 97.0).with_minos(true);
let mut peak = Model::gaussian("z").estimate_from(&h).lower_limit("sigma", 0.0);
let r = h.fit_into(&mut peak, &opts); // writes the fit back into `peak`
if let Some(minos) = &r.minos {
    println!("mean +{:.3} {:.3}", minos[1].1, minos[1].0); // asymmetric errors
}

// A custom model — Gaussian signal on a flat background — as a closure.
let sig_bkg = Model::new(
    "s+b", &["norm", "mean", "sigma", "bkg"], vec![h.maximum(), 91.0, 2.0, 0.0],
    |x, q| q[0] * (-0.5 * ((x - q[1]) / q[2]).powi(2)).exp() + q[3],
);
let r = h.fit(&sig_bkg);

// The SAME `Model` + `.fit()` works on a TGraph …
let graph = TGraph::with_errors(x.clone(), y.clone(), ex, ey).named("g");
let line = graph.fit(&Model::polynomial("line", 1).with_params(vec![0.0, 0.0]));

// … and on raw points (a `Vec`/slice of `Point`, or your own `FitData` impl).
let data = Points::new(&x, &y, &sigma);
let peak = data.fit(&Model::gaussian("g").estimate_from(&data));

A runnable worked example (fits a Z → μμ peak, then the same models to a TGraph and to raw points):

cargo run -p oxiroot --example fit

Graphs (oxiroot::hist)

A single TGraph type covers all three ROOT classes, selected by its errors field: plain (TGraph), symmetric (TGraphErrors), or asymmetric (TGraphAsymmErrors); the class is detected on read by TGraph::read_root. TGraph2D (3-D scatter) and TGraphMultiErrors (several y-error layers) are separate types with the same read/write traits.

use oxiroot::prelude::*;
let g = TGraph::with_errors(
    vec![1.0, 2.0, 3.0], vec![10.0, 20.0, 30.0], // x, y
    vec![0.1, 0.1, 0.1], vec![1.0, 2.0, 1.5],    // ex, ey
).named("res").titled("resolution");
g.write_root("graph.root", Compression::None)?;             // WriteRoot, like any object
let same = TGraph::read_root(&RFile::open("graph.root")?, "res")?;

A graph also round-trips ROOT's display frame (fHistogram) and the fitted functions ROOT stores in fFunctions — read back as GraphFunctions (faithful TF1/TFormula) and re-evaluable in ROOT:

use oxiroot::prelude::*;
let g = TGraph::new(vec![0.0, 1.0, 2.0], vec![1.0, 3.0, 5.0])
    .named("gfit")
    .with_function(GraphFunction::new("line", "[0]+[1]*x", vec![1.0, 2.0], 0.0, 2.0));
g.write_root("gfit.root", Compression::None)?;

Functions (oxiroot::hist)

  • Standalone TF1/TF2/TF3 keys — real ROOT function objects (each embedding a TFormula), built from a formula and a range and read/written like any other object. TF1::new("f", "[0]*sin([1]*x)", 0.0, 6.3)?.with_params(...).
  • A real TFormula expression engine (oxiroot-formula, a dependency-free crate): a tokenizer + Pratt parser + AST evaluator for arbitrary formulas — operators + - * / ^, the usual functions (sin/exp/log/sqrt/pow/…, with or without a TMath:: prefix), comparisons/?:, and ROOT's gaus/expo/ pol0..N shortcuts.
  • eval / integral / derivative in pure Rust — adaptive Gauss–Kronrod quadrature and a Richardson central derivative, matching TF1::Integral / TF1::Derivative.
  • The same engine powers Model::from_formula, so any formula is fittable, and TF1::to_model() bridges a function to a fit. oxiroot embeds the TF1/TF2/TF3/TFormula TStreamerInfo, so ROOT C++ and uproot both read and re-evaluate all three.
  • See the functions example.

Persistable objects (oxiroot::hist)

The two small classes ROOT constantly stashes alongside histograms — a labelled string (TObjString) and a named scalar (TParameter<T>, for a luminosity, an event count, a cut threshold) — read and write byte-for-byte as ROOT serializes them, through the same WriteRoot/ReadRoot traits as everything else.

use oxiroot::prelude::*;
RootFile::create("meta.root")
    .add(&TObjString::new("v2.1").named("version"))
    .add(&TParameter::f64("lumi", 137.5))      // TParameter<double>
    .add(&TParameter::i64("nevents", 9_000_000_000))  // <Long64_t>
    .write(Compression::None)?;

let f = RFile::open("meta.root")?;
assert_eq!(TObjString::read_root(&f, "version")?.value(), "v2.1");
assert_eq!(TParameter::read_root(&f, "lumi")?.value().as_f64(), 137.5);

TParameter comes in f64/f32/i32/i64 flavours (the double/float/ int/long long instantiations). Both ROOT C++ and uproot read them from oxiroot's files: the writer embeds the TStreamerInfo for whichever TParameter<…>/TObjString classes a file contains (merged into the histogram streamers), so uproot can model the templated TParameter class.

Collections (oxiroot::hist)

THStack (a stack of histograms) and TMultiGraph (several graphs in one frame) hold their members in a TList. Build them from existing TH1/TGraphs; on read they hand the members back. The members are serialized through ROOT's object protocol, so ROOT C++ and uproot read what oxiroot writes, and oxiroot reads ROOT's files (including the class back-references ROOT emits for repeated member types).

use oxiroot::prelude::*;
let stack = THStack::new().named("hs").titled("backgrounds")
    .add(Hist::reg(50, 0.0, 100.0).double().named("zjets"))
    .add(Hist::reg(50, 0.0, 100.0).double().named("ttbar"));
let graphs = TMultiGraph::new().named("mg")
    .add(TGraph::new(vec![0.0, 1.0], vec![1.0, 2.0]).named("obs"))
    .add(TGraph::new(vec![0.0, 1.0], vec![2.0, 1.0]).named("exp"));
RootFile::create("plots.root").add(&stack).add(&graphs).write(Compression::None)?;

let f = RFile::open("plots.root")?;
assert_eq!(THStack::read_root(&f, "hs")?.hists().len(), 2);
assert_eq!(TMultiGraph::read_root(&f, "mg")?.graphs()[0].name, "obs");

Linear algebra (oxiroot::linalg)

TVectorD (a vector), TMatrixD (a dense matrix), and TMatrixDSym (a symmetric matrix — the shape a fit's covariance takes) read and write byte-for-byte as ROOT's TVectorT<double> / TMatrixT<double> / TMatrixTSym<double>. The symmetric matrix is stored as the full matrix in memory but, like ROOT, written as just its upper triangle.

use oxiroot::prelude::*;
RootFile::create("fit.root")
    .add(&TVectorD::new(vec![91.2, 2.1]).named("pars"))
    .add(&TMatrixDSym::new(2, vec![0.04, 0.01, 0.01, 0.09]).named("cov")) // covariance
    .write(Compression::None)?;

let f = RFile::open("fit.root")?;
assert_eq!(TVectorD::read_root(&f, "pars")?.elements(), &[91.2, 2.1]);
let cov = TMatrixDSym::read_root(&f, "cov")?;
assert_eq!(cov.get(0, 1), cov.get(1, 0)); // symmetric

Particle data (oxiroot::particle)

A Rust take on scikit-hep particle: decode a PDG Monte Carlo ID straight from its digits (PdgId), and look particles up in a bundled ~600-particle PDG table (Particle) for their mass, width, lifetime, and quantum numbers. Dependency-free and verified against the particle package the way oxiroot::stat is verified against scipy.stats.

use oxiroot::particle::{Particle, PdgId};

// No table needed — the ID's digits say what it is.
assert!(PdgId::new(2212).is_baryon());          // proton
assert_eq!(PdgId::new(-11).charge(), Some(1.0)); // positron
let u235 = PdgId::new(1000922350);
assert_eq!((u235.a(), u235.z()), (Some(235), Some(92))); // uranium-235

// Physical properties from the bundled table (MeV, ns, mm), by name, id, or a
// `literals` named constant.
let muon = oxiroot::particle::literals::muon();
assert!((muon.mass().unwrap() - 105.6583755).abs() < 1e-4);
assert!((muon.lifetime().unwrap() - 2196.98).abs() < 0.1); // τ = ħ/Γ
assert_eq!(Particle::from_name("pi+").unwrap().invert().unwrap().name(), "pi-");

particle::literals mirrors scikit-hep's particle.literals — friendly named accessors like literals::proton(), literals::electron(), literals::jpsi().

Object collections (oxiroot::hist)

ObjList stores a bare TList or TObjArray of mixed objects under one key — the way ROOT groups, say, a set of systematic-variation histograms. Add any writable objects; on read, pull the members back out by type with items::<T>(). The members' streamer info is embedded automatically, so a TParameter or matrix inside the list is readable by uproot too.

use oxiroot::prelude::*;
let list = ObjList::list().named("systematics")
    .add(&Hist::reg(50, 0.0, 100.0).double().named("nominal"))
    .add(&TObjString::new("2024-data").named("tag"))
    .add(&TParameter::f64("lumi", 137.5));
RootFile::create("syst.root").add(&list).write(Compression::None)?;

let f = RFile::open("syst.root")?;
let list = ObjList::read_root(&f, "systematics")?;
assert_eq!(list.len(), 3);
assert_eq!(list.items::<TH1>()?.len(), 1);
assert_eq!(list.items::<TParameter>()?[0].value().as_f64(), 137.5);

ObjList::array() writes a TObjArray instead. items::<T>() covers the histogram family, TGraph, TObjString, TParameter, and the linear-algebra types; class_names() lists every member.

TMap is the keyed variant — ROOT's object → object map, the way ROOT stores string-keyed metadata. insert(key, value) takes a string key; look values up by key with get::<T>(key):

use oxiroot::prelude::*;
let meta = TMap::new().named("meta")
    .insert("version", &TObjString::new("2.1"))
    .insert("lumi", &TParameter::f64("lumi", 137.5));
RootFile::create("meta.root").add(&meta).write(Compression::None)?;

let meta = TMap::read_root(&RFile::open("meta.root")?, "meta")?;
assert_eq!(meta.get::<TParameter>("lumi").unwrap()?.value().as_f64(), 137.5);

uproot caveat: uproot has no TMap model, so a TMap is not readable there — a limitation that ROOT's own TMaps share. ROOT C++ reads what oxiroot writes, and oxiroot reads ROOT's TMaps.

Plotting (oxiroot::plot, plot feature)

Render histograms and graphs to SVG, PNG, and PDF with a matplotlib-like API and an mplhep histogram style — pure Rust, no matplotlib, no system fonts. One backend-independent draw IR fans out to a tiny-skia raster (PNG) and a hand-written SVG, so the two outputs share identical geometry. The default font is the bundled STIX Two (a LaTeX-like serif), and $…$ labels are typeset as real LaTeX math by the pure-Rust ReX TeX engine into the same IR.

Z to mu mu candidates: filled MC template with data points overlaid, matplotlib look The same histogram in the mplhep style with a bold CMS Preliminary label and luminosity/energy

2-D TH2 rendered as a viridis heatmap with a colorbar A main panel over a data/MC ratio panel sharing the x-axis

All figures are produced by cargo run -p oxiroot --example plot (PNG, SVG and PDF). Left: the plain matplotlib look (fancybox legend, STIX serif, LaTeX labels). Right: the HEP-publication look (Style::mplhep() + ax.hep_label("CMS", "Preliminary")).

use oxiroot::plot::{Axes, HistType, HistOpts, ErrorbarOpts, Hist2dOpts, Color};
use oxiroot::prelude::*; // the `plot` feature is on by default

// A filled MC histogram with "data" points overlaid + a legend.
let mut ax = Axes::new();
ax.hist_with(&mc, HistOpts::new().histtype(HistType::Fill).label("MC"));   // mplhep staircase
ax.errorbar_with(&data, ErrorbarOpts::new().color(Color::BLACK).label("data")); // a TGraph
ax.xlabel("$m_{\\mu\\mu}$ [GeV]");       // LaTeX math axis label
ax.ylabel("Events / 2 GeV");
ax.legend();
ax.save("mass.png")?;                    // format chosen by extension
ax.save("mass.svg")?;

// A TH2 as a viridis heatmap with a colorbar.
let mut ax2 = Axes::new();
ax2.hist2d_with(&th2, Hist2dOpts::new().label("entries"));
ax2.save("heatmap.svg")?;
# Ok::<(), oxiroot::plot::Error>(())
  • Axes mirrors matplotlib (with Rust-idiomatic names): hist/hist_with (TH1, mplhep step/fill/band/errorbar staircase with √N/Sumw2 error bars), errorbar/errorbar_with (TGraph, all three error variants), profile (TProfile), hist2d/hist2d_with (TH2 color mesh with a colorbar and the real matplotlib viridis/plasma colormaps; Hist2dOpts::log()/.norm(Norm::…) switches to a log / symlog color scale with a decade colorbar, like matplotlib's LogNorm/SymLogNorm), plot, function (overlay any analytic or fitted curve), grid, xlabel/ylabel/title, xlim/ylim (taking a Range, e.g. ax.xlim(0.0..100.0)), and legend. The *_with methods take an options builder; the bare ones use defaults.
  • Publication-ready out of the box — a matplotlib fancybox (rounded) legend, the STIX serif used by LaTeX, and Style::mplhep() for the HEP look (in-pointing minor ticks on all four sides, a heavier frame). ax.hep_label("CMS", "Preliminary") adds the bold experiment label, and ax.hep_rhs("138 fb$^{-1}$ (13 TeV)") the luminosity/energy above the frame.
  • Overlay a fitax.function(|x| …, x0..x1) draws any closure as a smooth curve; with the fit feature, ax.model(&model, x0..x1) overlays a fitted oxiroot::fit Model directly on a histogram, and ax.fit_stats(&model, &result) adds a ROOT-style stat box (TPaveStats) with the function name, χ²/ndf, and each fitted parameter ± its error.
  • Multi-panel layoutssubplots_grid(rows, cols) and a custom GridSpec (height/width ratios, spacing) with fig.sharex()/sharey() and a suptitle, plus a one-call ratio_subplots() for the HEP main-over-ratio plot (shared x-axis, the upper panel's x labels hidden).
  • Outputsave/save_with pick the format from the extension: .png, .svg, and .pdf (a hand-written vector PDF). SaveOpts sets the DPI for a sharper PNG (save_with(path, SaveOpts::new().dpi(300.0))) or a transparent background; to_png_bytes/to_svg_string render in memory. PdfPages collects several figures into one multi-page vector PDF (matplotlib's PdfPages).
  • Fonts — the default is STIX Two (a LaTeX-like serif: STIX Two Text + STIX Two Math) for a publication look with text and math in one typeface. ax.fonts(FontSet::dejavu()) switches to the matplotlib sans-serif look, and FontSet::from_path("MyFont.otf") / from_paths(text, math) plug in any TrueType/OpenType font (the math font needs a MATH table).
  • The default look otherwise reproduces a plain matplotlib figure (640×480, the tab10 color cycle, out-pointing ticks, 5 % margins, light grey grid); Style::mplhep() switches to in-pointing ticks on all four sides with minor ticks.
  • A worked example renders a Z → μμ overlay and a 2-D heatmap to PNG and SVG:
    cargo run -p oxiroot --example plot

TTree (oxiroot::tree)

  • Read & write scalar, fixed-size array (x[N], incl. multidimensional x[N][M]), variable-length / jagged (x[n]), string, multi-leaf (leaflist a/F:b/I), and std::vector<T> branches; read std::vector<std::string>, std::set<T> (an unsplit object-wise collection) and std::map<K,V> (split into parallel .first/.second collections), and TNtuple / TNtupleD (the all-float/double TTree subclasses) as ordinary trees.
  • Split std::vector<MyStruct> branches written as per-member sub-branches (TBranchElement), with a generated TStreamerInfo for the element class — ROOT-C++- and uproot-verified, both directions.
  • Branch::{i32, f64, bools, strings, …} for scalars, Branch::vec_* for fixed arrays, Branch::jagged_* for variable arrays, Branch::vector_* for std::vector<T>, and Branch::split_vector for split structs.
  • Tree::new(name, branches).write_root(path, compression) is the method form (mirroring hist.write_root); .write_root_baskets(…, entries_per_basket) writes several baskets per branch, and .to_root_bytes(…) returns the file bytes. The free write_tree_file/write_tree_file_baskets functions remain.
  • TTreeWriter streams a tree in batches (write_batch emits one basket per branch straight to disk, then finish), so only the current batch is held in memory — the way ROOT's TTree::Fill flushes baskets. ROOT-C++- and uproot-verified across many baskets, compressed and not. Use TTreeWriter::create_large for a tree that will exceed 2 GiB — it writes the 64-bit container form (again ROOT-C++/uproot-verified).
  • read_branch reads a whole branch, read_branch_range(start, stop) only the baskets covering a window, and read_branch_flat an offsets+flat (no Vec<Vec>) view; TChain spans many files (optional rayon decodes baskets in parallel). Introspect with branch_type/branch_shape/branch_title, and see what was skipped via unsupported_branches(). Worked example: cargo run -p oxiroot --example tree.
  • friends() returns the friend trees attached with TTree::AddFriend (read from the persisted fFriends list). A friend is read positionally — entry i of the main tree pairs with entry i of the friend — so opening the friend tree and reading its branches yields columns that line up by entry.
  • aliases() / alias(name) return the (name, expression) shorthands defined with TTree::SetAlias (read from fAliases); oxiroot reads the expression strings but does not evaluate them. TEntryList::open(file, name) reads a saved entry selection (a standalone key) into the ascending list of selected entry numbers, with entries() / contains(entry).
  • Old unsplit object branches (TBranchObject, the pre-TBranchElement way of storing a whole object per entry) are read by synthesizing one branch.member column per basic/string member of the object class — e.g. a branch of TNamed reads as branch.fName / branch.fTitle.
  • The reader is streamer-info-driven: it parses TTree/TBranch/ TBranchElement by walking the member list in the file's own TStreamerInfo (TTree::streamer_classes exposes it), so a schema change is absorbed instead of misread; an unknown member type is reported, never parsed at a guessed offset.
  • The writer generates that TStreamerInfo from a declarative class table (the whole TTree/TBranch/TLeaf*/TBranchElement hierarchy, with ROOT's own versions and checksums) rather than shipping baked blobs — every written file is self-describing and reads back in ROOT, uproot, and this crate.

RNTuple (oxiroot::ntuple)

  • Read the binary spec v1.0.0.0: anchor → envelopes → schema → clusters → pages. Every physical column encoding decodes — split/zigzag/delta, all integer widths (8/16/32/64-bit, signed & unsigned), reduced-precision reals (half, truncated, quantized), and the Switch column — on Zstd-compressed pages.
  • Typed field API (read_field) for scalars, std::string, std::vector<T>, nested collections — std::vector<std::string>, std::vector<std::vector<T>>, and vectors of records (std::vector<MyStruct>/std::pair) — std::variant, the nullable / "late" fields std::optional<T> / std::unique_ptr<T> (read as a [FieldValues::Opt] you can zip with opt_f32() etc.), std::atomic<T>, fixed-size std::array/std::bitset, and user-defined classes (split into a record of their members), across multiple clusters. Schema-extended RNTuples (fields added late via the footer's schema-extension record) are read too — the late fields merge into the schema and their deferred columns back-fill the entries written before the field existed.
  • Write the same surface it reads: bool, every integer width (8/16/32/64-bit, signed & unsigned), f32/f64, reduced-precision reals (Field::half / truncated / quantized), std::string, std::vector<T>, the nested collections (Field::vec_str / vec_vec_*, Column::Record/Nested), std::variant (Field::variant), and the nullable / atomic fields (Field::optional_* / unique_ptr_* from a Vec<Option<T>>, Field::atomic_*) — optionally Zstd-compressed.
  • Ntuple::write_root_extended writes a schema late extension: this RNTuple's fields go in the header, and each late (first_entry, field) is added through the footer's schema-extension record as a deferred column (its data covers entries first_entry..N; earlier ones default). ROOT reads the result as if the field had been added mid-writing with a model updater.
  • Ntuple::new(name, fields).write_root(path, compression) is the method form (mirroring hist.write_root), with .to_root_bytes(…) for the file bytes; the free write_rntuple_file remains.
  • NtupleFile writes several RNTuples per file and RNTuples inside a TDirectoryNtupleFile::new().add(events).add(runs).dir("cal", |d| d.add(pedestals)).write_root(…). Read a nested one with RNTuple::open_in(file, "cal", "pedestals"). ROOT and uproot navigate the result natively.
  • RNTupleWriter streams one cluster per write_batch, so a large dataset is never fully held in memory.

Merging files — hadd (oxiroot::hadd)

  • A pure-Rust haddmerge_files("all.root", &["run1.root", "run2.root"], Compression::Zstd(5))? combines several ROOT files the way ROOT's most-used command-line tool does: TH1/TH2/TH3/TProfile summed bin-by-bin (the exact add reduction — contents, Sumw2, entries, moments), and TTree / RNTuple entries concatenated. Other supported objects (graphs, 2D/3D profiles, efficiencies, functions, strings, matrices, …) are copied from the first file; unknown classes are skipped and listed in the report, never silently dropped.
  • Each concatenated branch keeps its original kind (scalar, x[N], jagged x[n], std::vector<T>, string). The standalone oxiroot_tree::concat_trees, oxiroot_rntuple::concat_ntuples, and oxiroot_hist::merge_histogram_files do the per-format work and can be called directly.
  • Merger::new().inputs(paths).compression(c).merge("all.root")? is the composable builder; merge_files returns a MergeReport (what was summed / copied / skipped, and the total entries) with a one-line Display.
  • One call writes one file and does not yet mix histograms with a TTree/RNTuple in a single output (each owns auxiliary basket/page keys): a fileset is an all-histogram set, a single TTree, or a single RNTuple — anything else is refused with a message naming the keys. Verified against ROOT 6.40's hadd (identical histogram sums, entry-for-entry tree concat) and read back by uproot and ROOT C++. See the merge example.

Command-line inspector — oxroot (oxiroot-cli)

  • Look into a ROOT file from the shell, no ROOT or Python: cargo install --path crates/oxiroot-cli builds the oxroot binary. Objects are addressed file.root:name (uproot-style), with a /-path for nested TDirectorys (file.root:cal/run2/Events).
  • oxroot ls [-l] [-r] — list objects (name, class, title; -l adds cycle and TTree/RNTuple entry count; -r recurses into every TDirectory, by full dir/sub/name path).
  • oxroot show file.root:Events — a TTree's branches with their types (double, double[3], double[], char*; unreadable branches flagged), or an RNTuple's fields with C++ type names.
  • oxroot dump file.root:obj [-n N] [-b a,b] — the first N TTree/RNTuple entries as a column table, a TH1's bins + stats, a TGraph's points, or a scalar value.
  • oxroot stat file.root — size, ROOT version, compression, key count, and the embedded streamer classes.
  • --json (global) emits machine-readable JSON for any command — dump rows are typed (numbers, booleans, arrays) — via a dependency-free writer.
  • See the command-line inspector guide.

Compression

  • Read: Zstd, zlib, LZ4, and LZMA (XZ) decode — every codec ROOT writes except the legacy CS. Uncompressed objects pass through directly.
  • Write: Zstd, zlib, LZ4, and LZMA via Compression::{Zstd, Zlib, Lz4, Lzma}(level), or Compression::None. Files written with any of them match ROOT's own block framing and read back in ROOT and uproot.

All four codecs are pure Rust (ruzstd, miniz_oxide, lz4_flex, lzma-rust2), so the no-libROOT promise holds. LZ4 blocks carry ROOT's XXH64 integrity check, verified on read.

Robustness & large files

  • Parsers are hardened against malformed input: every read path is bounds- and overflow-checked, capacity reservations are bounded by the remaining buffer, and histogram array lengths are validated against the axis geometry — so a crafted or truncated file yields an Err, never a panic. Byte-flip and truncation fuzz tests cover the container, RNTuple, TTree, and every histogram/graph reader.
  • 64-bit (> 2 GiB) files are supported on read and write. The one-shot TFile object writers (RootFile) and the RNTuple writer auto-switch to ROOT's big (64-bit) container form once a file would cross 2 GiB; the streaming TTreeWriter::create_large / RNTupleWriter::create_large opt into it up front (the plain create stays 32-bit and errors past 2 GiB rather than truncating its seek pointers). Appending (RootFile::open(...).add(...).write()) also crosses into the 64-bit form: the existing bytes stay put — preserving subdirectories and any RNTuple at their original offsets — while the header and root directory record are widened in place (every oxiroot/ROOT file reserves the 64-bit directory width up front, as ROOT does). All big-format writes are verified against ROOT C++ and uproot.
  • Error is #[non_exhaustive] and preserves the underlying io::ErrorKind.

Examples

Every example under crates/oxiroot/examples/ is a self-contained, runnable file. Run any of them with cargo run -p oxiroot --example <name>; the default build already has the fit and plot features on, so nothing extra is needed.

Example What it shows
Start here
analysis End-to-end mini analysis: weighted/variable-bin histograms → scale/merge/normalize → subdirectories → a columnar dataset → read-back
inspect Introspect any ROOT file you did not write — a programmatic oxroot ls
Statisticsoxiroot::stat
stat_intro Descriptive stats + the Normal/StudentT/ChiSquared/FisherF distributions and confidence intervals
stat_tests Hypothesis tests (t-test, Mann–Whitney, KS), correlation (Pearson/Spearman), goodness-of-fit, normality
stat_physics A HEP counting experiment: discovery significance, Feldman–Cousins, Garwood/Clopper–Pearson intervals, combining measurements
lineshapes HEP peak shapes (Crystal Ball, Voigt, Breit–Wigner, Novosibirsk, ARGUS…) + a Crystal Ball fit
particles PDG particle data — decode IDs (PdgId) and look up masses/widths/lifetimes (Particle)
Fittingoxiroot::fit
fit Fit a Gaussian peak (χ² and likelihood), a peak-on-background, a TGraph, and raw points — one API
robust_fit Outlier-resistant losses (SoftL1/Huber/Cauchy) versus ordinary least squares
Histograms & profiles
profile TProfile / TProfile2D — the mean of y in slices of x
efficiency TEfficiency turn-on curve with a Clopper–Pearson interval
sparse THnSparse — a memory-efficient N-dimensional histogram
threaded Multithreaded fill with ThreadedHist (ROOT's TThreadedObject analog)
Graphs & functions
graphs TGraph / TGraphErrors / TGraphAsymmErrors / TGraph2D / TMultiGraph
functions TF1/TF2/TF3 from formulas: eval / integral / derivative + round-trip
Objects & linear algebra
objects Store run provenance (TObjString / TParameter / TList) next to your data
linalg TVectorD / TMatrixD / TMatrixDSym round-trip (a fit-covariance shape)
I/O & formats
tree TTree write + read, introspection, entry ranges, streaming TTreeWriter
rntuple A basic flat RNTuple write/read, plus several RNTuples in one file
rntuple_nested Nested RNTuple fields: vector-of-string, vector-of-vector, vector-of-record
compression The None / Zstd / Zlib / Lz4 size trade-off — every codec lossless
merge A pure-Rust hadd: sum histograms, concatenate trees / RNTuples
plot Render histograms and graphs to SVG / PNG / PDF (matplotlib + mplhep look; needs plot)

Workspace layout

Crate Purpose
oxiroot Facade: prelude + re-exports of everything below
oxiroot-io-core TFile container, buffer primitives, streamer + object-reference engine, the WriteRoot/ReadRoot object framework, Error
oxiroot-compress ROOT 9-byte block framing + Zstd/zlib/LZ4/LZMA codecs
oxiroot-rntuple RNTuple reader/writer (spec v1.0.0.0)
oxiroot-hist Histograms, profiles, TEfficiency/THnSparse/TH2Poly, and the TGraph family
oxiroot-linalg ROOT linear-algebra objects — TVectorD/TMatrixD/TMatrixDSym
oxiroot-tree Classic TTree read/write
oxiroot-fit Minuit2 curve fitting for any 1-D data (FitData/Model); fit feature
oxiroot-stat Dependency-free statistics — special functions, distributions, descriptive stats, correlation & tests (verified vs scipy.stats)
oxiroot-particle PDG particle data — the numbering-scheme decoder + a bundled particle table (verified vs scikit-hep particle)
oxiroot-plot Matplotlib-style SVG/PNG plotting for histograms and graphs; plot feature
oxiroot-cli oxroot: a command-line inspector (ls/show/dump/stat)

Dependencies are pure Rust: ruzstd (Zstd), miniz_oxide (zlib), lz4_flex (LZ4), lzma-rust2 (LZMA/XZ), and xxhash-rust (RNTuple XXH3 + LZ4 XXH64).

Optional features

Feature Default Effect
mmap Memory-mapped read path (RFile::open_mmap) for large files; adds memmap2.
rayon Data-parallel histogram fill (hist::fill_par) and TTree basket decode; adds rayon.
fit Curve fitting (oxiroot::fit, TH1::fit) via the pure-Rust Minuit2 port; adds minuit2.
argmin Adds the gradient-free Nelder–Mead minimizer backend (Minimizer::NelderMead); implies fit, adds argmin.
plot Plotting (oxiroot::plot): SVG/PNG/PDF rendering of TH1/TH2/TGraph/TProfile; adds tiny-skia, ab_glyph, and the ReX TeX engine.
http Remote reads over HTTP(S) byte-range requests (RFile::open_url); adds the pure-Rust ureq (rustls) client. Off by default so the standard build needs no TLS/networking stack.
xrootd Remote reads over the XRootD root:// protocol (RFile::open_url), with unix auth for public data (e.g. root://eospublic.cern.ch). Pure std::net — adds no dependencies.

All are on by default — the facade is batteries-included. For a lean, pure-Rust format core with a minimal dependency set, opt out with default-features = false and re-enable what you need.

Build & test

cargo build  --workspace
cargo test   --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt    --all --check

The committed tests are pure Rust (no ROOT or Python needed): they check self-round-trips, byte-level agreement against committed reference files, and malformed-input hardening. CI additionally round-trips every type both ways against official ROOT (C++) and uproot — Rust writes files they read, and reads files they write.

Full local interop check

For a comprehensive cross-language check on your own machine (not CI), run:

bash scripts/interop_local.sh                 # full run; prints a PASS/FAIL matrix
bash scripts/interop_local.sh --no-fixtures   # skip the fixture-regen step (fast)
bash scripts/interop_local.sh --big           # also exercise the >2 GiB (64-bit) read path

It exercises oxiroot's full read+write surface against both ROOT C++ and uproot, in both directions: the lean canonical round-trip, a manifest-driven matrix (every histogram precision×dimension, TProfile, Sumw2, variable bins, multi-object/subdirs/append, every RNTuple scalar+vector type + multi-cluster, every TTree branch kind + scalar width + split std::vector<Struct>), cargo test --workspace, and a drift check that regenerates the committed fixtures from your local ROOT/uproot and re-tests. Missing tools (no ROOT, or no uproot venv) degrade to SKIP, never FAIL, so a machine with neither still gets a meaningful green from cargo test alone. Needs a Python venv at .venv with uproot numpy awkward, and root-config (+rootcling) on PATH for the ROOT-C++ side.

Roadmap

Experimental (0.0.x). On the list — each item targets the same bar as what already ships: byte-level round-trips verified against both ROOT and uproot. Grouped by the ROOT feature each fills.

  • TTree
    • Index-based friend join (TTree::BuildIndex) — read the persisted fTreeIndex (TTreeIndex) so friends can be joined on a (major, minor) key instead of by entry. (Positional friends — AddFriend, read entry-aligned — already work.)
  • RNTuple
    • A ROOT-C++-confirmed std::map write — oxiroot already writes a spec-compliant std::map RNTuple that uproot reads and oxiroot round-trips (Field::map; see the assoc tests). Confirming a ROOT-C++ round-trip is blocked because ROOT 6.40's std::map collection proxy is non-functional in the test build — it can neither create nor read a std::map RNTuple field — so this needs a ROOT install with the std::map dictionary loaded to verify.
  • Generic object reader — read any class in a file driven by its TStreamerInfo into a dynamic value tree (the streamer-info member walker already powers the TTree reader), so oxiroot can inspect arbitrary ROOT files (rootls / rootprint-style), not only the typed hist/graph/tree/RNTuple models.
  • Remote reads — richer XRootD auth (GSI / X.509 and token / ZTN) for access-controlled data. The root:// transport ships today (RFile::open_url, the xrootd feature) with unix auth for public data, alongside HTTP(S) range reads (the http feature).
  • TFile container
    • Arbitrary-depth TDirectory write — the builder nests one level today.
    • Delete / compact in update mode — append mode ships; rewriting a key at a new cycle and purging old cycles (TFile::Purge) does not.
    • Read an object at an explicit cycle (name;N).
  • Axestime axes (TAxis fTimeDisplay / fTimeFormat) on histograms and graphs, for monitoring-style time series.
  • RDataFrame-style analysis (far future) — a lazy, columnar analysis front-end (Define / Filter / Histo1D / Sum, executed in one pass over a TTree or RNTuple, parallelised across clusters) built on the existing readers. This is a compute engine, not IO — a large, separate undertaking — so it sits well beyond the current milestones, but it is the natural cap on the stack once the IO layer is complete. (RNTupleProcessor, ROOT's RNTuple-native iteration, is the same shape and would share the machinery.)

Out of scope: ROOT 7 RHist (no persistable on-disk format — its Streamer throws); and reading/writing ROOT's own graphics objects (TCanvas, TPad, …) — the plot feature renders the data, it does not (de)serialize ROOT graphics.

License

Licensed under the MIT License.

About

Pure-Rust reader and writer for CERN ROOT files (RNTuple and classic histograms). Interoperable with official ROOT and uproot. No libROOT.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors