I’ve investigated the issue for a while. TL;DR: RooRombergIntegrator is used for integration, and I suspect that it is buggy.
Test setup
MyExpo.cxx (463 Bytes)
MyExpo.hxx (1.1 KB)
MyLinkDef.hxx (161 Bytes)
roofit_test_create_projection.cxx (911 Bytes)
ROOT v6.36.04
Built for linuxx8664gcc on Sep 04 2025, 09:14:25
From tags/6-36-04@6-36-04
With clang version 19.1.7 (AlmaLinux OS Foundation 19.1.7-2.el9)
Binary directory: /opt/root-6.36.04-debug/bin
Compilation
rootcling -f G_MyExpo.cxx -c MyExpo.hxx MyLinkDef.hxx
g++ -std=c++17 -g -O0 -fPIE -Wall -Wextra -pedantic -Werror \
-o roofit_test_create_projection \
roofit_test_create_projection.cxx MyExpo.cxx G_MyExpo.cxx \
$(root-config --libs --cflags) -lRooFit -lRooFitCore
Result
$ ./roofit_test_create_projection
[#1] INFO:NumericIntegration -- RooRealIntegral::init(expo_Int[lambda]) using numeric integrator RooIntegrator1D to calculate Int(lambda)
---
[#1] INFO:NumericIntegration -- RooRealIntegral::init(expo_Int[lambda]_Norm[lambda]) using numeric integrator RooIntegrator1D to calculate Int(lambda)
x = 2.33755
marginal_x = 1
---
x = 2.33765
marginal_x = 7150.76
---
Debug attempt
gdb reveals RooRombergIntegrator under the hood
$ gdb roofit_test_create_projection
(gdb) start
(gdb) break RooRombergIntegrator.cxx:163
(gdb) continue
(gdb) backtrace
#0 RooFit::Detail::integrate1d(std::function<double (double)>, bool, int, int, int, double, double, bool, double, double, std::__ROOT::span<double>, std::__ROOT::span<double>) (func=..., doTrapezoid=true, maxSteps=20, minStepsZero=999,
fixSteps=0, epsAbs=9.9999999999999995e-08, epsRel=9.9999999999999995e-08, doExtrap=true, xmin=0, xmax=100, hArr=..., sArr=...)
at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRombergIntegrator.cxx:167
#1 0x00007ffff41b407b in RooRombergIntegrator::integral (this=0x24a7620, iDim=0, nSeg=1, wksp=...) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRombergIntegrator.cxx:540
#2 0x00007ffff41b3c30 in RooRombergIntegrator::integral (this=0x24a7620, yvec=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRombergIntegrator.cxx:488
#3 0x00007ffff3f511a5 in RooAbsIntegrator::calculate (this=0x24a7620, yvec=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsIntegrator.cxx:54
#4 0x00007ffff419a102 in RooRealIntegral::integrate (this=0x24a61c0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRealIntegral.cxx:977
#5 0x00007ffff419a012 in RooRealIntegral::sum (this=0x24a61c0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRealIntegral.cxx:963
#6 0x00007ffff4198f05 in RooRealIntegral::evaluate (this=0x24a61c0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRealIntegral.cxx:846
#7 0x00007ffff3f6c6b1 in RooAbsReal::traceEval (this=0x24a61c0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsReal.cxx:320
#8 0x00007ffff4198b75 in RooRealIntegral::getValV (this=0x24a61c0, nset=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRealIntegral.cxx:799
#9 0x000000000040bc31 in RooAbsReal::getVal (this=0x24a61c0, normalisationSet=0x0) at /opt/root-6.36.04-debug/include/RooAbsReal.h:117
#10 0x00007ffff3f5ad08 in RooAbsPdf::syncNormalization (this=0x7fffffffc550, nset=0x24441a0, adjustProxies=true) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsPdf.cxx:541
#11 0x00007ffff3f59a64 in RooAbsPdf::getValV (this=0x7fffffffc550, nset=0x24441a0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsPdf.cxx:334
#12 0x000000000040bc31 in RooAbsReal::getVal (this=0x7fffffffc550, normalisationSet=0x24441a0) at /opt/root-6.36.04-debug/include/RooAbsReal.h:117
#13 0x00007ffff4195656 in RooRealIntegral::RooRealIntegral (this=0x24354d0, name=0x24354a0 "expo_Int[lambda]_Norm[lambda]", title=0x241d370 "Integral of expo", function=..., depList=..., funcNormSet=0x7fffffffc280, config=0x0,
rangeName=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooRealIntegral.cxx:519
#14 0x00007ffff3f8eaa1 in std::make_unique<RooRealIntegral, char const*, char const*, RooAbsReal const&, RooArgSet&, RooArgSet const*&, RooNumIntConfig const*&, char const*&> (__args=@0x7fffffffbb70: 0x0, __args=@0x7fffffffbb70: 0x0,
__args=@0x7fffffffbb70: 0x0, __args=@0x7fffffffbb70: 0x0, __args=@0x7fffffffbb70: 0x0, __args=@0x7fffffffbb70: 0x0, __args=@0x7fffffffbb70: 0x0)
at /opt/rh/gcc-toolset-14/root/usr/lib/gcc/x86_64-redhat-linux/14/../../../../include/c++/14/bits/unique_ptr.h:1077
#15 0x00007ffff3f6eaa9 in RooAbsReal::createIntObj (this=0x7fffffffc550, iset2=..., nset2=0x7fffffffc280, cfg=0x0, rangeName=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsReal.cxx:634
#16 0x00007ffff3f6dcb1 in RooAbsReal::createIntegral (this=0x7fffffffc550, iset=..., nset=0x7fffffffc280, cfg=0x0, rangeName=0x0) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsReal.cxx:557
#17 0x00007ffff41859d8 in RooProjectedPdf::getProjection (this=0x2430c30, iset=0x7fffffffd220, nset=0x0, rangeName=0x0, code=@0x7fffffffc3d8: -137947173)
at /opt/root-6.36.04-source/roofit/roofitcore/src/RooProjectedPdf.cxx:126
#18 0x00007ffff4185647 in RooProjectedPdf::RooProjectedPdf (this=0x2430c30, name=0x241c430 "expo_Proj[lambda]", title=0x241c430 "expo_Proj[lambda]", _intpdf=..., intObs=...)
at /opt/root-6.36.04-source/roofit/roofitcore/src/RooProjectedPdf.cxx:64
#19 0x00007ffff3f63ca8 in RooAbsPdf::createProjection (this=0x7fffffffc550, iset=...) at /opt/root-6.36.04-source/roofit/roofitcore/src/RooAbsPdf.cxx:2450
#20 0x000000000040b6ae in main () at roofit_test_create_projection.cxx:13
Discussion
Apparently RooRombergIntegrator implementation, which is used by default, has either numerical stability issues or some bugs. I’ve narrowed down the problem to this class by inserting its code into another test setup, which I can send if needed.
RooRombergIntegrator has integrate1d
as main routine, which iteratively calculates integral sum via addTrapezoids
for finer and finer grids. It also uses extrapolate
to improve accuracy using previously calculated results.
After a glance at source code and documentation I’ve noted several weird or unclear things:
- Reference page for RooRombergIntegrator is available only for 6.34 as of now, but the class is still present in 6.36.
- The class implementation uses
std::span
, which was introduced in C++20, however, I’ve built my debug ROOT version with C++17 without problems.
extrapolate
function looks most suspicious to me, as I don’t understand its code completely, and I didn’t notice anything wrong in other functions. Documentation suggests that Richardson extrapolation is applied only fixed amount of times, contrary to Wiki implementation which applies it more and more times as grid step shrinks. In theory this fixed number of extrapolations reduces accuracy but might avoid stability issues with higher order extrapolation.
I hope that this information will be useful!