Skip to content

[RF] Make RooSimultaneous deal with categorical model parameters - #23142

Merged
guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:roofit-simpdf-fixes
Aug 27, 2026
Merged

guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:roofit-simpdf-fixes

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

This PR contains the necessary commits to make the RooSimultaneous work with categorical model parameter, i.e. RooCategory arguments that are not part of the fit dataset.

As a result, the RooSimultaneous behaves now like a RooMultiPdf without the penalty term.

A RooSimultaneous can appear as a node inside a compiled compute graph
when its index category is not an observable: it then acts as a "switch"
that evaluates to the component selected by the current category state,
analogous to RooMultiPdf. With an observable index category, the
likelihood is decomposed into per-channel terms instead and the
RooSimultaneous itself never reaches code generation.

Previously, such a node fell through to the generic codegenImpl()
fallback, which emits a constant "1.0" and only prints an error. The
generated likelihood was silently wrong: plain evaluation through the
RooEvaluatorWrapper still used the CPU evaluator and looked fine, but
the codegen backend differentiated the wrong function, and with
EvalBackend::CodegenNoGrad() the wrong values became directly visible.

Emit a nested ternary expression keyed on the actual category state
indices, like the RooMultiPdf implementation. The unsupported cases
(extendable components, whose scalar evaluation applies a relative yield
weight, and derived index categories) now throw a clear error instead of
generating wrong code.

🤖 Done with the help of AI
A RooSimultaneous whose index category is not among the observables acts
as a "switch" that evaluates to the component selected by the current
index state, analogous to RooMultiPdf (see the discussion in GitHub
issue root-project#22916). The legacy evaluation backend already handles this
correctly, and the discrete-profiling loop in RooMinimizer picks up any
floating RooCategory parameter, so such a pdf can be used for discrete
profiling when nested inside another model.

However, RooSimultaneous::compileForNormSet() unconditionally applied
the channel-splitting transformation, which renames the per-channel
observables with a "_<channel>_" prefix so that they can be filled from
split datasets. For a parameter-index RooSimultaneous there is no
splitting by category, the renamed observables never received data, and
the CPU backend silently computed wrong likelihood values.

Compile like an ordinary self-normalized pdf when no component of the
index category is in the normalization set. Together with the previous
commit, all three backends now agree with the legacy backend and with
the equivalent RooMultiPdf to machine precision, which is covered by the
new test.

🤖 Done with the help of AI
Creating a test statistic directly from a RooSimultaneous whose index
category is not among the data columns used to fail on all backends,
because the fitting infrastructure unconditionally tried to split the
dataset by the index category. Such a "switch"-mode RooSimultaneous (see
the previous commit) has no channels to split the data into: the index
category acts as a parameter of the fit, like for RooMultiPdf.

Introduce RooSimultaneous::indexCatIsObservable() as the single
predicate for deciding between the two modes (also used by
compileForNormSet() now), and consult it at every place that assumed
"RooSimultaneous implies channel splitting":

  - BatchModeDataHelpers::getDataSpans() ignores the simPdf argument if
    there is nothing to split by, covering the data loading of both
    RooEvaluatorWrapper::setData() and RooFuncWrapper::loadData()
  - the NLL and chi2 construction in FitHelpers, which now build an
    ordinary RooNLLVarNew over the full pdf instead of per-channel terms
  - the legacy RooAbsTestStatistic constructors, which now pick the
    SimMaster mode only for an actual simultaneous fit

With this, a parameter-index RooSimultaneous can be used in place of
RooMultiPdf for the discrete profiling method, both nested inside a
larger model and as the top-level pdf: the NLL values agree with the
NLL of the component pdf selected by the index state, and a fit with a
floating index category profiles the discrete choice like RooMultiPdf
does (without the penalty term, which RooMultiPdf adds on top).

Closes the remaining gap from the discussion in GitHub issue root-project#22916.

🤖 Done with the help of AI
@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 16h 26m 18s ⏱️
 3 857 tests  3 855 ✅ 0 💤 2 ❌
79 492 runs  79 484 ✅ 6 💤 2 ❌

For more details on these failures, see this check.

Results for commit c6953ee.

@lmoneta lmoneta left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
Thank you Jonas for implementing this case (categories as model parameters) in the RooSimultaneous pdf

@guitargeek
guitargeek merged commit aed138d into root-project:master Aug 27, 2026
33 of 38 checks passed
@guitargeek
guitargeek deleted the roofit-simpdf-fixes branch August 27, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants