Skip to content

Commit 8d9620e

Browse files
committed
[RF] Let RooGenericPdf/RooFormulaVar declare a piecewise-flat binning
A RooGenericPdf or RooFormulaVar that is constant within bins of an observable (a step function) was always integrated with the generic adaptive numeric integrator, which is needlessly expensive: for a flat distribution the integral is just the sum of each bin's value times its width. Add RooGenericPdf::setBinBoundaries() and RooFormulaVar::setBinBoundaries(), which take a RooAbsBinning for an observable and declare the function to be flat within those bins. Once set: * isBinnedDistribution() reports the observable as binned, so RooRealIntegral selects the fast RooBinIntegrator; * binBoundaries() and plotSamplingHint() expose the bins so that integration covers exactly the range and plotting draws crisp steps. A binning can be registered for several observables (e.g. for a multi-dimensional flat distribution). By default the function is sampled inside each bin to verify that it really is flat, and the binning is rejected with an error otherwise; pass checkFlatness=false to skip this. The binnings are stored in a std::map keyed by the observable's index in the internal variable list, so they survive renaming of a variable or a server redirection, and a RooUniformBinning keeps the storage compact even for many bins. Lookups resolve the observable by name, so a same-named stand-in (e.g. one read back separately from a file) is accepted too. The shared flatness check and bin-boundary helpers live in RooHelpers. The persistent schema version of both classes is bumped to 2.
1 parent 9430767 commit 8d9620e

8 files changed

Lines changed: 493 additions & 7 deletions

File tree

roofit/roofitcore/inc/RooFormulaVar.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
#include "RooArgList.h"
2121
#include "RooListProxy.h"
2222
#include "RooTrace.h"
23+
#include "RooAbsBinning.h"
2324

2425
#include <memory>
2526
#include <list>
27+
#include <map>
28+
#include <string>
2629

2730
class RooArgSet ;
2831
class RooFormula ;
32+
class RooAbsRealLValue;
2933

3034
class RooFormulaVar : public RooAbsReal {
3135
public:
@@ -68,6 +72,14 @@ class RooFormulaVar : public RooAbsReal {
6872

6973
double defaultErrorLevel() const override ;
7074

75+
// Declare this function to be piecewise constant (flat) within the bins of
76+
// the given `binning` of observable `obs`. This lets integration use the fast
77+
// bin integrator instead of the generic numeric integrator. A binning can be
78+
// set for more than one observable. Use a RooUniformBinning to describe many
79+
// uniform bins compactly.
80+
void setBinBoundaries(RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness = true);
81+
82+
bool isBinnedDistribution(const RooArgSet &obs) const override;
7183
std::list<double>* binBoundaries(RooAbsRealLValue& /*obs*/, double /*xlo*/, double /*xhi*/) const override ;
7284
std::list<double>* plotSamplingHint(RooAbsRealLValue& /*obs*/, double /*xlo*/, double /*xhi*/) const override ;
7385

@@ -94,7 +106,10 @@ class RooFormulaVar : public RooAbsReal {
94106
mutable RooArgSet* _nset{nullptr}; ///<! Normalization set to be passed along to contents
95107
TString _formExpr ; ///< Formula expression string
96108

97-
ClassDefOverride(RooFormulaVar,1) // Real-valued function of other RooAbsArgs calculated by a TFormula expression
109+
std::map<int, std::unique_ptr<RooAbsBinning>> _binnings; ///< User-defined binnings, keyed by the observable's index
110+
///< in _actualVars, for a piecewise-flat distribution
111+
112+
ClassDefOverride(RooFormulaVar, 2) // Real-valued function of other RooAbsArgs calculated by a TFormula expression
98113
};
99114

100115
#endif

roofit/roofitcore/inc/RooGenericPdf.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@
1818

1919
#include "RooAbsPdf.h"
2020
#include "RooListProxy.h"
21+
#include "RooAbsBinning.h"
22+
23+
#include <map>
24+
#include <memory>
25+
#include <string>
2126

2227
class RooArgList ;
2328
class RooFormula ;
29+
class RooAbsRealLValue;
2430

2531
class RooGenericPdf : public RooAbsPdf {
2632
public:
@@ -61,8 +67,18 @@ class RooGenericPdf : public RooAbsPdf {
6167

6268
std::string getUniqueFuncName() const;
6369

64-
protected:
70+
// Declare this pdf to be piecewise constant (flat) within the bins of the
71+
// given `binning` of observable `obs`. This lets the integration use the fast
72+
// bin integrator instead of the generic numeric integrator. A binning can be
73+
// set for more than one observable. Use a RooUniformBinning to describe many
74+
// uniform bins compactly.
75+
void setBinBoundaries(RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness = true);
76+
77+
bool isBinnedDistribution(const RooArgSet &obs) const override;
78+
std::list<double> *binBoundaries(RooAbsRealLValue &obs, double xlo, double xhi) const override;
79+
std::list<double> *plotSamplingHint(RooAbsRealLValue &obs, double xlo, double xhi) const override;
6580

81+
protected:
6682
RooFormula& formula() const ;
6783

6884
// Function evaluation
@@ -78,7 +94,10 @@ class RooGenericPdf : public RooAbsPdf {
7894
mutable RooFormula * _formula = nullptr; ///<! Formula engine
7995
TString _formExpr ; ///< Formula expression string
8096

81-
ClassDefOverride(RooGenericPdf,1) // Generic PDF defined by string expression and list of variables
97+
std::map<int, std::unique_ptr<RooAbsBinning>> _binnings; ///< User-defined binnings, keyed by the observable's index
98+
///< in _actualVars, for a piecewise-flat distribution
99+
100+
ClassDefOverride(RooGenericPdf, 2) // Generic PDF defined by string expression and list of variables
82101
};
83102

84103
#endif

roofit/roofitcore/inc/RooHelpers.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
#include <RooAbsArg.h>
1818
#include <RooAbsReal.h>
1919

20+
#include <ROOT/RSpan.hxx>
21+
2022
#include <sstream>
23+
#include <list>
2124
#include <vector>
2225
#include <string>
2326
#include <utility>
2427

2528
class RooAbsPdf;
2629
class RooAbsData;
30+
class RooAbsRealLValue;
2731

2832
namespace RooHelpers {
2933

@@ -91,6 +95,22 @@ void checkRangeOfParameters(const RooAbsReal *callingClass, std::initializer_lis
9195
/// set all RooRealVars to constants. return true if at least one changed status
9296
bool setAllConstant(const RooAbsCollection &coll, bool constant = true);
9397

98+
/// Check that `function` is constant (flat) inside each bin defined by the
99+
/// sorted `boundaries` when scanning the observable `obs`. Several interior
100+
/// points are sampled per bin and compared to the bin's first sample; if any
101+
/// of them deviates by more than `relTol` (relative to the value scale), the
102+
/// function is not flat and false is returned. The value of `obs` is restored
103+
/// on return.
104+
bool isFunctionFlatInBins(const RooAbsReal &function, RooAbsRealLValue &obs, std::span<const double> boundaries,
105+
double relTol = 1e-9);
106+
107+
/// Return a newly allocated list with the subset of `boundaries` that lies
108+
/// strictly inside [`xlo`, `xhi`], with `xlo` and `xhi` added as the first and
109+
/// last entries. This is the form expected by RooFit's binBoundaries()
110+
/// interface, so the bin integrator covers exactly the integration range.
111+
/// The caller takes ownership of the returned list.
112+
std::list<double> *binBoundariesInRange(std::span<const double> boundaries, double xlo, double xhi);
113+
94114
} // namespace RooHelpers
95115

96116
#endif

roofit/roofitcore/src/RooFormulaVar.cxx

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
#include "RooMsgService.h"
5353
#include "RooTrace.h"
5454
#include "RooFormula.h"
55+
#include "RooAbsRealLValue.h"
56+
#include "RooAbsBinning.h"
57+
#include "RooCurve.h"
58+
#include "RooHelpers.h"
5559

5660
#ifdef ROOFIT_LEGACY_EVAL_BACKEND
5761
#include "RooNLLVar.h"
@@ -123,6 +127,9 @@ RooFormulaVar::RooFormulaVar(const RooFormulaVar& other, const char* name) :
123127
_actualVars("actualVars",this,other._actualVars),
124128
_formExpr(other._formExpr)
125129
{
130+
for (auto const &item : other._binnings) {
131+
_binnings[item.first] = std::unique_ptr<RooAbsBinning>{item.second->clone()};
132+
}
126133
if (other._formula && other._formula->ok()) {
127134
_formula = new RooFormula(*other._formula);
128135
_formExpr = _formula->formulaString().c_str();
@@ -228,13 +235,74 @@ void RooFormulaVar::writeToStream(ostream& os, bool compact) const
228235
}
229236
}
230237

238+
////////////////////////////////////////////////////////////////////////////////
239+
/// Declare that this function is piecewise constant (flat) within the bins of
240+
/// the given `binning` of the observable `obs`, which must be one of the formula
241+
/// variables. The method can be called several times to set a binning for more
242+
/// than one observable. See RooGenericPdf::setBinBoundaries() for details.
243+
244+
void RooFormulaVar::setBinBoundaries(RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness)
245+
{
246+
// Match the observable to a formula variable by name, so that a same-named
247+
// stand-in for the actual server is accepted too.
248+
const int idx = _actualVars.index(obs.GetName());
249+
if (idx < 0) {
250+
coutE(InputArguments) << "RooFormulaVar::setBinBoundaries(" << GetName() << ") the observable " << obs.GetName()
251+
<< " is not one of the formula variables of this function, nothing done." << std::endl;
252+
return;
253+
}
254+
255+
if (checkFlatness) {
256+
// Vary the actual formula variable (the server), which may be a different
257+
// object than `obs` if `obs` is just a same-named stand-in.
258+
auto *serverObs = dynamic_cast<RooAbsRealLValue *>(_actualVars.at(idx));
259+
RooAbsRealLValue &flatObs = serverObs ? *serverObs : obs;
260+
std::span<const double> boundaries{binning.array(), static_cast<std::size_t>(binning.numBoundaries())};
261+
if (!RooHelpers::isFunctionFlatInBins(*this, flatObs, boundaries)) {
262+
coutE(InputArguments) << "RooFormulaVar::setBinBoundaries(" << GetName() << ") the expression \"" << _formExpr
263+
<< "\" is not flat within the given bins of " << obs.GetName()
264+
<< ". The binning is not set. Pass checkFlatness=false to override this check."
265+
<< std::endl;
266+
return;
267+
}
268+
}
269+
270+
// Key the binning by the observable's index in _actualVars (not its name), so
271+
// that it survives a renaming of the variable or a server redirection.
272+
_binnings[idx] = std::unique_ptr<RooAbsBinning>{binning.clone()};
273+
}
274+
275+
////////////////////////////////////////////////////////////////////////////////
276+
/// Return true if a binning was set with setBinBoundaries() for every
277+
/// observable in the integration set `obs`.
231278

279+
bool RooFormulaVar::isBinnedDistribution(const RooArgSet &obs) const
280+
{
281+
if (obs.empty() || _binnings.empty()) {
282+
return false;
283+
}
284+
for (RooAbsArg *o : obs) {
285+
if (_binnings.find(_actualVars.index(o->GetName())) == _binnings.end()) {
286+
return false;
287+
}
288+
}
289+
return true;
290+
}
232291

233292
////////////////////////////////////////////////////////////////////////////////
234-
/// Forward the plot sampling hint from the p.d.f. that defines the observable obs
293+
/// Return the boundaries of the binning set with setBinBoundaries() that fall
294+
/// within [xlo, xhi]. If no binning was set for this observable, forward the bin
295+
/// boundaries from the server that defines the observable obs.
235296

236297
std::list<double>* RooFormulaVar::binBoundaries(RooAbsRealLValue& obs, double xlo, double xhi) const
237298
{
299+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
300+
if (found != _binnings.end()) {
301+
const RooAbsBinning &binning = *found->second;
302+
return RooHelpers::binBoundariesInRange({binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo,
303+
xhi);
304+
}
305+
238306
for (const auto par : _actualVars) {
239307
auto func = static_cast<const RooAbsReal*>(par);
240308
list<double>* binb = nullptr;
@@ -247,13 +315,20 @@ std::list<double>* RooFormulaVar::binBoundaries(RooAbsRealLValue& obs, double xl
247315
return nullptr;
248316
}
249317

250-
251-
252318
////////////////////////////////////////////////////////////////////////////////
253-
/// Forward the plot sampling hint from the p.d.f. that defines the observable obs
319+
/// Return sampling hints that draw the piecewise-flat shape exactly if a binning
320+
/// was set for this observable. Otherwise, forward the plot sampling hint from
321+
/// the server that defines the observable obs.
254322

255323
std::list<double>* RooFormulaVar::plotSamplingHint(RooAbsRealLValue& obs, double xlo, double xhi) const
256324
{
325+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
326+
if (found != _binnings.end()) {
327+
const RooAbsBinning &binning = *found->second;
328+
return RooCurve::plotSamplingHintForBinBoundaries(
329+
{binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo, xhi);
330+
}
331+
257332
for (const auto par : _actualVars) {
258333
auto func = dynamic_cast<const RooAbsReal*>(par);
259334
list<double>* hint = nullptr;

roofit/roofitcore/src/RooGenericPdf.cxx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ the names of the arguments are not hard coded.
4848
#include "RooMsgService.h"
4949
#include "RooArgList.h"
5050
#include "RooFormula.h"
51+
#include "RooAbsRealLValue.h"
52+
#include "RooAbsBinning.h"
53+
#include "RooCurve.h"
54+
#include "RooHelpers.h"
5155

5256
using std::istream, std::ostream, std::endl;
5357

@@ -107,6 +111,9 @@ RooGenericPdf::RooGenericPdf(const RooGenericPdf& other, const char* name) :
107111
_actualVars("actualVars",this,other._actualVars),
108112
_formExpr(other._formExpr)
109113
{
114+
for (auto const &item : other._binnings) {
115+
_binnings[item.first] = std::unique_ptr<RooAbsBinning>{item.second->clone()};
116+
}
110117
formula();
111118
}
112119

@@ -122,7 +129,99 @@ RooFormula& RooGenericPdf::formula() const
122129
return *_formula ;
123130
}
124131

132+
////////////////////////////////////////////////////////////////////////////////
133+
/// Declare that this pdf is piecewise constant (flat) within the bins of the
134+
/// given `binning` of the observable `obs`, which must be one of the formula
135+
/// variables. The method can be called several times to set a binning for more
136+
/// than one observable. Use a RooUniformBinning to describe many uniform bins
137+
/// compactly.
138+
///
139+
/// Once set, integrals over `obs` use the fast bin integrator (which sums the
140+
/// central value of each bin times the bin width) instead of the generic
141+
/// numeric integrator, and plotting samples the step shape exactly.
142+
///
143+
/// If `checkFlatness` is true (the default), the function is sampled at several
144+
/// points inside each bin to verify that it is indeed flat; if it is not, an
145+
/// error is issued and the binning is not stored.
146+
147+
void RooGenericPdf::setBinBoundaries(RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness)
148+
{
149+
// Match the observable to a formula variable by name, so that a same-named
150+
// stand-in for the actual server is accepted too.
151+
const int idx = _actualVars.index(obs.GetName());
152+
if (idx < 0) {
153+
coutE(InputArguments) << "RooGenericPdf::setBinBoundaries(" << GetName() << ") the observable " << obs.GetName()
154+
<< " is not one of the formula variables of this pdf, nothing done." << std::endl;
155+
return;
156+
}
157+
158+
if (checkFlatness) {
159+
// Vary the actual formula variable (the server), which may be a different
160+
// object than `obs` if `obs` is just a same-named stand-in.
161+
auto *serverObs = dynamic_cast<RooAbsRealLValue *>(_actualVars.at(idx));
162+
RooAbsRealLValue &flatObs = serverObs ? *serverObs : obs;
163+
std::span<const double> boundaries{binning.array(), static_cast<std::size_t>(binning.numBoundaries())};
164+
if (!RooHelpers::isFunctionFlatInBins(*this, flatObs, boundaries)) {
165+
coutE(InputArguments) << "RooGenericPdf::setBinBoundaries(" << GetName() << ") the expression \"" << _formExpr
166+
<< "\" is not flat within the given bins of " << obs.GetName()
167+
<< ". The binning is not set. Pass checkFlatness=false to override this check."
168+
<< std::endl;
169+
return;
170+
}
171+
}
172+
173+
// Key the binning by the observable's index in _actualVars (not its name), so
174+
// that it survives a renaming of the variable or a server redirection.
175+
_binnings[idx] = std::unique_ptr<RooAbsBinning>{binning.clone()};
176+
}
177+
178+
////////////////////////////////////////////////////////////////////////////////
179+
/// Return true if a binning was set with setBinBoundaries() for every
180+
/// observable in the integration set `obs`.
125181

182+
bool RooGenericPdf::isBinnedDistribution(const RooArgSet &obs) const
183+
{
184+
if (obs.empty() || _binnings.empty()) {
185+
return false;
186+
}
187+
for (RooAbsArg *o : obs) {
188+
if (_binnings.find(_actualVars.index(o->GetName())) == _binnings.end()) {
189+
return false;
190+
}
191+
}
192+
return true;
193+
}
194+
195+
////////////////////////////////////////////////////////////////////////////////
196+
/// Return the boundaries of the binning set with setBinBoundaries() that fall
197+
/// within [xlo, xhi], or a null pointer if no binning was set for this observable.
198+
199+
std::list<double> *RooGenericPdf::binBoundaries(RooAbsRealLValue &obs, double xlo, double xhi) const
200+
{
201+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
202+
if (found == _binnings.end()) {
203+
return nullptr;
204+
}
205+
const RooAbsBinning &binning = *found->second;
206+
return RooHelpers::binBoundariesInRange({binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo,
207+
xhi);
208+
}
209+
210+
////////////////////////////////////////////////////////////////////////////////
211+
/// Return sampling hints that draw the piecewise-flat shape exactly (a pair of
212+
/// points just left and right of every bin boundary), or a null pointer if no
213+
/// binning was set for this observable.
214+
215+
std::list<double> *RooGenericPdf::plotSamplingHint(RooAbsRealLValue &obs, double xlo, double xhi) const
216+
{
217+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
218+
if (found == _binnings.end()) {
219+
return nullptr;
220+
}
221+
const RooAbsBinning &binning = *found->second;
222+
return RooCurve::plotSamplingHintForBinBoundaries(
223+
{binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo, xhi);
224+
}
126225

127226
////////////////////////////////////////////////////////////////////////////////
128227
/// Calculate current value of this object

0 commit comments

Comments
 (0)