Skip to content

Commit fe04644

Browse files
committed
[RF] Fix counting Asimov generation with multiple floating parameters
When generating a counting Asimov dataset, the AsymptoticCalculator identified the expected value of an observable in a Gaussian or Poisson term as "the single non-constant server that is not the observable". This heuristic failed whenever both the mean and the width of a Gaussian were floating, could not see constness through derived quantities like RooFormulaVars, and silently set the observable to the value of the *sigma* parameter when the mean was constant but sigma floated. The heuristic is unnecessary: RooGaussian and RooPoisson expose their proxies via getX() and getMean(). Use the exact (x, mean) pair and set whichever of the two is the observable to the value of the other one. The width and the constness of the parameters no longer matter, and the direction where the mean is the observable (constraint terms with global observables) keeps working. The RooMultiVarGaussian path goes through the same helper with xVec()[i] and muVec()[i]. Fixes the four failure cases from ROOT-10096, covered by a new test. Fixes [ROOT-10096](https://its.cern.ch/jira/browse/ROOT-10069) 🤖 Done with the help of AI
1 parent a26952a commit fe04644

2 files changed

Lines changed: 72 additions & 45 deletions

File tree

roofit/roostats/src/AsymptoticCalculator.cxx

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ The calculator can generate Asimov datasets from two kinds of PDFs:
6969

7070
#include "TStopwatch.h"
7171

72-
#include <ROOT/RSpan.hxx>
73-
7472
using namespace RooStats;
7573
using std::string, std::unique_ptr;
7674

@@ -939,41 +937,31 @@ void FillBins(const RooAbsPdf & pdf, const RooArgList &obs, RooAbsData & data, i
939937

940938
}
941939

942-
bool setObsToExpected(std::span<RooAbsArg *> servers, const RooArgSet &obs, std::string const &errPrefix)
940+
bool setObsToExpected(RooAbsArg &x, RooAbsArg &mean, const RooArgSet &obs, std::string const &errPrefix)
943941
{
944-
RooRealVar *myobs = nullptr;
945-
RooAbsReal *myexp = nullptr;
946-
for (RooAbsArg *a : servers) {
947-
if (obs.contains(*a)) {
948-
if (myobs != nullptr) {
949-
oocoutF(nullptr,Generation) << errPrefix << "Has two observables ?? " << std::endl;
950-
return false;
951-
}
952-
myobs = dynamic_cast<RooRealVar *>(a);
953-
if (myobs == nullptr) {
954-
oocoutF(nullptr,Generation) << errPrefix << "Observable is not a RooRealVar??" << std::endl;
955-
return false;
956-
}
957-
} else {
958-
if (!a->isConstant() ) {
959-
if (myexp != nullptr) {
960-
oocoutE(nullptr,Generation) << errPrefix << "Has two non-const arguments " << std::endl;
961-
return false;
962-
}
963-
myexp = dynamic_cast<RooAbsReal *>(a);
964-
if (myexp == nullptr) {
965-
oocoutF(nullptr,Generation) << errPrefix << "Expected is not a RooAbsReal??" << std::endl;
966-
return false;
967-
}
968-
}
969-
}
942+
// Figure out which of the two arguments is the observable that should be
943+
// set to the expected value given by the other one. Usually the observable
944+
// is "x", but also the mean parameter can be the observable: this happens
945+
// for example in constraint terms, where the global observable takes the
946+
// role of the mean.
947+
const bool xIsObs = obs.contains(x);
948+
const bool meanIsObs = obs.contains(mean);
949+
if (xIsObs && meanIsObs) {
950+
oocoutF(nullptr, Generation) << errPrefix << "Has two observables ?? " << std::endl;
951+
return false;
952+
}
953+
if (!xIsObs && !meanIsObs) {
954+
oocoutF(nullptr, Generation) << errPrefix << "No observable?" << std::endl;
955+
return false;
970956
}
971-
if (myobs == nullptr) {
972-
oocoutF(nullptr,Generation) << errPrefix << "No observable?" << std::endl;
957+
auto *myobs = dynamic_cast<RooRealVar *>(xIsObs ? &x : &mean);
958+
auto *myexp = dynamic_cast<RooAbsReal *>(xIsObs ? &mean : &x);
959+
if (myobs == nullptr) {
960+
oocoutF(nullptr, Generation) << errPrefix << "Observable is not a RooRealVar??" << std::endl;
973961
return false;
974962
}
975963
if (myexp == nullptr) {
976-
oocoutF(nullptr,Generation) << errPrefix << "No observable?" << std::endl;
964+
oocoutF(nullptr, Generation) << errPrefix << "Expected is not a RooAbsReal??" << std::endl;
977965
return false;
978966
}
979967

@@ -989,33 +977,34 @@ bool setObsToExpected(std::span<RooAbsArg *> servers, const RooArgSet &obs, std:
989977
////////////////////////////////////////////////////////////////////////////////
990978
/// set observed value to the expected one
991979
/// works for Gaussian, Poisson or LogNormal
992-
/// assumes mean parameter value is the argument not constant and not depending on observables
993-
/// (if more than two arguments are not constant will use first one but print a warning !)
994980
/// need to iterate on the components of the Poisson to get n and nu (nu can be a RooAbsReal)
995981
/// (code from G. Petrucciani and extended by L.M.)
996982

997-
bool SetObsToExpected(RooAbsPdf &pdf, const RooArgSet &obs)
983+
bool SetObsToExpected(RooGaussian &pdf, const RooArgSet &obs)
998984
{
999985
std::string const &errPrefix = "AsymptoticCalculator::SetObsExpected( " + std::string{pdf.ClassName()} + " ) : ";
1000-
std::vector<RooAbsArg *> servers;
1001-
for (RooAbsArg *a : pdf.servers()) {
1002-
servers.emplace_back(a);
1003-
}
1004-
return setObsToExpected(servers, obs, errPrefix);
986+
return setObsToExpected(const_cast<RooAbsReal &>(pdf.getX()), const_cast<RooAbsReal &>(pdf.getMean()), obs,
987+
errPrefix);
988+
}
989+
990+
bool SetObsToExpected(RooPoisson &pdf, const RooArgSet &obs)
991+
{
992+
std::string const &errPrefix = "AsymptoticCalculator::SetObsExpected( " + std::string{pdf.ClassName()} + " ) : ";
993+
return setObsToExpected(const_cast<RooAbsReal &>(pdf.getX()), const_cast<RooAbsReal &>(pdf.getMean()), obs,
994+
errPrefix);
1005995
}
1006996

1007997
bool setObsToExpectedMultiVarGauss(RooMultiVarGaussian &mvgauss, const RooArgSet &obs)
1008998
{
1009999
// In the case of the multi-variate Gaussian, we need to iterate over the
1010-
// dimensions and treat the servers for each dimension separately.
1000+
// dimensions and treat the observable and mean for each dimension
1001+
// separately.
10111002

10121003
std::string const &errPrefix = "AsymptoticCalculator::SetObsExpected( " + std::string{mvgauss.ClassName()} + " ) : ";
1013-
std::vector<RooAbsArg *> servers{nullptr, nullptr};
10141004
bool ret = true;
10151005
for (std::size_t iDim = 0; iDim < mvgauss.xVec().size(); ++iDim) {
1016-
servers[0] = &mvgauss.xVec()[iDim];
1017-
servers[1] = &mvgauss.muVec()[iDim];
1018-
ret &= setObsToExpected(servers, obs, errPrefix + " : dim " + std::to_string(iDim) + " ");
1006+
ret &= setObsToExpected(mvgauss.xVec()[iDim], mvgauss.muVec()[iDim], obs,
1007+
errPrefix + " : dim " + std::to_string(iDim) + " ");
10191008
}
10201009
return ret;
10211010
}

roofit/roostats/test/testAsymptoticCalculator.cxx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,41 @@ TEST(AsymptoticCalculator, SignedTestStatistic)
109109
std::unique_ptr<HypoTestResult> resRightSideCapped{calc.GetHypoTest()};
110110
EXPECT_NEAR(resRightSideCapped->NullPValue(), resRightSide->NullPValue(), 1e-6);
111111
}
112+
113+
// Check that counting Asimov datasets can be generated no matter which
114+
// parameters are floating and even if the mean or width of a Gaussian are
115+
// derived quantities (covers JIRA ROOT-10096).
116+
TEST(AsymptoticCalculator, CountingAsimovDataSetFloatingParams)
117+
{
118+
RooWorkspace ws;
119+
ws.factory("obs[10.0, 0.0, 1000.0]");
120+
ws.factory("Poisson::poisson(obs, mean[20.0, 0.0, 1000.0])");
121+
ws.factory("Gaussian::gauss1(obs, mean, sigma[3.0, 1.0, 10.0])");
122+
ws.factory("expr::sqrt_mean('sqrt(@0)', mean)");
123+
ws.factory("Gaussian::gauss2(obs, mean, sqrt_mean)");
124+
ws.factory("expr::mean2('2 * @0', mean)");
125+
ws.factory("expr::sqrt_mean2('sqrt(@0)', mean2)");
126+
ws.factory("Gaussian::gauss3(obs, mean2, sqrt_mean2)");
127+
128+
RooArgSet observables{*ws.var("obs")};
129+
130+
auto checkAsimov = [&](const char *pdfName, double expectedObsVal) {
131+
std::unique_ptr<RooAbsData> data{
132+
RooStats::AsymptoticCalculator::GenerateAsimovData(*ws.pdf(pdfName), observables)};
133+
ASSERT_NE(data, nullptr) << pdfName;
134+
ASSERT_EQ(data->numEntries(), 1) << pdfName;
135+
EXPECT_DOUBLE_EQ(data->get(0)->getRealValue("obs"), expectedObsVal) << pdfName;
136+
};
137+
138+
checkAsimov("poisson", 20.0);
139+
// Both mean and sigma floating: used to fail with "Has two non-const arguments".
140+
checkAsimov("gauss1", 20.0);
141+
// Width derived from the mean: also used to fail, with no workaround for gauss3.
142+
checkAsimov("gauss2", 20.0);
143+
checkAsimov("gauss3", 40.0);
144+
145+
// With a constant mean and a floating sigma, the old server-based heuristic
146+
// silently set the observable to the value of the sigma parameter.
147+
ws.var("mean")->setConstant(true);
148+
checkAsimov("gauss1", 20.0);
149+
}

0 commit comments

Comments
 (0)