Repository navigation
Conversation
1 of 2 tasks
cburgard
force-pushed
the
hs3-category-conditional
branch
2 times, most recently
from
November 10, 2026 19:21
8c5522d to
dab16c1
Compare
cburgard
force-pushed
the
hs3-category-conditional
branch
4 times, most recently
from
November 12, 2026 22:38
ebe0269 to
46e5b61
Compare
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. The labels and the absence of a range or binning are checked when the document is parsed (HS3Parser), and that all domains agree on a categorical axis when it is validated (HS3Validate). The categories are created and checked against the workspace in RooJSONFactoryWSTool. 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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HS3 root-project#126 was merged with a last change (root-project#137): the range of an irregular binning is given by its "edges", and documents of HS3 0.3 or later must not have "min" or "max" alongside them, in binned data and in domains. Documents before 0.3 may have them, equal to the first and last edge. - Export: binned axes with edges (HS3Writer, and writeAxisBinning for histograms and generic formulas) are written without "min" and "max". Domain axes with edges are written without them if they agree with the edges, so that a range that differs from its binning is not lost. - Import: edges without "min" and "max" were already read. In documents of HS3 0.3 or later, "min" or "max" alongside "edges" give a warning (HS3Upgrade); that they agree with the edges is checked as before. Test: IrregularBinnedAxesHaveRange is replaced by IrregularBinnedAxesWithoutRange (data and domain axes without range, round trip, and the warning only for HS3 0.3 documents). Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. Ported onto the typed HS3 document: the categories are recorded in the export state and written as categorical axes of the "default_domain", sorted by name together with the continuous axes. Assisted-by: claude-code:claude-fable-5-1 Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. Ported onto the typed HS3 document: a combined data set is a CombinedData in HS3::Data. The parser checks its structure, the validation checks the columns against the categorical axes of the domains, the labels of the keys, and that each sub-data set is a data set of the document that belongs to one combined data set. The likelihood of a ModelConfig names the RooSimultaneous, and the constraint terms of its HistFactory channels are still listed in "aux_distributions". Assisted-by: claude-code:claude-fable-5-1 Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. Ported onto the typed HS3 document: the keys of the combined data set are taken from the document. Assisted-by: claude-code:claude-fable-5-1 Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A HistFactory model is the standard case of a RooSimultaneous with combined data sets. HS3ClosureLoop now also checks that it is written as a combined_dist with combined data sets, without the ROOT_internal combined_distributions and combined_datasets, and read back into a RooSimultaneous over channelCat and data sets with the index category, with the same number of entries and the same sum of weights. The stability of the document and the fit results were already checked. Assisted-by: claude-code:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
…ervable 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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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. Ported onto the typed HS3 document: the categorical axes are written through the HS3::CategoricalAxis of binned and unbinned data, with the position of the label as the entry of unbinned data. Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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. Ported onto the typed HS3 document. The data set of the test is called "data" instead of "obs": its sub-data sets "obs_SR" and "obs_CR" had the names of the renamed observables, which the validation of HS3 0.3 documents now rejects. Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
HS3 requires the components of a combined distribution to have distinct
variates, and the terms of a likelihood as well. A variate in two of
them is one quantity modelled twice, e.g. a constraint that is both a
term of the likelihood and a factor of a channel, or two quantities with
one name, e.g. the observable "mass" of two channels, which need their
own names ("mass_SR", "mass_CR").
All cases are now checked by one function, sharedVariates(), on the
document, after the export and before the import:
- the terms of each likelihood: its distributions (the components of a
combined_dist, or of a RooSimultaneous in ROOT_internal of older
documents, are terms of their own), its "aux_distributions", and the
constraint terms named by the modifiers of its histfactory_dist terms;
- the components of the combined distributions and of the combined data
sets that no likelihood lists.
The variates of a distribution are the "x" of the fundamental
distributions and the axes of distributions with axes, through products,
mixtures and combined distributions; those of a data set are its axes.
This replaces the two checks on the sub-data sets of combined data sets:
the export error (only with a data set) and the import warning.
- Export: a constraint that is both a factor of a channel's product and
a term of the likelihood (a HistFactory constraint, or an external
constraint of the ModelConfig) is removed from the product, so that it
is written once; RooFit counts it once. Only products that no other
distribution uses are changed. Remaining shared variates are an error,
now also for a RooSimultaneous without data set. The error mentions
config().renameCombinedObservables, and the new
config().allowExportSharedVariates, which writes the document anyway,
with a warning for every shared variate. It is not valid HS3, and has
to be fixed, e.g. by hand in the JSON, before it can be read back.
- Import: shared variates are an error for HS3 0.3, and a warning for
documents of older versions, which RooFit wrote that way.
Tests:
- CombinedComponentsMustNotShareObservables: the export error, the
override with warning, the rejection of the written document, and of
a combined data set with a shared axis (read with a warning as 0.2).
- SharedVariatesInLikelihood: two channels with the observable "x".
- ConstraintInModelAndExternal: a constraint in the model and in the
external constraints is written once, reads back with the same NLL.
- SimultaneousGaussians, CombinedDist, CombinedDistSeveralColumns,
CombinedDistImportErrors and CombinedDataImportErrors give the
components their own observables.
Ported onto the typed HS3 document: the check works on the JSON
tree as before. On export, it runs after writeDocument(), since the
likelihoods and the data sets are only in the JSON tree from then
on; on import, it runs on the upgraded document.
Assisted-by: claude-code:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nction A RooHistFunc or RooHistPdf is evaluated with its own observables, which are mapped by position onto the variables of its histogram and need not have the same names. The exporters took the names of the axes from the variables of the histogram. With RooJSONFactoryWSTool::config().renameCombinedObservables, where a RooCustomizer replaces the observables of the components of a RooSimultaneous, the "histfactory_dist" of a channel therefore kept the original observable "x" as axis while the data had "obs_SR" (reported in the review of this PR, with a reproducer). The same happened for "step" and "histogram_dist", and for any RooHistFunc constructed with other observables than the variables of its histogram. The axes are now named after the observables onto which the variables of the histogram are mapped (histogramVariablesNamedAfterObservables() in JSONIOUtils). The HistFactory exporter keeps these variables for the channel, so the observables are also left out of the parameters of the channel. RooHistPdf gets getHistObsList(), like RooHistFunc. Test: HistogramAxesNamedAfterObservables (a RooHistFunc with observables of other names, a HistFactory channel and a RooHistPdf on the same observable with renamed observables: axes of the distributions and of the data, likelihood after the import, stable document). Ported onto the typed HS3 document: the observables of the exported channel (ExportedChannel::observables) are the renamed variables. Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… per bin A one-dimensional weighted RooDataSet is written as binned data if its entries are the bins in their order, the form in which combined data sets of HistFactory models are often stored. The check stopped at the first entry that was not in the next bin and only compared the number of entries before it to the number of bins: entries in the bins 0, 1, 1 counted as binned data, and the export wrote one content per entry, reading past the end of the bin contents. This happened on the second export of the reproducer in the review of this PR, where a HistFactory channel is paired with unbinned data. The data set is now binned data only if it has exactly one entry per bin. Test: WeightedDataSetAsBinnedData. Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lumns
RooAbsData::split() keeps a column of a data set that no channel of the
RooSimultaneous uses (e.g. an event number) in every sub-data set, so
all components of the combined data set have it as an axis. HS3
requires distinct axes, and such a data set could not be exported
unless a likelihood listed it, since only then were the variates of the
distributions checked instead of the axes of the data.
Such a column has no statistical meaning. On export, an axis that
several components of a combined data set share is declared as a
bookkeeping column in misc.ROOT_internal.bookkeeping_columns
({"<data set>": ["eventNumber"]}) if its variable is used by nothing
else in the workspace: it has no clients and is in no set of a
ModelConfig. Bookkeeping columns are exempt from the requirement of
distinct axes; all other shared axes are an error. The axes of combined
data sets are now checked whether or not a likelihood lists the data
set. On import, a declared column is accepted with a warning, and it is
not added to the observables of the ModelConfig, so the document is
stable. A declared column that is a variate of a distribution is no
bookkeeping column.
Test: CombinedDataColumnWithoutDistribution (with and without
likelihood: the declaration, import with warning, stable document, the
same document without declaration rejected; a column in the
observables of the ModelConfig and a declared column that is a variate
are errors).
Ported onto the typed HS3 document: misc.ROOT_internal.bookkeeping_columns
is a field of RooFitInternal, which the parser reads and the writer
writes. The check works on the JSON tree like the one of distinct
variates, and the importer of an analysis gets the bookkeeping
columns from the import of the document.
Assisted-by: claude-code:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HS3 root-project#147 (split out of 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. Export: RooFit doesn't distinguish the two uses of a RooSimultaneous by the object, only by the normalization set. A combined distribution is the top-level pdf of a model, while a conditional one (e.g. a choice between alternative background shapes) is a building block of a model. A RooSimultaneous that is used by another pdf is therefore written as category_conditional_dist, with "categorical_parameter" instead of "columns", and one that no other pdf uses as combined_dist. This is a heuristic until RooFit has a more explicit way to mark the conditional use. A category_conditional_dist that no other distribution uses is read with a warning that it will be written back as a combined_dist. The pairing checks of combined_dist in a likelihood use the type in the document. 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. For the check of distinct variates (see "Distinct variates of likelihood terms and combined components"), a category_conditional_dist has the variates of its alternatives, which may share them. A RooSimultaneous that no other pdf uses is written as combined_dist, so if its alternatives share an observable the export fails; the import warning for an unused category_conditional_dist says so. Test: CategoryConditionalDist. Ported onto the typed HS3 document: the value of a categorical parameter is a label of the HS3::ParameterValue, the columns of combined distributions are kept in the export state, and the pairing check and the import warning look up the type of the generic distribution in the document. Assisted-by: claude-code:claude-fable-5-1 Assisted-by: claude-code:claude-opus-5-5 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A RooMultiPdf (discrete profiling, e.g. in CMS Combine) couldn't be
exported ("no export keys available"). It is now written as a
category_conditional_dist: its category is the categorical parameter,
and the key of the pdf at list position i is the label of the state
with index i, since a RooMultiPdf selects by index. A pdf without a
state of its index is an error.
The penalty term that a RooMultiPdf adds to the likelihood (a correction
for the number of free parameters of the selected pdf) has no HS3
representation yet. It is dropped with a warning, and its constants are
not written. The category_conditional_dist is read back as a
RooSimultaneous, which evaluates to the same pdf.
Test: MultiPdfExport.
Ported onto the typed HS3 document: the category is declared through
recordCategory(), and the distribution is recorded as a generic object
of the exported document.
Assisted-by: claude-code:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cburgard
force-pushed
the
hs3-category-conditional
branch
from
November 13, 2026 11:21
46e5b61 to
cbc32e6
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
Depends on #24005 (which depends on #24001). This branch is stacked on it: please review only the last 2 commits. It stays in draft until HS3 PR #147 is merged, and will be rebased when the PRs below it are merged.
Rebased onto
masterafter 36821748060 ("Import and export through a typed representation of HS3 documents"). Where the commits conflicted with it, they were re-implemented on the typedHS3Document(parser, validator, writer) instead of the removed code; the messages of these commits say how ("Ported onto the typed HS3 document: …").The first commit was part of #24005. HS3 moved
category_conditional_distand thecategoricalfunction from #119 to #147, so the ROOT side is split the same way.This Pull request:
Reads and writes the HS3
category_conditional_distof HS3 PR #147. It selects one of several sub-distributions by the value of categorical parameters and evaluates it conditioned on that value. This is the construction used for discrete profiling over alternative shape hypotheses.It is read as a
RooSimultaneous.RooSimultaneousand existingRooMultiPdfobjects are written ascategory_conditional_dist. The penalty term ofRooMultiPdfis dropped with a warning; how to represent it is the main open question, see below.Changes or fixes:
1.
category_conditional_dist↔RooSimultaneousRooFit has this distribution as the "switch mode" of
RooSimultaneous(since #23142). When its index category 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.category_conditional_distbecomes aRooSimultaneousover its categorical parameters, which must be declared by categorical axes of a domain.RooSimultaneousapart by the object, only by the normalization set. A combined distribution is the top-level pdf of a model, while a conditional one (e.g. a choice between alternative background shapes) is a building block of a model. So aRooSimultaneousthat another pdf uses is written ascategory_conditional_dist, withcategorical_parameterinstead ofcolumns. One that no other pdf uses is written ascombined_dist, as before.category_conditional_distthat no other distribution uses is read with a warning that it will be written back as acombined_dist. The pairing checks of acombined_distwith combined data in a likelihood use the type in the document.RooCategoryin a snapshot is written to the parameter point as its label (categorical parameters), and the category is declared in the default domain. The columns of combined distributions are not parameters and are left out.For the check of distinct variates of #24005 (section 8 there), a
category_conditional_disthas the variates of its alternatives, which may share them: only one is selected. ARooSimultaneousthat no other pdf uses is written ascombined_dist, so if its alternatives share an observable, the export now fails with the shared-variate error instead of writing acombined_distwith the wrong meaning; the import warning for an unusedcategory_conditional_distsays so.The distinction by clients is a heuristic. A
RooSimultaneousused in a product with constraint terms, for example, would be taken as conditional. It is the best criterion available without an explicit marker, and we would prefer a more reliable one in the future (see question 2).Test:
CategoryConditionalDistcovers:category_conditional_distused in amixture_dist;RooSimultaneousbuilt in RooFit can't be written as acombined_distat the top level (its alternatives share the observable), and is acategory_conditional_distonce aRooAddPdfuses it;category_conditional_dist;category_conditional_distthat is not used;2.
RooMultiPdf→category_conditional_distExporting a
RooMultiPdffailed ("no export keys available"). It is now written as acategory_conditional_dist:RooMultiPdfselects by index;The penalty term that
RooMultiPdfadds to the likelihood (a correction for the number of free parameters of the selected pdf) has no HS3 representation yet. It is dropped with a warning, and its constants are not written. Thecategory_conditional_distis read back as aRooSimultaneousthat evaluates to the same pdf, so the class doesn't survive a round trip.Test:
MultiPdfExportchecks:RooSimultaneousand of a mixture using it, for both states.Questions for the RooFit maintainers
#23454 (comment) describes the plan to deprecate
RooMultiPdfin favour ofRooSimultaneous, and to pass the penalty term of discrete profiling as an argument at likelihood creation (createNLL()/fitTo()). This PR follows that direction: the import target isRooSimultaneous, andRooMultiPdfis only written. Two questions remain.categoricalfunction used by a distribution inaux_distributions. Which form fits the planned interface? Until this is settled, the export drops the penalty with a warning, and we would bring the answer to HS3.RooSimultaneousknow about its conditional use more explicitly, so that the export can rely on it?The same questions for
RooMultiRealand the HS3categoricalfunction are collected in #24006.Not covered, deferred
categoricalfunction (→RooMultiReal): #24006.RooMultiPdf(question 1).components. The import doesn't check that a parameter's domain only contains labels with a component.Behaviour changes users may notice
RooCategorys in snapshots that are not columns of a combined distribution) appear in parameter points as strings.RooSimultaneousthat another pdf uses is written ascategory_conditional_distinstead ofcombined_dist.RooMultiPdfcan be exported, without its penalty term (warning), and comes back as aRooSimultaneous.Checklist:
Tests run (Linux, Ubuntu 26.04, GCC 15.2, RelWithDebInfo,
-Droofit=ON -Dtesting=ON):testRooFitHS3135 → 137 tests (2 new),testHistFactory60, all passing. Each commit of this PR and of the rebased #24005 was 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 original
category_conditional_distcommit; claude-opus-5-5 the split out of #24005, the structural distinction, and theRooMultiPdfexport. The commits carry the correspondingAssisted-bytrailers. The port onto the typed HS3 document of 3682174 was done by claude-opus-5-5 as well.🤖 Generated with Claude Code