Skip to content

[RF][HS3] Small HS3 conformance fixes - #23840

Draft
cburgard wants to merge 22 commits into
root-project:masterfrom
cburgard:hs3-conformance-fixes
Draft

cburgard wants to merge 22 commits into
root-project:masterfrom
cburgard:hs3-conformance-fixes

Conversation

@cburgard

@cburgard cburgard commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

This Pull request:

Brings RooFit's HS3 import/export closer to the HS3 standard (hep-statistics-serialization-standard
master at cb3e9eb, v0.2.9 → v0.3) with small, independent fixes: renamed type keys, added validation and
warnings, and changed defaults. It also moves HistFactory constraints to the form of HS3 PRs
#124 and #129, which this PR assumes to be merged: constraint distributions
referenced by name. Larger structural items (categorical axes, the expression language, the new histfactory
function of #129) are not touched here; they will be tracked in a separate ROOT issue (to be opened).

Background: the HS3–RooFit compatibility audit (October 2026)
of RooFit's HS3 layer against HS3 cb3e9eb (findings also tracked in HS3 issues #95 "Inconsistencies between ROOT HS3 export and spec",
#99 "Potential spec deviations in HS3TestSuite" and #133 "Checklist: RooFit HS3
types/fields not yet in the standard"). Related ROOT PR: #23839
(pinned HS3TestSuite with an expected-failure list).

The PR has three groups of commits:

  • Group I (first 16 commits) is independent of pending HS3 changes and can be merged now.
  • Group II (next 3 commits, labelled [after HS3 json-fixes]) follows HS3 PR
    #135 "Small normative JSON fixes for v0.3" (branch json-fixes-0.3).
  • Group III (last 3 commits, labelled [after HS3 #124/#129]) follows HS3 PRs #124
    "Histfactory Specfication Sync" and #129 "Move HistFactory to an appendix and add the
    histfactory function".

If the HS3 PRs are delayed, Group I (or Groups I and II) can be merged on their own.

Each commit builds on its own and passes testRooFitHS3 and testJSONInterface.

Pre-flight findings

Behaviour on master before this PR:

  1. HistFactory modifier constraints on export (exportChannel() in JSONFactories_HistFactory.cxx): RooFit
    writes "constraint": "<name of the constraint distribution>" (e.g. "alpha_syst1Constraint") and exports that
    distribution separately. It does not write constraint_name / constraint_type (ROOT 6.38 still wrote
    constraint_name). On import, constraint is first resolved as a distribution name and then as a legacy type
    (Gauss, ...). Groups I and II keep this (Group I only adds the staterror constraint type of the current
    standard); Group III replaces it by the parameters/constraints lists of HS3 Add support for abi-tags (adapted from Fedora's patch) #124.
  2. Infinite domain bounds (Domains.cxx, ProductDomain::writeJSON()): RooFit writes infinite bounds as JSON
    null. The axis itself is always written; there is no "skip the axis entirely" shortcut. Group II item 3
    therefore changes null to an omitted key.

Changes or fixes:

Group I: independent of pending HS3 changes

# Item File(s) Change Test
1 Version string RooJSONFactoryWSTool.cxx, JSONIOUtils.cxx hs3_version must be a string. Parse MAJOR.MINOR, and warn if it is newer than the supported version or can't be parsed. The export value stays a single constant. The version of the document being imported is available via RooJSONFactoryWSTool::hs3VersionInput(). VersionStringHandling
2 Duplicate names RooJSONFactoryWSTool.cxx Throw on names duplicated within or across distributions, functions, data, domains, parameter_points. A clash involving likelihoods or analyses only warns (legacy ROOT files, see below). Export: likelihoods are named <analysis>_likelihood, and an analysis without dataset is named after its ModelConfig. DuplicateNamesAreRejected
3 default_domain optional Domains.cxx Without default_domain, variables take the range of the first domain that mentions them. Still written on export. DefaultDomainIsOptional
4 Parameter-point-only parameters RooJSONFactoryWSTool.cxx Create a RooRealVar, constant unless a domain lists it (an explicit const wins). String value sets a RooCategory of that name; otherwise it is ignored with a warning. ParametersOnlyInParameterPoints
5 Unbinned axes RooJSONFactoryWSTool.cxx Unbinned data axes carry only name/min/max. UnbinnedDatasetAxesHaveNoBinning
6 init in analyses RooJSONFactoryWSTool.cxx Export the ModelConfig snapshot as analyses[].init. Import re-creates it under the same name (stable round trips), and warns if the parameter point doesn't exist. AnalysisInitRoundTrip
7 Type keys JSONFactories_RooFitCore.cxx, RooFitHS3_wsexportkeys.cxx, RooFitHS3_wsfactoryexpressions.cxx argus_dist (ARGUS_dist deprecated alias); binsampling_dist for export and import (binsampling deprecated alias); double-sided RooCrystalBall → crystalball_dist; rate_extended_dist uses rate/dist (norm/pdf accepted). generic/generic_dist were already conformant. TypeKeysMatchTheCatalog
8 ifelse JSONFactories_RooFitCore.cxx Formula exporter rewrites c ? a : b (nested, in parentheses) to ifelse(c,a,b); it warns and leaves the expression unchanged if that isn't safely possible. Also translates ifelse back on import, because TFormula doesn't understand it. TernaryIsExportedAsIfElse
9 mixture_dist.extended JSONFactories_RooFitCore.cxx Check an explicit extended against the coefficient count; throw on contradictions. MixtureDistExtendedFlag
10a LogNormal, normfactor parameter JSONFactories_HistFactory.cxx Accept LogNormal (keep Lognormal). The normfactor parameter is named after parameter; name is the label only. HistFactoryNormFactorParameterAndLogNormal
10b Missing constraint JSONFactories_HistFactory.cxx normsys/histosys without a constraint key are unconstrained (info message). Export accepts unconstrained modifiers instead of throwing "cannot find constraint". HistFactoryModifierWithoutConstraintIsUnconstrained
10c staterror constraint JSONFactories_HistFactory.cxx Read constraint (Poisson/Gauss) from the staterror modifier, falling back to the channel-level staterror.constraint_type. Export writes both. (Superseded on export by Group III; still read.) HistFactoryStatErrorConstraint
10d Missing interpolation JSONFactories_HistFactory.cxx Throw if a modifier has no interpolation and there is no default_interpolation, for hs3_version ≥ 0.3. Older documents keep code 4, with one deprecation warning per channel. Export already always writes default_interpolation when needed (verified, unchanged). HistFactoryMissingInterpolation
11 Bounds checks JSONFactories_RooFitCore.cxx, RooJSONFactoryWSTool.cxx, JSONIOUtils.cxx Square covariance/correlation matrices; errors length = contents length; edges has ≥ 2 strictly ascending values. MalformedArraysAreRejected
12 Unbinned axis ranges RooJSONFactoryWSTool.cxx An observable without a domain range takes min/max from the axis; a disagreeing domain range is kept with a warning. UnbinnedAxisRangesOnImport
13 Developer docs roofit/doc/developers/roofit_hs3.md Embedded catalogs instead of etc/*.json; importArg() as primary API; std::unique_ptr / template registration. The snippets were compile-checked against the current headers. —

Deviations from the original plan, and the reasons:

  • Item 2: RooFit's own files name the likelihood (and, without dataset, the analysis) after the analysis or
    pdf. Throwing on these would make every file written by an existing ROOT release unreadable. Analyses aren't
    referenced by name, and likelihoods are only looked up from analyses, so these clashes only warn. New exports
    don't produce them.
  • Item 7, Crystal Ball: the symmetric field set (alpha, n) is what RooFit already uses for the
    single-sided RooCBShape, and RooFit imports it back as RooCBShape. A symmetric RooCrystalBall is
    therefore always exported with alpha_L/n_L/alpha_R/n_R, both sides pointing to the same parameter. This
    is valid HS3 and round-trips exactly. The width is written as sigma if shared, otherwise as
    sigma_L/sigma_R. Single-sided RooCrystalBall can't be expressed as crystalball_dist and keeps the
    previous export (crystalball_doublesided_dist).
  • Item 8: ifelse → ternary translation on import was added, since otherwise RooFit couldn't read its own
    output.
  • Item 9: RooAddPdf can't be forced to be non-extendable. extended: false with one coefficient per summand
    (valid HS3) therefore only warns; the shape is the same.
  • Item 10d: ROOT ≤ 6.38 never wrote the interpolation of histosys modifiers (or of normsys modifiers with
    code 4), nor default_interpolation. Throwing unconditionally would make all HistFactory JSON from released ROOT
    versions unreadable, so the strict behaviour is gated on hs3_version ≥ 0.3. Objects imported without a
    document (importJSONElement) are treated as legacy.
  • Item 5: a category observable never reaches the unbinned writer (datasets with a category go through the
    combined-data path), so no categories branch was added.

Group II: after HS3 PR #135

Checked against HS3 PR #135 at head 0e5d5a3 (2026-10-26); the only change after the originally checked
bebff05 is a reformatting of the Crystal Ball section. Re-check this table if the PR changes before merging.

# Item Change
1 Version value Export hs3_version: "0.3".
2 Analysis keys Export already wrote parameters_of_interest and domains (with NP/GO/POI domains as entries). Now also import the legacy parameter_of_interest and domain (a string), with a deprecation warning. Test: AnalysisLegacySingularKeys.
3 Infinite bounds Omit infinite min/max of domain axes instead of writing null, and write a default binning only for bounded axes (the HS3 PR requires both bounds for nbins). Name-only axes are kept (valid per the HS3 PR); RooFit needs them to tell a RooRealVar from a RooConstVar. null is still read.
4 Irregular axes No change: the HS3 PR makes min/max optional with edges.
5 HistFactory errors No change: the HS3 PR makes errors optional.

Group III: after HS3 PRs #124 and #129

Checked against #124 at head bc5f670 and #129 at head e76fc4a.

# Item File(s) Change Test
1 Constraints by name JSONFactories_HistFactory.cxx Import parameters and constraints (names of distributions, null = unconstrained) for all modifiers; they take precedence over the legacy parameter, constraint, constraint_name and constraint_type, which are still read. staterror uses the named constraints instead of creating Poisson terms; without any constraint information its gammas are unconstrained for hs3_version ≥ 0.3 (older documents get the implicit Poisson terms as before). Export writes parameters/constraints and exports the constraint distributions; the staterror RooPoisson terms become named poisson_dist. parameter, constraint, shapesys data.vals and the channel-level staterror are no longer written. HistFactoryConstraintsByName, updated HistFactoryStatErrorConstraint, HistFactoryConstraintKeyMigration, HistFactoryLegacyConstraintTypeInConstraintKey, HistFactoryModifierWithoutConstraintIsUnconstrained
2 Axis range RooJSONFactoryWSTool.cxx Binned axes written via exportAxis() always carry min/max, also with edges. #124 requires them on histfactory_dist axes; #135 allows them if they equal the first and last edge. IrregularBinnedAxesHaveRange
3 Tutorial tutorials/roofit/roofit/rf515_hfJSON.json Modifiers use parameters/constraints; the formerly implicit constraints are explicit gaussian_dist (mean 0, sigma 1) and poisson_dist (staterror, tau 400/100) entries; hs3_version 0.3. fitted from C++, see below

Notes on Group III:

Behaviour changes users may notice

  • normsys/histosys without constraint are no longer silently Gaussian-constrained. Hand-written files
    that relied on this must add "constraint": "Gauss" (Group I) or, in the form of HS3 Add support for abi-tags (adapted from Fedora's patch) #124, reference a
    constraint distribution in constraints (Group III). The rf515_hfJSON tutorial JSON was updated accordingly.
    After Group I its fit result is identical to master (min NLL −1043.45938119, same parameters). After Group III
    (explicit, named constraints) its likelihood equals the old one up to an additive constant of 1.3e-5, checked at
    several parameter points; the fit agrees within the minimizer tolerance (mu 1.10955 vs. 1.10947).
  • Missing interpolation now throws for documents with hs3_version ≥ 0.3. Older documents get a deprecation
    warning and the old code-4 default.
  • Type keys renamed (with import aliases): ARGUS_dist → argus_dist, binsampling → binsampling_dist,
    crystalball_doublesided_dist → crystalball_dist (double-sided), rate_extended_dist fields pdf/norm →
    dist/rate.
  • Exported likelihoods are named <analysis>_likelihood; analyses without dataset are named after the
    ModelConfig.
  • Files with duplicate object names, malformed matrices, mismatched errors or invalid edges are now rejected.
  • Exported formulas use ifelse() instead of ?:.
  • Group II: hs3_version is "0.3", and infinite domain bounds are omitted instead of null.
  • Group III: HistFactory modifiers are written with parameters/constraints and named constraint
    distributions (including poisson_dist terms for staterror); shapesys data.vals and the channel-level
    staterror are gone. In documents with hs3_version ≥ 0.3, a staterror without constraints is unconstrained.
    Binned axes with edges also carry min/max.

Changed tests and reference data

No reference JSON files exist for these tests; expectations are inline. The following existing expectations or
fixtures were changed because of intended behaviour changes:

  • HistFactoryIncompatibleDuplicateModifiers/Constraint: the two modifiers now request "constraint": "Gauss"
    explicitly, because the test needs two different constraint terms (previously these were implicit).
  • RooGaussianConstVarSigmaExport, ProductDomainEntriesExportExplicitBounds (Group II.3): expect omitted
    instead of null infinite bounds. The second test now also checks that null is still read.
  • tutorials/roofit/roofit/rf515_hfJSON.json: explicit "constraint": "Gauss" on the three normsys, and a
    default_interpolation (Group I); rewritten with named constraint distributions (Group III).
  • HistFactoryConstraintKeyMigration, HistFactoryLegacyConstraintTypeInConstraintKey,
    HistFactoryModifierWithoutConstraintIsUnconstrained, HistFactoryStatErrorConstraint (Group III): expect the
    exported constraints list instead of constraint.

HistFactory round trip

roofit/histfactory/test/ref_6.16_example_UsingC_combined_meas_model.root (workspace combined), exported with
master and with this branch. Every changed key:

  • analyses[simPdf_obsData].likelihood: "simPdf_obsData" → "simPdf_obsData_likelihood" (same for
    simPdf_asimovData)
  • likelihoods[simPdf_obsData] → likelihoods[simPdf_obsData_likelihood] (same for simPdf_asimovData),
    content unchanged
  • distributions[model_channel1].samples[background1_…].modifiers[staterror].constraint: absent → "Poisson"
    (same for background2_…)
  • distributions[model_channel1].staterror.constraint_type: absent → "Poisson"
  • Group II only: metadata.hs3_version: "0.2" → "0.3"
  • Group III only: on every modifier, parameter → parameters (list) and constraint → constraints (list of
    names; for staterror the names of the gamma constraints instead of "Poisson"); the channel-level
    staterror.constraint_type is gone; new distributions gamma_stat_channel1_bin_{0,1}_constraint
    (poisson_dist), functions gamma_stat_channel1_bin_{0,1}_poisMean (product) and their constant parameters
    gamma_stat_channel1_bin_{0,1}_tau and nom_gamma_stat_channel1_bin_{0,1}.

A second export after re-importing differs from the first in the same keys on master and on this branch
(legacy binWidth_* variables of the 6.16 workspace, misc.minimization.parameter_stepwidths). That is
pre-existing and not touched here.

Checklist:

  • tested changes locally
  • updated the docs (if necessary)

Tests run (Linux, GCC 13, RelWithDebInfo, -Droofit=ON -Dtesting=ON):
ctest -R 'RooFitHS3|JSONInterface': baseline 71 + 1 tests pass; with this PR 89 + 1 tests pass (18 new). The
two test binaries were also built and run for every single commit.

Not run: the PyROOT tests (bindings/pyroot/pythonizations/test/roofit.py, which uses HistFactory JSON via
importJSONElement), the rf515_hfJSON.py tutorial itself (its model was fitted from C++ instead), and the
optional HS3 test suite (test_roofit_hs3testsuite). My build has pyroot=OFF.

AI assistance: Claude Code (claude-opus-5-5) implemented the changes, the tests and this description from a
written plan, under review of the author. The commits carry Assisted-by: claude-code:claude-opus-5-5.

🤖 Generated with Claude Code

The importer only checked that `metadata.hs3_version` exists. It now also
requires it to be a string, parses the leading `MAJOR.MINOR`, and warns if
the document is newer than the version RooFit writes (or if the string
cannot be parsed). The exported version remains a single constant.

The parsed version is kept available to importers during the import via
`RooJSONFactoryWSTool::hs3VersionInput()`.

Assisted-by: claude-code:claude-opus-5-5
HS3 requires the names of all objects in a document to be unique. The
importer built its function/distribution indices with `emplace`, so a
duplicate silently resolved to the first definition. Duplicated names
within or across `distributions`, `functions`, `data`, `domains` and
`parameter_points` are now an error.

RooFit itself named likelihoods after their analysis, and analyses
without matching dataset after the pdf, so its own files violate this
rule. On export, likelihoods are now called `<analysis>_likelihood`, and an
analysis without dataset is named after its ModelConfig. On import, a name
clash that involves `likelihoods` or `analyses` only triggers a warning,
so that files written by older ROOT versions can still be read. This is
unambiguous: analyses are not referenced by name, and likelihoods are only
looked up from analyses.

Assisted-by: claude-code:claude-opus-5-5
The importer required a domain named exactly `default_domain`, which is not
part of the HS3 standard. If it is absent, variables are now created with
the range of the first domain that mentions them (or RooFit's default if
none does). Export still writes `default_domain`, and documents that contain
it are imported exactly as before.

Assisted-by: claude-code:claude-opus-5-5
A parameter listed in a parameter point that was not created by any
distribution, function or domain used to be ignored silently. It is now
created as a `RooRealVar`, constant unless a domain lists it (an explicit
`const` flag takes precedence).

A string `value` is accepted for categorical parameters if a `RooCategory` of
that name exists; otherwise it is ignored with a warning instead of being
read as a number.

Assisted-by: claude-code:claude-opus-5-5
HS3 forbids `nbins` and `edges` on the axes of unbinned data, but the
unbinned-data exporter used the shared binning writer. It now writes only
`name`, `min` and `max` for each axis. The binning of the observables is
still preserved in the domains.

Assisted-by: claude-code:claude-opus-5-5
On export, if a ModelConfig has a snapshot, the analysis now references the
parameter point of that snapshot as `init`. On import, the snapshot is
re-created under the same name so that repeated round trips are stable, and
an `init` that refers to a parameter point that doesn't exist is reported
with a warning instead of being skipped silently. (All parameter points are
imported before the analyses, so their order in the file does not matter.)

Assisted-by: claude-code:claude-opus-5-5
* RooArgusBG is exported as `argus_dist`. `ARGUS_dist` is still imported,
  with a one-time deprecation warning.
* RooBinSamplingPdf is exported as `binsampling_dist`, the key its importer
  was registered for, so it can be read back. `binsampling` is still
  imported, with a one-time deprecation warning.
* Double-sided RooCrystalBall shapes are exported as `crystalball_dist`.
  The width is written as `sigma` if both sides share it, otherwise as
  `sigma_L`/`sigma_R`. The tails are always written as
  `alpha_L`/`n_L`/`alpha_R`/`n_R`, also when both sides share their
  parameters, because RooFit reads the symmetric `alpha`/`n` form back as a
  single-sided RooCBShape. On import, a `crystalball_dist` with the
  asymmetric tail or width fields creates a RooCrystalBall. Single-sided
  RooCrystalBall shapes and `crystalball_doublesided_dist` are unchanged.
* `rate_extended_dist` uses the HS3 fields `rate` and `dist`. `norm` and
  `pdf` are still accepted on import.

RooFormulaVar and RooGenericPdf were already exported as `generic` and
`generic_dist`, with `generic_function` still importable, so they needed
no change.

Assisted-by: claude-code:claude-opus-5-5
HS3 expresses conditionals as `ifelse(condition, a, b)`, but RooFit wrote
the C-style ternary `c ? a : b` that TFormula uses. The generic formula
exporter now rewrites ternaries at any nesting level into `ifelse()`. If an
expression can't be rewritten safely, a warning is printed and the
expression is exported unchanged.

TFormula doesn't understand `ifelse()`, so on import it is translated back
into a parenthesized ternary. Without this, RooFit could not read its own
output. Expressions without ternaries are exported unchanged.

Assisted-by: claude-code:claude-opus-5-5
The `extended` field of `mixture_dist` was ignored on import; the RooAddPdf
derived its extended mode from the number of coefficients only. If the
flag is present, it is now checked against the coefficient count:

* `extended: true` with one coefficient fewer than summands is an error
  (HS3 requires such a mixture to be non-extended);
* `extended: false` without coefficients is an error (RooFit would build an
  extended sum of extended distributions);
* `extended: false` with one coefficient per summand is valid HS3, but a
  RooAddPdf cannot be forced to be non-extendable, so a warning advises not
  to use the extended term when fitting.

Assisted-by: claude-code:claude-opus-5-5
…factors

* HS3 spells the log-normal constraint type `LogNormal`. It is now
  recognized as a constraint type like the older RooFit spelling
  `Lognormal`, which is kept as an alias.
* For a `normfactor` that has both `name` and `parameter`, the RooRealVar
  was named after `name`, breaking the correlation of normfactors that share
  a `parameter` but have different labels. The parameter is now named after
  `parameter`; `name` is only the label of the modifier.

Assisted-by: claude-code:claude-opus-5-5
…trained

In HS3, a modifier without `constraint` is unconstrained. RooFit instead
created a unit Gaussian constraint for `normsys` and `histosys` modifiers
that had none of `constraint`, `constraint_name` or `constraint_type`. Now
no constraint is created and an info message is printed. The `nom_*`
global observable is only created for constrained modifiers.

On export, a non-constant normsys/histosys parameter without a recognized
constraint used to be an error. Such a modifier is now exported without
`constraint`, as long as no other constraint term of the channel depends
on its parameter (that case is still an error, because the constraint is
not supported). This keeps RooFit able to write the models it reads.

The rf515_hfJSON tutorial relied on the implicit Gaussian constraints; its
normsys modifiers now request them with `"constraint": "Gauss"`, so the
tutorial model is unchanged. For the same reason, the "Constraint" case of
the HistFactoryIncompatibleDuplicateModifiers test now requests its two
different constraint terms explicitly.

Assisted-by: claude-code:claude-opus-5-5
… modifier

HS3 gives the constraint type of a `staterror` on the modifier (`Poisson`
or `Gauss`), but RooFit ignored it and only read the private channel-level
field `histfactory_dist.staterror.constraint_type`, defaulting to Poisson.
The export wrote neither, so the choice between Gaussian and Poisson stat
constraints was lost.

On import, the `constraint` of the staterror modifier is now used, with the
channel-level field as fallback. On export, the constraint type is written
both on the staterror modifiers and on the channel level, so that the files
can still be read by older ROOT versions.

Assisted-by: claude-code:claude-opus-5-5
…>= 0.3

HS3 requires `default_interpolation` if a modifier has no `interpolation`,
and forbids implementation-specific defaults. RooFit silently fell back to
code 4. Documents with `hs3_version` 0.3 or newer that rely on this are now
rejected.

ROOT 6.38 and earlier never wrote the interpolation of histosys modifiers
(or of normsys modifiers with the default code 4), nor a
`default_interpolation`. To keep these files readable, documents with an
older `hs3_version` (and objects imported without a document, e.g. with
`importJSONElement()`) still get code 4, with one deprecation warning per
channel.

The export already writes `default_interpolation` whenever a modifier
omits `interpolation`, so RooFit doesn't produce such documents. The
rf515_hfJSON tutorial now states its interpolation explicitly with a
`default_interpolation`, which avoids the deprecation warning.

Assisted-by: claude-code:claude-opus-5-5
Malformed input could be read out of bounds:

* the rows of `multivariate_normal_dist` covariance and correlation
  matrices are now checked to form a square matrix of the right size;
* the `errors` of binned data must have as many entries as `contents`;
* the `edges` of binned axes must contain at least two strictly ascending
  values.

Assisted-by: claude-code:claude-opus-5-5
The `min`/`max` of the axes of unbinned data were ignored on import; the
observables kept whatever range they had, and entries outside of it were
clipped. If an observable got no range from a domain, its range is now set
from the axis. If a domain range disagrees with the axis, it is kept and a
warning is printed.

Assisted-by: claude-code:claude-opus-5-5
* The export keys and factory expressions are no longer central JSON files
  in `etc/`, but catalogs embedded in `roofit/hs3/src/RooFitHS3_wsexportkeys.cxx`
  and `roofit/hs3/src/RooFitHS3_wsfactoryexpressions.cxx`. Point to them,
  and explain how to add entries there or load additional catalogs at
  runtime.
* Describe `importArg()` as the function to override, with
  `importFunction()`/`importPdf()` as compatibility shims.
* Register importers and exporters with the template helpers or with
  `std::unique_ptr`, as the current API requires, instead of raw `new`.

Assisted-by: claude-code:claude-opus-5-5
HS3 PR "json-fixes-0.3" defines `hs3_version` as a `"MAJOR.MINOR"` string
and the version of the revised standard as `"0.3"`. RooFit now writes this
version. Documents with a newer version are imported with a warning.

This commit depends on the HS3 PR and should only be merged together with
or after it.

Assisted-by: claude-code:claude-opus-5-5
HS3 PR "json-fixes-0.3" renames the analysis keys to
`parameters_of_interest` and `domains` (a list of domain names), which is
what RooFit already writes, including one entry each for the nuisance
parameter, global observable and parameter-of-interest domains.

The keys of HS3 v0.2.9, `parameter_of_interest` and `domain` (a single
name), are now also read, with a deprecation warning. The plural keys take
precedence.

Assisted-by: claude-code:claude-opus-5-5
…f writing null

RooFit wrote infinite bounds of product-domain axes as JSON `null`, which
HS3 doesn't allow. HS3 PR "json-fixes-0.3" makes `min` and `max` of domain
axes optional, with an absent bound meaning an unbounded range. Infinite
bounds are now omitted on export. Since that PR also requires both bounds
for a binning, the default binning is only written for bounded axes.

Axes without any bound are still written (with only their `name`), which
the HS3 PR keeps valid. RooFit needs them to tell an unbounded RooRealVar
from a RooConstVar. On import, an absent bound means -inf/+inf, and `null`
is still accepted for files written by older ROOT versions.

The RooGaussianConstVarSigmaExport and ProductDomainEntriesExportExplicitBounds
tests are updated to expect omitted instead of null bounds.

Assisted-by: claude-code:claude-opus-5-5
…eference constraints by name

HS3 PR root-project#124 replaces the `parameter` and `constraint` (a constraint type
like `Gauss`) of HistFactory modifiers with the lists `parameters` and
`constraints`. Each entry of `constraints` names a distribution in the
top-level `distributions`, and `null` leaves a parameter unconstrained.
`shapesys` loses its `data.vals`, since the uncertainties are encoded in
the constraint distributions. HS3 PR root-project#129 states that the constraint terms
belong to the prediction, which is how RooFit already builds the model.

Import:
* `parameters` and `constraints` are read for all modifiers, and take
  precedence over the legacy `parameter`, `constraint`, `constraint_name`
  and `constraint_type`, which are still read for older files.
* For `staterror`, the named constraint distributions are used instead of
  creating Poisson terms. Without any constraint information, the gammas
  are unconstrained in documents with `hs3_version` >= 0.3; older documents
  still get the Poisson constraints that RooFit used to create implicitly.
* The `nom_*` global observable is now only created when RooFit builds a
  constraint itself (legacy `Gauss`).

Export:
* All modifiers write `parameters` and, if any parameter is constrained,
  `constraints` with the names of the constraint distributions, which are
  exported alongside. `parameter`, `constraint` and `shapesys` `data.vals`
  are no longer written.
* The RooPoisson (or RooGaussian) constraint terms of the staterror gammas
  are exported as named `poisson_dist` (or `gaussian_dist`) and referenced
  from the staterror modifiers. root-project#124 recommends a
  `barlow_beeston_lite_poisson_constraint_dist`, which RooFit doesn't
  support yet; any distribution type is allowed.
* The channel-level `staterror.constraint_type` and the `constraint` type
  on staterror modifiers (written since the previous HistFactory commits)
  are no longer written, but still read.

Tests that checked the exported `constraint` key are updated to the new
form, and a new test covers import and export by name.

This commit depends on HS3 PRs root-project#124 and root-project#129 and should only be merged
together with or after them.

Assisted-by: claude-code:claude-opus-5-5
…max on irregular binned axes

RooFit wrote only `edges` for binned axes with an irregular binning. HS3
PR root-project#124 requires `name`, `min`, `max` and exactly one of `nbins` or
`edges` on the axes of a `histfactory_dist`, while HS3 PR root-project#135 makes
`min`/`max` optional next to `edges`, but allows them if they equal the
first and last edge. Writing them satisfies both, so binned axes written
via `exportAxis()` (binned data, histograms, ParamHistFunc and HistFactory
axes) now always carry `min` and `max`. The `axes` of generic formulas are
unchanged, because their reader requires either `edges` or
`min`/`max`/`nbins`.

Assisted-by: claude-code:claude-opus-5-5
… named constraints in rf515_hfJSON

Write the HistFactory modifiers of the rf515_hfJSON tutorial in the form
of HS3 PR root-project#124: every modifier names its parameters in `parameters`, and
its constraint distributions by name in `constraints`. The constraint
terms that RooFit used to create implicitly are now explicit:

* unit Gaussians (mean 0, sigma 1) for the normsys parameters, as
  recommended by root-project#124;
* Poisson terms for the two staterror gammas, with the tau values
  (400 and 100) that follow from the sample errors. root-project#124 recommends a
  `barlow_beeston_lite_poisson_constraint_dist`, which RooFit doesn't
  support yet, so `poisson_dist` is used.

The gammas are declared in a domain and in the default parameter point, so
that the functions referring to them can be imported. The document now
declares `hs3_version` 0.3.

The likelihood is the same as before up to an additive constant of
1.3e-5 (checked at several parameter points), and the fit result agrees
within the minimizer tolerance.

Assisted-by: claude-code:claude-opus-5-5
@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 22h 31m 33s ⏱️
 3 896 tests  2 787 ✅   0 💤 1 109 ❌
79 902 runs  78 358 ✅ 435 💤 1 109 ❌

For more details on these failures, see this check.

Results for commit 625d361.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant