Skip to content

[RF][HS3] Bring the HS3 import and export closer to the HS3 standard - #23996

Merged
guitargeek merged 35 commits into
root-project:masterfrom
cburgard:hs3-conformance-fixes-independent
Nov 5, 2026
Merged

guitargeek merged 35 commits into
root-project:masterfrom
cburgard:hs3-conformance-fixes-independent

Conversation

@cburgard

@cburgard cburgard commented Nov 5, 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) with small, independent fixes: renamed type keys, added validation and warnings, and
changed defaults. All changes follow the HS3 standard as it is merged today; none depend on pending HS3 PRs.

These are the first 19 commits of #23840 ("Group I"), split
off so that they can be merged now. #23840 stays in draft with the parts that follow
pending HS3 PRs (#135, #124), and will be rebased on this PR.

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).

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

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, ...). This PR keeps this and only adds the staterror constraint type of the current standard.

Changes or fixes:

# 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. It is constant if "const": true or if no domain lists it (there is no range to vary it in); "const": false is the same as omitting const. This is valid under the current standard (such a parameter must be listed in a domain or be const) and matches HS3 PR #131. 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); rate_extended_dist uses rate/dist (norm/pdf accepted). generic/generic_dist were already conformant. The Crystal Ball keys are not changed here: they follow a pending change of HS3 PR #135, see #23840. 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. (To be superseded on export by the named constraints of HS3 PR #124, see #23840.) 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. —
14 Zero errors in binned data RooJSONFactoryWSTool.cxx An error of 0 before the first positive error of binned data was read as squared weight = content, because RooDataHist initializes the squared weights from the weights when it first sees a positive error. That bin is now set first, so errors of 0 stay 0. BinnedDataZeroErrors
15 RooConstVar vs. constant RooRealVar (test only) A RooRealVar that is constant in all parameter points and a RooConstVar are written the same way in the parameter points (a value and "const": true); only the domains tell them apart. The new test checks this, the restored types and constness (also from snapshots), and that the round trip is stable. ConstRooRealVarStaysDistinctFromRooConstVar
16 Formatting tutorials/roofit/roofit/rf515_hfJSON.json The code-formatting check also covers JSON files; the tutorial JSON changed here is now formatted with clang-format. Whitespace only, the parsed content is unchanged. —

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 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.

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". The rf515_hfJSON tutorial JSON was updated accordingly;
    its fit result is identical to master (min NLL −1043.45938119, same parameters).
  • 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,
    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 ?:.
  • Errors of 0 in binned data are read as 0 also before the first positive error (they used to become the
    bin content as squared weight).

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).
  • tutorials/roofit/roofit/rf515_hfJSON.json: explicit "constraint": "Gauss" on the three normsys, and a
    default_interpolation.

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"

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, Ubuntu 26.04, GCC 15.2, RelWithDebInfo, -Droofit=ON -Dtesting=ON):
ctest -R 'RooFitHS3|JSONInterface': baseline 71 + 1 tests pass; with this PR 88 + 1 tests pass (17 new). The
two test binaries and testHistFactory (69 tests) 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
@cburgard

cburgard commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @guitargeek , this PR factors out a few of the upcoming changes to the HS3 import/export that will be needed for HS3 milestone 0.3.
Most of the changes are fairly trivial.
I would appreciate a speedy review.

@cburgard
cburgard force-pushed the hs3-conformance-fixes-independent branch from ce4ffc6 to 2b083bb Compare November 5, 2026 12:35
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`. It is constant if `"const": true` or if no domain
lists it, since there is no range to vary it in; `"const": false` is the same
as omitting `const`.

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.
* `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. The Crystal Ball keys are left unchanged here; they follow a
pending change of the HS3 standard.

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
…e error

RooDataHist only starts to store squared weights when a bin is set with a
positive error, and then initializes them from the weights of the bins set
so far. So for "errors": [0, 2], the first bin got a squared weight equal to
its content instead of 0. The bin with the first positive error is now set
first.

Assisted-by: claude-code:claude-opus-5-5
@cburgard
cburgard force-pushed the hs3-conformance-fixes-independent branch from 2b083bb to 5092e2b Compare November 5, 2026 12:45
…nstVar

A RooRealVar that is constant in all parameter points and a RooConstVar are
written the same way in the parameter points (a value and "const": true).
Only the domains tell them apart: the RooRealVar is listed in a domain, the
RooConstVar is not, and a parameter without domain is constant and read
back as a RooConstVar. The test checks this, the restored types and
constness (also from snapshots), and that the round trip is stable.

Assisted-by: claude-code:claude-opus-5-5
The code-formatting check also covers JSON files, and the tutorial JSON
changed in this PR didn't follow the clang-format style. Whitespace only:
the parsed JSON content is unchanged.

Assisted-by: claude-code:claude-opus-5-5
Pdfs that share a parameter, like two RooExponentials with one slope,
each wrote the same helper function. The duplicate-name check then
rejected the file on import.

Assisted-by: claude-code:claude-opus-5-5
ModelConfigs sharing a pdf and a dataset, like the usual S+B and B-only
pair, got the same analysis name. Only the last one survived, and its
domains were written twice, which the duplicate-name check rejects.

Assisted-by: claude-code:claude-opus-5-5
RooFit still writes clashing names, for example for a named range and a
pdf of the same name, and it labels its files as older than 0.3.
Throwing on these made it unable to read its own output, so they only
get a warning now, like a missing interpolation.

Assisted-by: claude-code:claude-opus-5-5
The exporter writes infinite bounds as null, but the new range handling
for unbinned axes called val_double() on them. Files with an unbounded
observable could not be read anymore.

Assisted-by: claude-code:claude-opus-5-5
Zero coefficients for one summand also satisfied the check for "one
coefficient fewer than summands", so RooFit rejected its own export of a
RooAddPdf with a single extended pdf.

Assisted-by: claude-code:claude-opus-5-5
parseOverallModifierFormula() requires a constraint but didn't store
it, which only worked because the importer implied a Gaussian. Now that
a missing constraint means unconstrained, the export threw instead,
claiming that the RooGaussian is not supported.

Assisted-by: claude-code:claude-opus-5-5
A formula like 1+0.1*alpha was exported with hi=0.1 and lo=-0.1, but the
factors of a normsys are absolute: hi=1.1 and lo=0.9. The re-imported
model scaled the sample by 0.1 at alpha=1, and evaluated to NaN for
alpha<0.

Assisted-by: claude-code:claude-opus-5-5
GetSnapshot() was only called as a null check, but it loads the snapshot
and restores nothing but the values. After exportJSON(), a fixed
parameter was floating and had lost its error and step width.

Scanning the snapshot names also dropped or mixed up "init" for
ModelConfigs with a common name prefix, like ModelConfig and
ModelConfig_with_poi_0, and after several SetSnapshot() calls.

Assisted-by: claude-code:claude-opus-5-5
AddLogNormSyst() and AddGammaSyst() of HistFactory::Measurement use a
function like alphaOfBeta_x as normsys parameter. The check for
unsupported constraints missed this case, so it was exported as a free
parameter and the re-imported model silently differed.

Assisted-by: claude-code:claude-opus-5-5
The former HistFactory::JSONTool and the old rf515 tutorial file write
normsys and histosys modifiers without constraint and with version
0.1.90. These models rely on the implicit unit Gaussian, so dropping it
silently changed their fits. Objects imported without a document also
keep it.

Assisted-by: claude-code:claude-opus-5-5
Older RooFit versions constrained these parameters, so a floating one
might not be what the author of the document meant. Warn once per
channel instead of writing an info message.

Assisted-by: claude-code:claude-opus-5-5
createPHF() turned everything except "Poisson" and "Const" into a
Gaussian. A shapesys with "constraint": "LogNormal", the HS3 spelling,
therefore silently got the wrong constraint instead of an error. The
older spelling "Lognormal" did the same.

Assisted-by: claude-code:claude-opus-5-5
ROOT 6.38 and 6.40.00 wrote this key instead of "constraint", so their
Gaussian stat errors still came back as Poisson.

Assisted-by: claude-code:claude-opus-5-5
Without any positive error, RooDataHist never started to store squared
weights, so "errors": [0, 0] still read back as squared weight = content
while [0, 2] was fixed. Switch on the squared weights up front instead
of setting the first bin with a positive error first.

Assisted-by: claude-code:claude-opus-5-5
The index categories of combined distributions were created after the
parameter points were read. A string value for such a category was
dropped, and a numeric value created a RooRealVar that took the place of
the category.

Assisted-by: claude-code:claude-opus-5-5
RooJSONFactoryWSTool::warning() logged on RooFit::IO, which no default
stream covers. Users therefore never saw these warnings, for example
about newer HS3 versions, deprecated type keys, name clashes or lost
constraints. The tests only did because they hijack the stream.

Assisted-by: claude-code:claude-opus-5-5
@guitargeek
guitargeek merged commit d61fecf into root-project:master Nov 5, 2026
30 of 31 checks passed
@guitargeek
guitargeek deleted the hs3-conformance-fixes-independent branch November 5, 2026 20:19
@guitargeek guitargeek changed the title [RF][HS3] Small HS3 conformance fixes (independent of pending HS3 changes) [RF][HS3] Bring the HS3 import and export closer to the HS3 standard Nov 5, 2026
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.

2 participants