Skip to content

[RF][HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVar#22842

Open
Phmonski wants to merge 4 commits into
root-project:masterfrom
Phmonski:feat/hs3-generic-binning
Open

[RF][HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVar#22842
Phmonski wants to merge 4 commits into
root-project:masterfrom
Phmonski:feat/hs3-generic-binning

Conversation

@Phmonski

Copy link
Copy Markdown
Contributor

Motivation

Following the introduction of explicitly binned RooGenericPdf and RooFormulaVar objects in #22708, their declared binning information was not preserved by HS3 serialization.

Only the formula expression was exported, so an HS3 round trip lost the binning registered through setBinning(). The imported object consequently no longer reported itself as a binned distribution and could not use the intended binned integration path.

Changes

This PR adds optional inline axes key to HS3 generic formulas.

Uniform binnings are represented as:

"axes": [
  {
    "name": "x",
    "min": 0.0,
    "max": 10.0,
    "nbins": 5
  }
]

Non-uniform binnings use explicit edges:

"axes": [
  {
    "name": "x",
    "edges": [0.0, 1.0, 1.5, 3.0]
  }
]

The implementation:

  • Adds a read-only getBinning() accessor to RooGenericPdf and RooFormulaVar.
  • Returns only binnings explicitly registered through setBinning().
  • Refactors binBoundaries() and plotSamplingHint() to use this accessor.
  • Exports axes only when an explicit formula binning exists.
  • Preserves uniform binnings compactly and non-uniform binnings with their complete edge arrays.
  • Imports the axes into a temporary formula object before adding it to the workspace.
  • Calls setBinning(..., false) during import because serialized binnings are treated as trusted declarations, avoiding potentially expensive flatness sampling.
  • Keeps RooFormulaVar export type as generic while continuing to import both generic and generic_function.
  • Reuses a shared internal axis writer for ordinary observable axes and generic-formula axes.

No persistent members or class versions are changed.
JSONFactories_HistFactory.cxx and a few unrelated test lines contain formatting-only changes.

Validation

Imported axes are checked for:

  • A valid sequence and map structure.
  • Unique axis names.
  • A matching direct RooAbsRealLValue dependency.
  • Positive integral nbins.
  • Finite, ordered uniform bounds.
  • At least two strictly increasing explicit edges.
  • No mixing of uniform and explicit-edge representations.

Tests cover:

  • Uniform RooGenericPdf round trips.
  • Uniform RooFormulaVar round trips.
  • Both supported function type aliases.
  • Multiple binned observables.
  • Exact non-uniform boundaries, including boundaries outside the observable's current range.
  • Accessor lookup through same-named observables.
  • Binning removal.
  • Rejection of malformed axes.
  • Generic formulas without explicit binnings remaining unchanged.

The complete GenericPdf test suite and testRooFitHS3 pass.

@cburgard @will-cern

@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 15h 30m 48s ⏱️
 3 874 tests  3 874 ✅ 0 💤 0 ❌
78 808 runs  78 808 ✅ 0 💤 0 ❌

Results for commit c63e052.

@guitargeek guitargeek changed the title [HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVa [HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVar Jul 20, 2026
@guitargeek guitargeek changed the title [HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVar [RF][HS3] Preserve explicit binnings for RooGenericPdf and RooFormulaVar Jul 20, 2026
Phmonski and others added 4 commits July 22, 2026 10:00
The axis parser for RooGeneric/RooFormulaVar binning read "edges",
"min" and "max" via JSONNode::val_double() without first checking the
node was actually a number. For a non-numeric JSON value (e.g. a
string) this either threw a backend-specific exception that is not a
std::runtime_error (aborting past the tool's error handling) or, with
the stringstream-based backend, silently coerced the value to 0 and
imported a wrong binning.

Add a JSONNode::is_number() predicate (native in the nlohmann backend,
with a val()-parsing fallback in the base interface) and guard the edge
and min/max conversions with it, so malformed bounds are rejected with a
descriptive RooJSONFactoryWSTool::error() like every other malformed
axis case. Full precision is preserved because the value is still read
through val_double() once validated.
readPositiveInteger parsed "nbins" with a strict std::from_chars over
the node's textual value, which rejected an integer bin count encoded as
a JSON float: e.g. 1000000.0 renders as "1e+06", where from_chars stops
at 'e' and the whole import fails with a misleading "must be a positive
integer" error. Other nbins readers in HS3 use the lenient val_int().

Read the value through val_double() instead and require it to be finite,
>= 1, integral and within int range. This accepts integer-valued floats
like 1e6 while still rejecting fractional (2.5), non-positive (0) and
non-numeric values, so the existing rejection tests keep passing. Drops
the now-unused <charconv> include.
The separate roofit/jsoninterface package made sense when there was
also a YAML backend, but since only the nlohmann/json backend remains,
it was just unnecessary abstraction boilerplate as a standalone
library.

Move the RooFit::Detail::JSONInterface header into RooFitHS3 and merge
the implementation (JSONInterface.cxx, JSONParser.h, JSONParser.cxx)
into a single translation unit, with the TJSONTree implementation class
now in an anonymous namespace. This remains the only translation unit
that includes nlohmann/json.hpp, so the JSON engine could still be
swapped out in the future by changing only this one file.

Also remove the unused writeYML() interface method, a leftover from the
YAML backend, and move testJSONInterface into the RooFitHS3 test suite.
@guitargeek
guitargeek force-pushed the feat/hs3-generic-binning branch from c63e052 to 775655a Compare July 22, 2026 08:00
@guitargeek
guitargeek requested a review from bellenot as a code owner July 22, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants