Skip to content

Commit 0a25f80

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::setBinning() and RooFormulaVar::setBinning(), 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. (cherry picked from commit 9ea4ec6)
1 parent 5984e56 commit 0a25f80

7 files changed

Lines changed: 584 additions & 9 deletions

File tree

roofit/roofitcore/inc/RooFormulaVar.h

Lines changed: 14 additions & 3 deletions
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,8 +72,12 @@ class RooFormulaVar : public RooAbsReal {
6872

6973
double defaultErrorLevel() const override ;
7074

71-
std::list<double>* binBoundaries(RooAbsRealLValue& /*obs*/, double /*xlo*/, double /*xhi*/) const override ;
72-
std::list<double>* plotSamplingHint(RooAbsRealLValue& /*obs*/, double /*xlo*/, double /*xhi*/) const override ;
75+
void setBinning(const RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness = true);
76+
bool removeBinning(const RooAbsRealLValue &obs);
77+
78+
bool isBinnedDistribution(const RooArgSet &obs) const override;
79+
std::list<double>* binBoundaries(RooAbsRealLValue& obs, double xlo, double xhi) const override ;
80+
std::list<double>* plotSamplingHint(RooAbsRealLValue& obs, double xlo, double xhi) const override ;
7381

7482
// Function evaluation
7583
double evaluate() const override ;
@@ -94,7 +102,10 @@ class RooFormulaVar : public RooAbsReal {
94102
mutable RooArgSet* _nset{nullptr}; ///<! Normalization set to be passed along to contents
95103
TString _formExpr ; ///< Formula expression string
96104

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

100111
#endif

roofit/roofitcore/inc/RooGenericPdf.h

Lines changed: 17 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,14 @@ class RooGenericPdf : public RooAbsPdf {
6167

6268
std::string getUniqueFuncName() const;
6369

64-
protected:
70+
void setBinning(const RooAbsRealLValue &obs, const RooAbsBinning &binning, bool checkFlatness = true);
71+
bool removeBinning(const RooAbsRealLValue &obs);
72+
73+
bool isBinnedDistribution(const RooArgSet &obs) const override;
74+
std::list<double> *binBoundaries(RooAbsRealLValue &obs, double xlo, double xhi) const override;
75+
std::list<double> *plotSamplingHint(RooAbsRealLValue &obs, double xlo, double xhi) const override;
6576

77+
protected:
6678
RooFormula& formula() const ;
6779

6880
// Function evaluation
@@ -78,7 +90,10 @@ class RooGenericPdf : public RooAbsPdf {
7890
mutable RooFormula * _formula = nullptr; ///<! Formula engine
7991
TString _formExpr ; ///< Formula expression string
8092

81-
ClassDefOverride(RooGenericPdf,1) // Generic PDF defined by string expression and list of variables
93+
std::map<int, std::unique_ptr<RooAbsBinning>> _binnings; ///< User-defined binnings, keyed by the observable's index
94+
///< in _actualVars, for a piecewise-flat distribution
95+
96+
ClassDefOverride(RooGenericPdf, 2) // Generic PDF defined by string expression and list of variables
8297
};
8398

8499
#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: 99 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,94 @@ 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::setBinning() for details.
231243

244+
void RooFormulaVar::setBinning(const 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::setBinning(" << 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+
// Sample the function by varying the actual formula variable (the server),
257+
// which may be a different object than `obs` if `obs` is just a same-named
258+
// stand-in: the function's value depends on the server, not on `obs`.
259+
if (auto *serverObs = dynamic_cast<RooAbsRealLValue *>(_actualVars.at(idx))) {
260+
std::span<const double> boundaries{binning.array(), static_cast<std::size_t>(binning.numBoundaries())};
261+
if (!RooHelpers::isFunctionFlatInBins(*this, *serverObs, boundaries)) {
262+
coutE(InputArguments) << "RooFormulaVar::setBinning(" << 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+
271+
// Key the binning by the observable's index in _actualVars (not its name), so
272+
// that it survives a renaming of the variable or a server redirection.
273+
_binnings[idx] = std::unique_ptr<RooAbsBinning>{binning.clone()};
274+
}
232275

233276
////////////////////////////////////////////////////////////////////////////////
234-
/// Forward the plot sampling hint from the p.d.f. that defines the observable obs
277+
/// Remove a binning previously declared with setBinning() for observable `obs`,
278+
/// reverting to the generic numeric integrator for it. Returns true if a binning
279+
/// was removed, false if none was set for `obs`.
280+
281+
bool RooFormulaVar::removeBinning(const RooAbsRealLValue &obs)
282+
{
283+
return _binnings.erase(_actualVars.index(obs.GetName())) > 0;
284+
}
285+
286+
////////////////////////////////////////////////////////////////////////////////
287+
/// Return true if a binning was set with setBinning() for every
288+
/// observable in the integration set `obs`.
289+
290+
bool RooFormulaVar::isBinnedDistribution(const RooArgSet &obs) const
291+
{
292+
if (obs.empty() || _binnings.empty()) {
293+
return false;
294+
}
295+
for (RooAbsArg *o : obs) {
296+
const int idx = _actualVars.index(o->GetName());
297+
// Observables that are not formula variables of this function are ones we
298+
// do not depend on: the function is constant (hence trivially binned) in
299+
// them, so they must be ignored here. This matches the convention that
300+
// composite functions like RooProduct rely on, where each component's
301+
// isBinnedDistribution() is queried with the full observable set.
302+
if (idx < 0) {
303+
continue;
304+
}
305+
if (_binnings.find(idx) == _binnings.end()) {
306+
return false;
307+
}
308+
}
309+
return true;
310+
}
311+
312+
////////////////////////////////////////////////////////////////////////////////
313+
/// Return the boundaries of the binning set with setBinning() that fall
314+
/// within [xlo, xhi]. If no binning was set for this observable, forward the bin
315+
/// boundaries from the server that defines the observable obs.
235316

236317
std::list<double>* RooFormulaVar::binBoundaries(RooAbsRealLValue& obs, double xlo, double xhi) const
237318
{
319+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
320+
if (found != _binnings.end()) {
321+
const RooAbsBinning &binning = *found->second;
322+
return RooHelpers::binBoundariesInRange({binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo,
323+
xhi);
324+
}
325+
238326
for (const auto par : _actualVars) {
239327
auto func = static_cast<const RooAbsReal*>(par);
240328
list<double>* binb = nullptr;
@@ -247,13 +335,20 @@ std::list<double>* RooFormulaVar::binBoundaries(RooAbsRealLValue& obs, double xl
247335
return nullptr;
248336
}
249337

250-
251-
252338
////////////////////////////////////////////////////////////////////////////////
253-
/// Forward the plot sampling hint from the p.d.f. that defines the observable obs
339+
/// Return sampling hints that draw the piecewise-flat shape exactly if a binning
340+
/// was set for this observable. Otherwise, forward the plot sampling hint from
341+
/// the server that defines the observable obs.
254342

255343
std::list<double>* RooFormulaVar::plotSamplingHint(RooAbsRealLValue& obs, double xlo, double xhi) const
256344
{
345+
auto found = _binnings.find(_actualVars.index(obs.GetName()));
346+
if (found != _binnings.end()) {
347+
const RooAbsBinning &binning = *found->second;
348+
return RooCurve::plotSamplingHintForBinBoundaries(
349+
{binning.array(), static_cast<std::size_t>(binning.numBoundaries())}, xlo, xhi);
350+
}
351+
257352
for (const auto par : _actualVars) {
258353
auto func = dynamic_cast<const RooAbsReal*>(par);
259354
list<double>* hint = nullptr;

0 commit comments

Comments
 (0)