Skip to content

feat: native reading and writing of TEfficiency - #1822

Open
jburzy wants to merge 2 commits into
scikit-hep:mainfrom
jburzy:jburzy/feat_tefficiency
Open

jburzy wants to merge 2 commits into
scikit-hep:mainfrom
jburzy:jburzy/feat_tefficiency

Conversation

@jburzy

@jburzy jburzy commented Nov 5, 2026 •

Copy link
Copy Markdown

Refs #38.

The problem

TEfficiency::fBeta_bin_params is a std::vector<std::pair<double,double>>, which ROOT serializes memberwise. Uproot can only read it when the file also contains a TStreamerInfo for pair<double,double>, and ROOT generally does not write one. In practice, released Uproot (5.7.0) cannot read TEfficiency objects from recent ROOT:

ROOT version pair streamer in file Uproot 5.7.0
6.22/02 (uproot-issue209.root) no NotImplementedError
6.22/06 (uproot-issue38c.root) yes reads
6.28/04, 6.30/02, 6.32/02, 6.34/02, 6.36/02 no NotImplementedError
6.40/02 no NotImplementedError

For each version I wrote two objects, one with default settings and one with per-bin priors (SetBetaBinParameters); both fail. The error happens even when the vector is empty.

What this PR does

  • models/TEfficiency.py: a hand-written Model_TEfficiency_v2 that reads fBeta_bin_params directly, so it no longer depends on the file's streamers. The objectwise layout (very old ROOT) still uses the generic path. The model is writable. It writes ROOT's memberwise layout and keeps the TEfficiency status bits stored in TObject::fBits (Bayesian, posterior mode, shortest interval, bin prior, weights). It also names the histograms <name>_passed and <name>_total, as TEfficiency::SetName does.

  • behaviors/TEfficiency.py:

    • .passed and .total return the histograms.
    • .values(flow=False) returns the efficiencies, and .errors(flow=False) / .intervals(flow=False) return the uncertainties and interval bounds.
    • Accessors for the statistic option, confidence level, beta parameters and option flags.

    The calculations are ported from TEfficiency.cxx (GetEfficiency, GetEfficiencyErrorLow/Up and the interval functions) for all 9 EStatOption values, including weights, posterior mode, shortest intervals and per-bin priors. SciPy is needed for the uncertainties; it's imported lazily through the new uproot.extras.scipy().

  • writing/identify.py: to_TEfficiency(name, title, passed, total, ...) builds a writable TEfficiency from anything to_writable accepts (uproot histograms, hist/boost-histogram, numpy.histogram tuples). It checks that the binning matches and detects weighted histograms automatically. A TEfficiency read from a file can be written back with file["eff"] = eff.

  • Other changes: scipy added to the dev extra group, and reset_classes() reloads the new model.

import uproot

# reading
with uproot.open("file.root") as f:
    eff = f["efficiency"]
    values = eff.values()             # efficiency per bin
    err_low, err_high = eff.errors()  # uncertainties, as in GetEfficiencyErrorLow/Up
    edges = eff.axis().edges()
    passed, total = eff.passed, eff.total  # uproot histograms

# writing
with uproot.recreate("out.root") as f:
    f["copy"] = eff  # a TEfficiency read from a file
    f["new"] = uproot.writing.identify.to_TEfficiency(
        "new", "efficiency;x;#epsilon", passed_hist, total_hist,
        fStatisticOption="kFWilson",

Behavior change

fBeta_bin_params is now a NumPy structured array with fields "first" and "second", rather than a list of dynamically generated pair objects (and only for the files where the old version could read it at all). tests/test_0038_memberwise_serialization.py is updated accordingly. Its test_read_efficiency_without_streamer previously asserted NotImplementedError and now checks that the object reads.

Not included (possible follow-ups)

  • A general fix in containers.py for streamerless memberwise vector<pair<...>> in other classes.
  • Conversion to TGraphAsymmErrors, like TEfficiency::CreateGraph.
  • Contributing small ROOT 6.28–6.40 TEfficiency files to scikit-hep-testdata as regression fixtures. I have them and can open a PR there if that's useful.

AI disclosure

Following CONTRIBUTING.md: this contribution was developed with significant help from Claude (Anthropic) via Claude Code. That covers diagnosing the problem, writing the code and tests, and running the validation against ROOT described above. I reviewed the changes, checked them on my own files, and am happy to answer questions about them.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the type/feat PR title type: feat (set automatically) label Nov 5, 2026

@ariostas ariostas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI text below 🤖

Thanks for this, it's a really nice addition! I checked the efficiencies and intervals against ROOT 6.40.02 (writing with PyROOT, reading back with this branch), and almost everything matches exactly, including the weighted and Bayesian paths. A few things I found, roughly in order of importance:

  1. Feldman-Cousins is wrong for bins with more than ~1030 entries (inline comment with a fix that matches ROOT exactly).
  2. The objectwise branch of _read_beta_bin_params can't read ROOT-written files (inline comment with a fix).
  3. to_TEfficiency isn't exported from uproot.writing, unlike the other to_* functions.
  4. Two minor ones: histogram names when fName is None, and Feldman-Cousins performance for large 2D efficiencies.

One design note, not blocking: the root cause of needing a hand-written reader is that containers.AsVector can't read a memberwise vector<pair<T1,T2>> when the file has no pair streamer (main fails on modern TEfficiency files with NotImplementedError: streamerless memberwise serialization of class AsVector(pair<double,double>)). Synthesizing the pair model from its template arguments there would fix this for any class with such a member, not just TEfficiency. That's fine as a follow-up though.

Comment thread src/uproot/behaviors/TEfficiency.py Resolved
Comment thread src/uproot/models/TEfficiency.py Resolved
Comment thread src/uproot/writing/identify.py Resolved
Comment thread src/uproot/models/TEfficiency.py Resolved
Comment thread src/uproot/behaviors/TEfficiency.py Resolved
@ariostas

Copy link
Copy Markdown
Member

Hi @jburzy, thanks for the PR! I left a few review comments.

Btw don't worry about the CI failures. Those are unrelated and should work if you rebase or merge main.

jburzy and others added 2 commits November 13, 2026 10:53
TEfficiency's fBeta_bin_params member is a std::vector<std::pair<double,double>>
that ROOT serializes memberwise. Uproot could only read it when the file
contained a TStreamerInfo for pair<double,double>, which ROOT generally does
not write (none of the files written by ROOT 6.28 through 6.40 that were
tested have it), so TEfficiency objects from recent ROOT could not be read.

- models/TEfficiency.py: hand-written Model_TEfficiency_v2 that reads
  fBeta_bin_params directly (memberwise; objectwise falls back to the generic
  path), independent of the file's streamers. It is also writable, emitting
  ROOT's memberwise layout and preserving the TEfficiency status bits in
  TObject::fBits.
- behaviors/TEfficiency.py: passed/total histograms, values(), errors(),
  intervals(), and option accessors. Efficiencies and intervals are ported
  from TEfficiency.cxx for all EStatOption values, weights, posterior mode,
  shortest intervals, and per-bin priors (requires scipy).
- writing/identify.py: to_TEfficiency() builds a writable TEfficiency from
  any histograms that to_writable() accepts.
- extras.scipy(), scipy in the dev "extra" group, and reset_classes() reloads
  the new model.

fBeta_bin_params is now a NumPy structured array with fields "first" and
"second", rather than a list of pair objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Compute Feldman-Cousins probabilities and likelihood ratios in log space,
  as ROOT does; they overflowed for totals above ~1030 entries. Each distinct
  (total, passed) pair is computed once and both bounds are cached.
- Read objectwise vector<pair<double,double>> directly, skipping each pair's
  byte count, version and checksum.
- Export to_TEfficiency from uproot.writing.
- Name the histograms "passed"/"total" when the efficiency has no name.
- Add tests for large-total Feldman-Cousins, objectwise reading (written by
  ROOT), the export and the histogram names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jburzy
jburzy force-pushed the jburzy/feat_tefficiency branch from f2032ad to eac9fd6 Compare November 13, 2026 19:58
@jburzy

jburzy commented Nov 13, 2026

Copy link
Copy Markdown
Author

Thank you for the review @ariostas ! I have addressed all of your comments in a new commit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feat PR title type: feat (set automatically)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants