Check duplicate issues.
Description
Consider a RooSimultaneous over some category (e.g. region) that is itself put into an outer RooSimultaneous over a second category (e.g. analysis), alongside a plain pdf that does not depend on region. RooFit flattens the two levels into one, indexed by the super-category analysis x region. The model is built from three pdfs, but the super-category has four states, and the pdf without a region split is the one selected in two of them. If all pdfs are extended, its expected number of events is therefore counted twice.
This is the shape of an analysis combination: analysis A fits a signal and a control region simultaneously, analysis B measures the same signal in a single region. Below A contributes 10 and 50 events and B contributes 40, so the intended total is 100. Tested with ROOT 6.40.02, sole dependency ROOT.
Consequences
{B;CR} does not exist — analysis B has no control region — but pdf_B is selected there and contributes its yield a second time:
| super-category state |
pdf |
expectedEvents |
{A;SR} |
pdf_A_SR |
10 |
{A;CR} |
pdf_A_CR |
50 |
{B;SR} |
pdf_B |
40 |
{B;CR} |
pdf_B |
40 |
Nothing records that a pdf has already contributed, so everything that sums over the index is affected alike:
|
actual |
if pdf_B counted once |
comb.expectedEvents({x, comb.indexCat()}) |
140 |
100 |
generated analysis=B fraction |
0.570 (= 80/140) |
0.400 |
fitted nB |
20 |
40 |
Only nB is biased: the extended term expects it in both B states while the data has 40 events in one and none in the other, minimizing at nB = 20. The factor is the number of states of the inner category. Non-extended nesting is unaffected, one state being active at a time and each normalized on its own.
Note also that analysis and region are no longer servers of comb after the flattening, so comb.expectedEvents({x, analysis, region}) returns the current state's yield rather than the sum.
Question
Is the right-hand column intended? I'd assume a component that does not depend on the inner category should contribute its yield once (total 100, generated B fraction 0.4) so the flattening should either deduplicate it or split its yield across the replicated states.
If the replication is deliberate instead, 140 is correct and the problem is only that nesting silently changes the model. Should the initialize() INFO then be a warning when a component does not depend on all index categories?
Workarounds for the former: one flat RooSimultaneous over the super-category with a dummy/zero pdf for the combinations that do not exist, or pdf_B made a RooSimultaneous over region too, with its yield split explicitly.
Reproducer
import ROOT
x = ROOT.RooRealVar("x", "x", -8, 8)
mean = ROOT.RooRealVar("mean", "mean", 0)
sigma = ROOT.RooRealVar("sigma", "sigma", 2)
shape = ROOT.RooGaussian("shape", "shape", x, mean, sigma)
# analysis A: signal region + control region. analysis B: one region only.
nA_SR = ROOT.RooRealVar("nA_SR", "nA_SR", 10, 0, 1e6)
nA_CR = ROOT.RooRealVar("nA_CR", "nA_CR", 50, 0, 1e6)
nB = ROOT.RooRealVar("nB", "nB", 40, 0, 1e6)
pdf_A_SR = ROOT.RooExtendPdf("pdf_A_SR", "pdf_A_SR", shape, nA_SR)
pdf_A_CR = ROOT.RooExtendPdf("pdf_A_CR", "pdf_A_CR", shape, nA_CR)
pdf_B = ROOT.RooExtendPdf("pdf_B", "pdf_B", shape, nB)
region = ROOT.RooCategory("region", "region", {"SR": 0, "CR": 1})
analysis = ROOT.RooCategory("analysis", "analysis", {"A": 0, "B": 1})
anaA = ROOT.RooSimultaneous("anaA", "anaA", {"SR": pdf_A_SR, "CR": pdf_A_CR}, region)
comb = ROOT.RooSimultaneous("comb", "comb", {"A": anaA, "B": pdf_B}, analysis)
ROOT.RooMsgService.instance().setGlobalKillBelow(ROOT.RooFit.WARNING) # quiet the fit
for state in comb.indexCat():
label = str(state.first)
print(f"{label:<8} -> {comb.getPdf(label).GetName()}")
print("expected:", comb.expectedEvents(ROOT.RooArgSet(x, comb.indexCat())), "(intended 100)")
# toy generation follows the inflated total
toy = comb.generate(ROOT.RooArgSet(x, analysis, region), 100_000)
for a, r in (("A", "SR"), ("A", "CR"), ("B", "SR"), ("B", "CR")):
n = toy.sumEntries(f"analysis==analysis::{a} && region==region::{r}")
print(f"generated {{{a};{r}}}: {n / toy.numEntries():.3f}")
# an extended fit to the dataset the combination actually has: A in SR and CR,
# B in its single region, nothing in {B;CR}
w = ROOT.RooRealVar("w", "w", 0, 1e6)
data = ROOT.RooDataSet("data", "data", ROOT.RooArgSet(x, analysis, region, w), WeightVar=w)
for a, r, n in (("A", "SR", 10), ("A", "CR", 50), ("B", "SR", 40)):
analysis.setLabel(a), region.setLabel(r), x.setVal(0)
data.add(ROOT.RooArgSet(x, analysis, region), n)
comb.fitTo(data, Extended=True, PrintLevel=-1)
for v, truth in ((nA_SR, 10), (nA_CR, 50), (nB, 40)):
print(f"fitted {v.GetName()}: {v.getVal():.1f} (truth {truth})")
[#1] INFO:InputArguments -- RooSimultaneous::initialize(comb) INFO: one or more input
component of simultaneous p.d.f.s are simultaneous p.d.f.s themselves, rewriting composite
expressions as one-level simultaneous p.d.f. in terms of final constituents and extended
index category
{A;CR} -> pdf_A_CR
{A;SR} -> pdf_A_SR
{B;CR} -> pdf_B
{B;SR} -> pdf_B
expected: 140.0 (intended 100)
generated {A;SR}: 0.072
generated {A;CR}: 0.358
generated {B;SR}: 0.285
generated {B;CR}: 0.284
fitted nA_SR: 10.0 (truth 10)
fitted nA_CR: 50.0 (truth 50)
fitted nB: 20.0 (truth 40)
ROOT version
| Welcome to ROOT 6.40.02 https://root.cern |
| (c) 1995-2025, The ROOT Team; conception: R. Brun, F. Rademakers |
| Built for macosxarm64 on Aug 31 2026, 16:31:09 |
| From tags/6-40-02@6-40-02 |
| With std202302 |
| Try '.help'/'.?', '.demo', '.license', '.credits', '.quit'/'.q' |
Installation method
conda-forge https://conda.anaconda.org/conda-forge/osx-arm64/root-6.40.02-py314h5c24228_4.conda
Operating system
macOS 26.6.1
Additional context
No response
Check duplicate issues.
Description
Consider a
RooSimultaneousover some category (e.g.region) that is itself put into an outerRooSimultaneousover a second category (e.g.analysis), alongside a plain pdf that does not depend onregion. RooFit flattens the two levels into one, indexed by the super-categoryanalysis x region. The model is built from three pdfs, but the super-category has four states, and the pdf without aregionsplit is the one selected in two of them. If all pdfs are extended, its expected number of events is therefore counted twice.This is the shape of an analysis combination: analysis A fits a signal and a control region simultaneously, analysis B measures the same signal in a single region. Below A contributes 10 and 50 events and B contributes 40, so the intended total is 100. Tested with ROOT 6.40.02, sole dependency ROOT.
Consequences
{B;CR}does not exist — analysis B has no control region — butpdf_Bis selected there and contributes its yield a second time:{A;SR}pdf_A_SR{A;CR}pdf_A_CR{B;SR}pdf_B{B;CR}pdf_BNothing records that a pdf has already contributed, so everything that sums over the index is affected alike:
pdf_Bcounted oncecomb.expectedEvents({x, comb.indexCat()})analysis=BfractionnBOnly
nBis biased: the extended term expects it in both B states while the data has 40 events in one and none in the other, minimizing atnB = 20. The factor is the number of states of the inner category. Non-extended nesting is unaffected, one state being active at a time and each normalized on its own.Note also that
analysisandregionare no longer servers ofcombafter the flattening, socomb.expectedEvents({x, analysis, region})returns the current state's yield rather than the sum.Question
Is the right-hand column intended? I'd assume a component that does not depend on the inner category should contribute its yield once (total 100, generated B fraction 0.4) so the flattening should either deduplicate it or split its yield across the replicated states.
If the replication is deliberate instead, 140 is correct and the problem is only that nesting silently changes the model. Should the
initialize()INFO then be a warning when a component does not depend on all index categories?Workarounds for the former: one flat
RooSimultaneousover the super-category with a dummy/zero pdf for the combinations that do not exist, orpdf_Bmade aRooSimultaneousoverregiontoo, with its yield split explicitly.Reproducer
ROOT version
| Welcome to ROOT 6.40.02 https://root.cern |
| (c) 1995-2025, The ROOT Team; conception: R. Brun, F. Rademakers |
| Built for macosxarm64 on Aug 31 2026, 16:31:09 |
| From tags/6-40-02@6-40-02 |
| With std202302 |
| Try '.help'/'.?', '.demo', '.license', '.credits', '.quit'/'.q' |
Installation method
conda-forge https://conda.anaconda.org/conda-forge/osx-arm64/root-6.40.02-py314h5c24228_4.conda
Operating system
macOS 26.6.1
Additional context
No response