Skip to content

[RF][HS3] Combined data sets and combined/conditional distributions (HS3 #119; depends on #24001) - #24005

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

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

Conversation

@cburgard

@cburgard cburgard commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Depends on #24001 (which depends on #23840). This branch is stacked on it: please review only the last 10 commits. It stays in draft until HS3 PR #119 is merged, and will be rebased when the PRs below it are merged.

Rebased onto master after the merge of #23996, including the follow-up commits added there during review (e.g. isHS3VersionBefore() instead of isLegacyHS3Version(), analyses named after their ModelConfig).

This Pull request:

Combined distributions and data sets in RooFit's HS3 import/export, following HS3 PR #119 ("combined data sets and combined/conditional distributions"):

  • a RooSimultaneous is written as a combined_dist and a data set with category observables as a combined data set, instead of the ROOT-internal description in misc.ROOT_internal that only RooFit could read;
  • category_conditional_dist (a distribution selected by the value of a categorical parameter) is read and written;
  • the likelihood of a ModelConfig pairs the combined objects, and the import checks that pairing.

Files written by earlier ROOT versions, with misc.ROOT_internal.combined_distributions / combined_datasets, are still read.

Changes or fixes:

1. RooSimultaneous ↔ combined_dist

Direction Change
Export A RooSimultaneous is a combined_dist. Its columns are the index category or, for a RooSuperCategory index, the input categories, sorted by name. The columns are declared as categorical axes of default_domain (with the states in their order). components has one entry per state with a pdf, with the key as one label per column. A RooSuperCategory index used to be an export error.
Import A combined_dist is a RooSimultaneous over the column category, or over a RooSuperCategory of the columns in the order of the document. The columns must be declared by categorical axes of a domain; keys are validated against them (length, labels, duplicates). The ROOT-internal form is still read.

The components are written in the order of the columns and their states, which does not depend on the order of the inputs of a RooSuperCategory, so the document is stable under round trips. Category indices are not part of HS3: after a round trip, the index of a state is its position in the domain's categories.

Tests: CombinedDist (structure, round trip, legacy form), CombinedDistSeveralColumns, CombinedDistImportErrors. CombinedObjectsKeepNamesWithoutMisc checks that a document without misc imports with the same names for the pdf, its index category and the data set (they used to be encoded in misc.ROOT_internal), and that the ModelConfig finds its pdf through the likelihood.

2. Combined data sets ↔ combined

Direction Change
Export A data set whose observables include RooCategorys is a combined entry with the categories as columns (sorted by name) and one sub-data set per key, named <data>_<label>[_<label>…], written like any other data set. A key is written also without entries: RooFit treats a channel without entries as present and empty. Several category observables used to be an export error.
Export If the workspace has a RooSimultaneous with the same columns, each sub-data set carries only the observables of its component (as before), and keys without entries for which it has no component are left out. Otherwise all observables are written.
Export The likelihood of a ModelConfig pairs the RooSimultaneous with the combined data set ("distributions": ["simPdf"], "data": ["obsData"]) instead of listing the components of both. An analysis is written for every combined data set with the same columns.
Import A combined data set is a RooDataSet with one category column per entry of columns, filled from the components, which are then not imported on their own. One code path for one and for several columns. The components of combined data sets are assembled before the analyses are read, so the ModelConfig observables include the index category, as in the original HistFactory workspace. The ROOT-internal form is still read, and handled after the analyses as before, because its likelihoods list the components.

Tests: CombinedData (structure, round trip, data without pdf, legacy form), CombinedDataSeveralColumns, CombinedDataImportErrors, and HS3RoundTripOfReferenceWorkspace in testHistFactory for the ROOT 6.16 reference workspace (same objects, the same likelihood up to a constant, stable document). The existing SimultaneousFit and HS3ClosureLoop tests now exercise the new form.

3. Pairing checks

HS3 #119 treats a key of a combined_dist without a sub-data set in the paired combined data set as an unobserved component, which is marginalised, and a present but empty sub-data set as observed with zero events. RooFit has no notion of an unobserved component: a combined RooDataSet without entries for a channel is a present and empty channel, and both evaluation backends then add the expected number of events of an extended component to the likelihood (#24004).

When a likelihood pairs a combined_dist with a combined data set, the import now checks the keys:

Case Behaviour
extended component without sub-data set error (the fit would differ from what the document means)
non-extended component without sub-data set warning (changes nothing)
sub-data set without component warning (RooFit ignores the data)
different columns error

Test: CombinedDataPairing.

3b. extended on a combined_dist

HS3 #119 states that a combined_dist must not have the component extended: whether it is extended follows from its components. The importer rejects it with an error, whatever its value (test: CombinedDistImportErrors). RooFit never writes it.

4. category_conditional_dist

RooFit has this distribution as the "switch mode" of RooSimultaneous: with an index category that is not an observable, it evaluates to the component selected by the current state, and its expected number of events is the one of that component.

Direction Change
Import A category_conditional_dist is a RooSimultaneous over its categorical parameters (declared by categorical axes of a domain), tagged with the attribute hs3_category_conditional_dist. RooFit decides between joint and conditional by the normalization set, i.e. by context; the tag makes the export context-free.
Export A RooSimultaneous with the tag is written as category_conditional_dist, with categorical_parameter instead of columns, and its analysis is paired with data like any other pdf. A RooSimultaneous built in RooFit is a combined_dist unless the user sets the attribute.
Export The value of a RooCategory in a snapshot is written to the parameter point as its label (categorical parameters). The columns of combined distributions are not parameters and are left out.

Test: CategoryConditionalDist.

5. Fix: empty weighted data sets

An empty weighted data set over a variable without bins was detected as binned data with zero bins. Unbinned data sets imported from HS3 leave their axes without binning, so a present but empty sub-data set was not stable under round trips. The detection now requires a binning.

6. Components must not share observables

HS3 #119 requires that the components of a combined distribution share no variate, and that the sub-data sets of a combined data set have distinct axis names: a quantity measured in several components is a separate random variable in each of them (e.g. mass_SR and mass_CR), since it follows a different distribution there. Exporting a data set that is split by a RooSimultaneous whose components share an observable is therefore an error, which asks to give the observable a separate name per component. A document whose sub-data sets share an axis is still read, with a warning. Tests: CombinedComponentsMustNotShareObservables; CombinedDataSeveralColumns now uses one observable per component.

7. Data sets with categories but without a combined distribution

A combined data set only makes sense paired with a combined distribution. Without a matching RooSimultaneous, the category of a data set is an ordinary observable, so such a data set is now written as a single data set with a categorical axis (the form of HS3 #108, read since #24001), binned or unbinned. Before, it was split by the category into sub-data sets that all carried the same observables, which #119 forbids. Test: the data-only part of CombinedData.

8. Option: rename the observables of combined data sets

Workspaces whose RooSimultaneous components share an observable (common in older workspaces and in CMS Combine) can't be exported because of 6. The new option RooJSONFactoryWSTool::config().renameCombinedObservables (off by default) renames the observables of every combined data set per key on export: obs_<key> (the labels of the key joined by _), or obs_<key>_<observable> if a component has several observables. The components of the matching RooSimultaneous are written with the renamed observables: they are rebuilt with RooCustomizer, which clones every node depending on a renamed observable with the suffix _<key>, so nodes shared by several components get one copy per component (e.g. shift(mass) → shift_SR(obs_SR), shift_CR(obs_CR)). Observables that already have their new name are left alone, so a second export of an imported document gives the same document. Test: RenameCombinedObservables (the likelihood is the same after the round trip).

Not covered, deferred

  • The categorical function of Master tmva dataloader vi #119 and RooMultiPdf/RooMultiReal (both select by category index = list position, RooMultiPdf is never extended and adds a penalty term): #24006.
  • Unobserved components (see 3): RooFit would need a way to mark a channel of a combined data set as unobserved (#24004).
  • A RooSuperCategory whose inputs are not RooCategorys (nested super categories): explicit error.

Behaviour changes users may notice

  • misc.ROOT_internal.combined_distributions and combined_datasets are no longer written; the standard objects replace them. Files with the old form are still read.
  • The likelihood of a ModelConfig with a RooSimultaneous lists one pair (the RooSimultaneous and the combined data set) instead of the components. Analyses are named after their ModelConfig (<ModelConfig>_<data>), like all analyses since [RF][HS3] Bring the HS3 import and export closer to the HS3 standard #23996.
  • After import, the ModelConfig observables include the index category.
  • Category indices become the positions in the domain's categories after a round trip.
  • The sub-data sets of a combined data set follow its combined entry in data (they used to be written before all other data sets).
  • Values of categorical parameters appear in parameter points as strings.
  • Import errors and warnings for the pairing of combined objects (see 3).
  • A RooSimultaneous whose components share an observable can no longer be exported together with its data set, unless renameCombinedObservables is enabled (see 6 and 8).
  • A data set with a category observable but without a matching RooSimultaneous is written as one data set with a categorical axis, not as sub-data sets (see 7).

Checklist:

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

Tests run (Linux, Ubuntu 26.04, GCC 15.2, RelWithDebInfo, -Droofit=ON -Dtesting=ON): testRooFitHS3 115 → 126 tests (11 new), testJSONInterface 1, testHistFactory 69 → 70 (1 new), all passing; each commit built and tested on its own. clang-format clean.

AI assistance: Claude Code implemented the changes, the tests and this description under review of the author: claude-fable-5-1 the first 6 commits (sections 1–5), claude-opus-5-5 the last 4 (sections 3b, 6–8). The commits carry the corresponding Assisted-by trailer.

🤖 Generated with Claude Code

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
…ll_dist and crystalball_doublesided_dist

HS3 PR root-project#135 defines `crystalball_dist` as the single-sided Crystal Ball
(`sigma`, `alpha`, `n`, as RooCBShape) and the new
`crystalball_doublesided_dist` as the double-sided one (`alpha_L`, `n_L`,
`alpha_R`, `n_R`, and `sigma` or `sigma_L`/`sigma_R`, without a symmetric
shorthand).

* RooCBShape is exported as `crystalball_dist`, as before.
* A single-sided RooCrystalBall is exported as `crystalball_dist`, and read
  back as a RooCBShape (same function, same sign convention for alpha).
  Before, it was written as an incomplete `crystalball_doublesided_dist`
  without `alpha_R`/`n_R`, which could not be read back.
* A double-sided RooCrystalBall is exported as
  `crystalball_doublesided_dist`, with `sigma` if both sides share it, and
  the tails always as `alpha_L`/`n_L`/`alpha_R`/`n_R`.
* `crystalball_doublesided_dist` is imported as RooCrystalBall, also with a
  single `sigma`, and invalid field combinations are rejected.
* `crystalball_dist` with any `_L`/`_R` field is the double-sided form of
  HS3 versions before 0.3. It is still read as RooCrystalBall (including
  `alpha`/`n` for both tails with `sigma_L`/`sigma_R`), with a one-time
  deprecation warning.

The `crystalball_doublesided_dist` factory expression and the RooCrystalBall
export keys are removed, since the new importer and exporter handle all
cases.

Assisted-by: claude-code:claude-opus-5-5
…on't contribute to the staterror

HS3 PRs root-project#135 and root-project#124 make the "errors" of a HistFactory sample optional: a
sample without "errors" contributes no statistical uncertainty to a
staterror modifier. RooFit used the content as squared error instead, i.e.
assumed Poisson uncertainties. That is kept for documents before HS3 0.3.

In HS3 >= 0.3 documents, this only matters if the staterror is constrained
through the legacy "constraint" key, since otherwise the gammas are either
unconstrained or use the named constraint distributions.

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
… shapefactor as "shapefactor"

Following the proposal for HS3 PR root-project#124, HS3 has a single per-bin modifier
type, `shapefactor`, whose parameters are constrained by the distributions
named in `constraints`, if given, and free otherwise. `shapesys` is a
deprecated alias.

* Export: HistFactory's ShapeSys and ShapeFactor are both written as
  `shapefactor`, with the constraint distributions found for the gammas in
  `constraints` (null for free gammas, omitted if all are free). The type is
  no longer derived from the name of the ParamHistFunc, so free gammas no
  longer make the export fail with "cannot find constraint", and constraints
  of any type are allowed (the unused relative errors, which only supported
  Gaussian and Poisson constraints, are no longer computed).
* Import: `shapesys` is read as `shapefactor`, with a deprecation warning
  (once per channel) for HS3 >= 0.3. The ParamHistFunc is named
  `<channel>_<name>_ShapeSys` if constrained and
  `<channel>_<name>_shapeFactor` if free, as in HistFactory.
* Without `constraints`, RooFit only creates constraints from `data.vals`
  for documents before HS3 0.3 or with a legacy constraint key; otherwise the
  parameters are free and `data.vals` is ignored with a warning.

The HistFactory test of the HS3 import of ShapeFactors now round-trips a
free ShapeFactor, which needed constant gammas before.

Assisted-by: claude-code:claude-opus-5-5
cburgard and others added 13 commits November 6, 2026 09:23
HS3 PR root-project#108 added categorical axes (`{"name": ..., "categories": [...]}`) to
domains and data sets. RooFit used to read a categorical domain axis as an
unbounded RooRealVar and could not read categorical axes of data.

* Domains: a categorical axis creates a RooCategory with one state per
  category, in the order of the document and with the position as index. An
  existing RooCategory of that name must have the same labels. The axis must
  not have `min`, `max`, `nbins` or `edges`, and the labels must be unique
  non-empty strings. A string value in a parameter point sets the state.
* Unbinned data: the entries of a categorical axis are labels of the
  category, which is declared by a domain.
* Binned data: a categorical axis is one bin per category. HS3 identifies
  categories by name, so the data may list them in another order than the
  domain. RooDataHist looks up the bin of a category by the order of the
  states of the category it is given, so the data's category takes over the
  states and their order from the workspace category, and the contents and
  errors are permuted from the order of the document accordingly.
* histfactory_dist: categorical axes are rejected with an explicit error.

Export is unchanged: datasets and distributions with categories are still
written with the ROOT-internal combined form until the HS3 representation of
combined data (HS3 PR root-project#119) is final.

Assisted-by: claude-code:claude-opus-5-5
…ned contents

HS3 PR root-project#126 defines the storage order of multi-dimensional binned contents:
the last axis listed in "axes" varies fastest. This is RooDataHist's own
order, and RooFit writes the axes in the order of the variables of the
RooDataHist, so no change is needed for binned data. A new test pins it.

HistFactory channels write the axes from the first sample's histogram, and
the contents of every sample from its own histogram. If two histograms of a
channel had their observables in different orders, the contents would be
written along different axes. This is now an explicit error.

Assisted-by: claude-code:claude-opus-5-5
…amHistFunc in HS3 bin order

HS3 PR root-project#126 lays out every per-bin array with the last axis varying fastest,
like RooDataHist. ParamHistFunc, however, orders its parameters like the bins
of a TH1, with the first variable varying fastest. For more than one
dimension, RooFit therefore wrote the parameters of ParamHistFunc ("step")
and of the staterror and shapesys/shapefactor modifiers of HistFactory in a
different order than the bin contents. On import, it paired the relative
uncertainty of bin k with the k-th gamma in TH1 order, so the gammas of
multi-dimensional channels got the constraints of other bins.

The parameters are now converted between the two orders at the HS3
boundary: they are written in bin order (via ParamHistFunc::getParameter),
paired with the per-bin values in bin order, and only then put into TH1
order for the ParamHistFunc. One-dimensional functions are unchanged.

Documents before HS3 0.3 with multi-dimensional ParamHistFuncs don't define
the order; none are known, and they are rejected with an error instead of
being guessed.

New tests check the order in the written file for binned data, RooHistFunc
and ParamHistFunc (each with both orders of the variables), and for a
two-dimensional HistFactory channel with staterror.

Assisted-by: claude-code:claude-opus-5-5
…ed_dist and read it back

A RooSimultaneous used to be exported only as ROOT-internal information
(misc.ROOT_internal.combined_distributions). HS3 PR root-project#119 defines the
"combined_dist": its components are selected by a key with one label per
column, and the columns are categorical axes of the domains.

Export: a RooSimultaneous becomes a "combined_dist". The columns are its
index category or, for a RooSuperCategory index, the input categories,
sorted by name so that a distribution and a data set agree on the order
without looking at each other. The columns are declared as categorical
axes of the default domain. The components are written in the order of
the columns and their states, which does not depend on the order of the
inputs of a RooSuperCategory, so the document is stable under round
trips. States without a pdf are not written. Category indices are not
part of HS3: after a round trip, the index is the position in the
domain's "categories".

Import: a "combined_dist" becomes a RooSimultaneous over the column
category, or over a RooSuperCategory of the columns in the order of the
document. The columns must be declared by categorical axes of a domain;
keys are validated against them. The ROOT-internal form of files written
by earlier ROOT versions is still read.

Tests: CombinedDist (structure, round trip, legacy form),
CombinedDistSeveralColumns, CombinedDistImportErrors.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… HS3 form and read them back

A RooDataSet with a category observable used to be written as its
components plus ROOT-internal information on how to combine them
(misc.ROOT_internal.combined_datasets), and the likelihood of a
ModelConfig listed the components of the RooSimultaneous paired with
the components of the data. HS3 PR root-project#119 defines the "combined" data
set: sub-data sets selected by a key, with the columns declared as
categorical axes of the domains.

Export: a data set whose observables include RooCategories becomes a
"combined" entry with the categories as columns, sorted by name, and
one sub-data set per key, named after the data set and the labels. The
categories are declared in the default domain. A key is written also
without entries, since RooFit treats a channel without entries as
present and empty. If the workspace has a RooSimultaneous with the same
columns, each sub-data set carries only the observables of its
component, as before, and keys without entries for which it has no
component are left out; otherwise all observables are written. The
likelihood of a ModelConfig now pairs the RooSimultaneous with the
combined data set, and an analysis is written for every combined data
set with the same columns. A RooSimultaneous without matching data gets
an analysis named after the ModelConfig, like any other pdf, instead of
one named after the pdf.

Import: a "combined" data set becomes a RooDataSet with one category
column per entry of "columns", filled from the components, which are
then not imported on their own. One code path for one and for several
columns. The ModelConfig observables then include the index category.
The ROOT-internal form of files written by earlier ROOT versions is
still read; its likelihoods list the components, so it is handled after
the analyses as before.

An empty weighted data set over a variable without bins was detected as
binned data with zero bins, which made the document of a present but
empty sub-data set unstable under round trips. The detection now
requires a binning.

Tests: CombinedData (structure, round trip, data without pdf, legacy
form), CombinedDataSeveralColumns, CombinedDataImportErrors.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…istributions and data sets

HS3 root-project#119 treats a key of a combined distribution without a sub-data set
in the paired combined data set as an unobserved component, which is
marginalised, and a present but empty sub-data set as observed with zero
events. RooFit has no notion of an unobserved component: a combined
RooDataSet without entries for a channel is a present and empty channel,
and both evaluation backends then add the expected number of events of
an extended component to the likelihood (see ROOT issue root-project#24004).

When a likelihood pairs a combined_dist with a combined data set, the
import now checks the keys. A missing sub-data set of an extended
component is an error, since the fit would differ from what the document
means. A missing sub-data set of a non-extended component changes
nothing and is a warning, as is a sub-data set for a key without a
component, which RooFit ignores. Differing columns are an error.

Test: CombinedDataPairing.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…onal_dist

HS3 root-project#119 defines the "category_conditional_dist": one of several
sub-distributions, selected by the value of categorical parameters and
conditional on it. RooFit has this as the "switch mode" of
RooSimultaneous: with an index category that is not an observable, it
evaluates to the selected component, and its expected number of events
is the one of that component.

Import: a category_conditional_dist becomes a RooSimultaneous over its
categorical parameters, which must be declared by categorical axes of a
domain like the columns of a combined_dist. The RooSimultaneous carries
the attribute "hs3_category_conditional_dist": RooFit decides between
joint and conditional by the normalization set, i.e. by context, and
the attribute makes the export context-free. A RooSimultaneous built in
RooFit is a combined_dist unless the user sets the attribute.

Export: a tagged RooSimultaneous is written as category_conditional_dist
with "categorical_parameter" instead of "columns". Its analysis is
paired with data like any other pdf. The value of a RooCategory in a
snapshot is written to the parameter point as the label, and the
category to the default domain; the columns of combined distributions
are not parameters and are left out.

Test: CategoryConditionalDist.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…HistFactory reference workspace

The reference workspace of ROOT 6.16 is the standard case of a
RooSimultaneous with combined data sets. The test checks that it is
written as a combined_dist with combined data sets, read back into the
same objects with the same likelihood values, and that the document is
stable. Stability holds from the third export on: the 6.16 workspace
has binWidth constants that the imported model doesn't use, and the
HistFactory importer sets the default parameter errors, which the 6.16
workspace lacks. Both are unrelated to the combined objects.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… their names without misc

The names of a RooSimultaneous and of a combined data set used to live
in misc.ROOT_internal, so a document without "misc" lost them. Now they
are part of the combined_dist and combined entries. The test exports a
workspace with a RooSimultaneous (not named "simPdf", the historical
default), removes "misc", imports the document and checks that the pdf,
its components, the index category and the data set keep their names,
that the ModelConfig finds its pdf through the likelihood, and that the
re-export describes the same objects.

Assisted-by: claude-code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hose components share an observable

HS3 PR root-project#119 requires that the components of a combined distribution share
no variate, and that the sub-data sets of a combined data set have distinct
axis names: a quantity measured in several components is a separate random
variable in each of them, e.g. "mass_SR" and "mass_CR", since it follows a
different distribution there.

A RooSimultaneous whose components use the same observable can therefore
not be written together with its data set. Exporting such a data set, split
by the components of the RooSimultaneous, is now an error that asks to give
the observable a separate name in each component. A document whose
sub-data sets share an axis is still read, with a warning.

The test of a data set with several columns now uses a separate observable
per component.

Assisted-by: claude-code:claude-opus-5-5
… as categorical axes without a combined distribution

A combined data set is paired with a combined distribution, and HS3 PR root-project#119
requires its sub-data sets to have distinct axis names. A data set with a
category observable but without a matching RooSimultaneous was split by
the category anyway, so all sub-data sets carried the same observables.

Without a matching RooSimultaneous, the category is an ordinary
observable: such a data set is now written as a single data set with a
categorical axis (HS3 PR root-project#108), binned or unbinned, which RooFit reads back
since the axis handling PR. The categories are declared by a categorical
axis of the default domain, and the entries of an unbinned categorical axis
are the labels.

Assisted-by: claude-code:claude-opus-5-5
…es of combined data sets per key

HS3 PR root-project#119 requires that the components of a combined distribution share
no variate. Workspaces whose RooSimultaneous components share an observable
(common in older workspaces and in CMS Combine) can therefore not be
exported. The new option RooJSONFactoryWSTool::config()
.renameCombinedObservables (off by default) renames the observables of
every combined data set per key on export: "obs_<key>" (the labels of the
key joined by "_"), or "obs_<key>_<observable>" if a component has several
observables.

The components of the matching RooSimultaneous are written with the
renamed observables: they are rebuilt with RooCustomizer, which clones
every node that depends on a renamed observable with the suffix "_<key>",
so nodes shared by several components get one copy per component.
Observables that already have their new name are left alone, so that a
second export of an imported document gives the same document. The
likelihood is unchanged.

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

HS3 PR root-project#119 now states that a combined_dist must not have the component
"extended": whether it is extended follows from its components. The
importer ignored such a component; it now raises an error, whatever its
value. RooFit never writes it.

Assisted-by: claude-code:claude-opus-5-5
@cburgard
cburgard force-pushed the hs3-combined branch 2 times, most recently from a594b72 to 9dcd02b Compare November 6, 2026 10:25

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.

2 participants