Skip to content

[RF][HS3] Import and export through a typed representation of HS3 documents - #24107

Merged
guitargeek merged 20 commits into
root-project:masterfrom
guitargeek:hs3-document
Nov 12, 2026
Merged

guitargeek merged 20 commits into
root-project:masterfrom
guitargeek:hs3-document

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

Makes RooFitHS3 read and write HS3 documents through RooFit::HS3Document, a typed representation that follows the structure of the standard and does not depend on RooFit.

Until now, parsing and building RooFit objects were intertwined: malformed documents were only noticed halfway through an import, and every importer dealt with JSON details on its own. With this PR a document is parsed and validated completely before anything is created in the workspace, and all problems are reported with their location in the document. The export collects everything except the various pdf and function objects in the same representation, checks it with the same validation, and writes it in one place.

In the future, we could even think about extending the strongly typed internal representation also to to the pdfs and functions, but that is currently not possible because of the open-world custom importers and exporters. Maybe that can change one day if HS3 is general enough so no custom IO is needed, because making the JSON IO of RooFit a closed-world system has clear benefits too.

The changes in this PR were fully implemented by AI (Claude Code with Opus 5.5), instructed with the architecture I had in mind.

Not adding any tests was a deliberate choice of mine: the tests that I'd write would only lock in current behavior which might or might not yet be fully HS3 conform, therefore acting as an additional source of truth that has to be taken into account while RooFit HS3 is still evolving. That's why I leave the implementation of tests (mostly regression and compatibility tests) to the developers of HS3 who are users at the same time, like @cburgard, @Phmonski, and @stalbrec.

@cburgard

Copy link
Copy Markdown
Contributor

That's a pretty impressive refactor, nice work!
I haven't had the time to look at everything yet, but largely, it's pretty uncontroversial.
Is this intended to go in in this cycle already? If yes, we need some quick conflict resolution with the still-open PRs #24001, #24005, #24013 and #24099, but I guess we can manage that.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Test Results

    22 files      22 suites   3d 12h 59m 53s ⏱️
 3 883 tests  3 881 ✅ 0 💤 2 ❌
76 026 runs  76 024 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit ce3c956.

♻️ This comment has been updated with latest results.

@guitargeek

Copy link
Copy Markdown
Contributor Author

Yes, I intend to merge this today because I think the new types and other added guardrails like the check that RooFit understands the whole JSON document with warnings if it doesn't are pretty useful also for your PRs!

And your PRs are mostly touching the RooFit-dependent side I think, which is not changes as much.

@guitargeek
guitargeek force-pushed the hs3-document branch 2 times, most recently from dc6f49e to d8bb5a2 Compare November 12, 2026 12:37
@guitargeek

Copy link
Copy Markdown
Contributor Author

Rebased on top of master to fix the conflicts with #24099

Objects that depend on each other made the import recurse until the
stack overflowed. Keep track of the objects that are being imported and
report the cycle instead.

Assisted-by: claude-code:claude-opus-5-5
An analysis that listed an unknown parameter of interest dereferenced a
null pointer.

Assisted-by: claude-code:claude-opus-5-5
Parsing and building RooFit objects are intertwined, so that malformed
documents are only noticed halfway through an import, and every
importer deals with JSON details on its own.

Add RooFit::HS3Document, which follows the structure of the standard and
doesn't depend on RooFit, and a parser that reports all problems with
their location in the document. The header stays private for now.

Distributions and functions are generic objects for the time being,
which are still interpreted by the registered importers.

Assisted-by: claude-code:claude-opus-5-5
Parse the whole document before anything is created in the workspace,
and use the typed representation for all top-level sections. Most of
the import state moves out of the public header. The exported JSON
doesn't change.

Assisted-by: claude-code:claude-opus-5-5
Parse HistFactory distributions into typed modifiers with a variant for
the constraint, and create all constraints in one place. What older
RooFit versions wrote differently, like integer interpolation codes,
"constraint_type", or implied Gaussian constraints, is rewritten into
the current form before parsing, for which JSONNode gets erase(). The
builder has no version checks anymore.

This fixes some imports that crashed or built the wrong model:
 - a "Gauss" constraint of a normfactor, whose nominal value is one
 - a sample without "modifiers", which lost its histogram
 - a shapesys or shapefactor with a single "parameter", as in the
   example of the standard, and a shapefactor without data
 - modifiers without name in different samples, which shared objects
   because they got the same generated name
 - a custom modifier that refers to an object later in the document

Objects of different samples or modifiers that would get the same name
are reported instead of being shared. A function as parameter of a
modifier is reported instead of corrupting memory, and a constant
without domain is no longer used as a variable, which was undefined
behaviour. Importers that users register for "histfactory_dist" still
have priority. The exported JSON doesn't change.

Assisted-by: claude-code:claude-opus-5-5
A component that RooFit doesn't know, like a misspelled key, was
silently ignored. Report it with its location, also for the objects that
are imported by the registered importers: the JSON tree tells a
JSONReadRecorder about each read of a map.

Older versions of RooFit wrote the binning of the axes of unbinned data,
which is now removed before parsing.

Assisted-by: claude-code:claude-opus-5-5
Check what doesn't depend on how the objects are built before anything
is created in the workspace: that names are unique, also with respect
to the parameters, that references can be resolved, that binnings are
valid, and that the sizes of arrays are consistent. Documents that fail
these checks are not imported.

Assisted-by: claude-code:claude-opus-5-5
Collect everything but the generic objects, which the exporters still
write directly, in an HS3Document, check it with the same validation as
on import, and write it in one place. HistFactory distributions are
exported as typed modifiers.

The exported JSON doesn't change, except that an empty workspace now
gets an empty array of default parameter values instead of null, and no
"domains": null.

Assisted-by: claude-code:claude-opus-5-5
Export RooGaussModel as "gauss_model_function" and RooTruthModel as
"truth_model_function", which the standard lists as functions, and
RooAddModel as "mixture_model". The former keys are still read, with a
deprecation warning.

Assisted-by: claude-code:claude-opus-5-5
The standard requires them for all continuous axes of binned data, also
if they have "edges". Bounds that don't agree with the edges are now
rejected, like axes with both "edges" and "nbins". Older documents
without them are still read. Generic formulas accept "min" and "max"
next to "edges", and write them like the other binned axes.

Assisted-by: claude-code:claude-opus-5-5
Map the "prior" of an analysis to the prior pdf of the ModelConfig.

Assisted-by: claude-code:claude-opus-5-5
Constraint terms of a channel that no modifier names in its
"constraints" were lost with a warning on export, and the export failed
if they depended on the parameter of a modifier. They are now exported
as "aux_distributions" of the likelihoods, which the standard adds to
the constraint terms of the prediction, and recorded in
misc.ROOT_internal.histfactory_constraints, which puts them back into
the channel on import.

A Gaussian of a normfactor parameter that is not a term of the channel,
like an external constraint or a prior, is no longer exported as the
constraint of the modifier.

Assisted-by: claude-code:claude-opus-5-5
The correlations of the uncertainties of binned data and the
uncertainties of the entries of unbinned data were dropped silently,
because RooDataHist and RooDataSet can't store them. Datasets of type
"point" get a clear error.

Assisted-by: claude-code:claude-opus-5-5
The document can't tell what RooFit makes of its objects. Parameters of
interest that name another object, or that are constants because no
domain lists them, and likelihood terms, priors, auxiliary distributions
or components of combined distributions that are functions in RooFit or
skipped made the import crash or were dropped silently. They are
reported now.

Assisted-by: claude-code:claude-opus-5-5
Only the tool uses it, and its header forward-declares it for the
_domains member already.

Assisted-by: claude-code:claude-opus-5-5
The ranges of the variables are now looked up with HS3::DomainRanges, which
doesn't depend on RooFit, and the domains are written directly as
HS3::Domain. Variables are created in the order of the document.

Assisted-by: claude-code:claude-opus-5-5
The exporter fills an HS3::HistFactoryDist instead of its own channel model.
Merging duplicate modifiers and choosing the default interpolation now work on
the document, so they don't depend on RooFit. Inconsistent binnings are
reported by the validation of the exported document.

Assisted-by: claude-code:claude-opus-5-5
The functions "interpolation" and "interpolation0d" are read into
HS3::InterpolatedFunction and HS3::InterpolatedNumber, which also handle the
legacy "interpolationCodes", and written from them. The upgrade of legacy
interpolation codes of modifiers shares the code.

Assisted-by: claude-code:claude-opus-5-5
- Import a single variable without writing and parsing a document for it.
- Read the axes of histograms with the parser instead of a second reader.
- Drop the branches for useListsInsteadOfDicts, which is always true.
- Check the size of channel-level staterror constraints in the validation
  instead of when building the model.

Assisted-by: claude-code:claude-opus-5-5
There is no Writer that shares the describe() functions. Most structs are read
with legacy keys and defaults that the writer doesn't mirror, so sharing them
would not save code.

Assisted-by: claude-code:claude-opus-5-5
@guitargeek
guitargeek merged commit 3682174 into root-project:master Nov 12, 2026
29 of 34 checks passed
@guitargeek
guitargeek deleted the hs3-document branch November 12, 2026 17:29
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.

3 participants