Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RooBinSamplingPdf.cxx
Go to the documentation of this file.
1// Authors: Stephan Hageboeck, CERN; Andrea Sciandra, SCIPP-UCSC/Atlas; Nov 2020
2
3/*****************************************************************************
4 * RooFit
5 * Authors: *
6 * WV, Wouter Verkerke, UC Santa Barbara, verkerke@slac.stanford.edu *
7 * DK, David Kirkby, UC Irvine, dkirkby@uci.edu *
8 * *
9 * Copyright (c) 2000-2020, Regents of the University of California *
10 * and Stanford University. All rights reserved. *
11 * *
12 * Redistribution and use in source and binary forms, *
13 * with or without modification, are permitted according to the terms *
14 * listed in LICENSE (http://roofit.sourceforge.net/license.txt) *
15 *****************************************************************************/
16
17/**
18 * \class RooBinSamplingPdf
19 * The RooBinSamplingPdf is supposed to be used as an adapter between a continuous PDF
20 * and a binned distribution.
21 * When RooFit is used to fit binned data, and the PDF is continuous, it takes the probability density
22 * at the bin centre as a proxy for the probability averaged (integrated) over the entire bin. This is
23 * correct only if the second derivative of the function vanishes, though. This is shown in the plots
24 * below.
25 *
26 * For PDFs that have larger curvatures, the RooBinSamplingPdf can be used. It integrates the PDF in each
27 * bin using an adaptive integrator. This usually requires 21 times more function evaluations, but significantly
28 * reduces biases due to better sampling of the PDF. The integrator can be accessed from the outside
29 * using integrator(). This can be used to change the integration rules, so less/more function evaluations are
30 * performed. The target precision of the integrator can be set in the constructor.
31 *
32 * If the wrapped PDF supports analytical integration over the sampled observable, the exact analytical integral
33 * is used for each bin instead of the numeric integrator. This is both faster and more accurate, and happens
34 * transparently without any user intervention.
35 *
36 *
37 * ### How to use it
38 * There are two ways to use this class:
39 * - Manually wrap a PDF:
40 * ```
41 * RooBinSamplingPdf binSampler("<name>", "title", <binned observable of PDF>, <original PDF> [, <precision for integrator>]);
42 * binSampler.fitTo(data);
43 * ```
44 * When a PDF is wrapped with a RooBinSamplingPDF, just use the bin sampling PDF instead of the original one for fits
45 * or plotting etc.
46 * \note The binning will be taken from the observable. Make sure that this binning is the same as the one of the dataset that should be fit.
47 * Use RooRealVar::setBinning() to adapt it.
48 * - Instruct test statistics to carry out this wrapping automatically:
49 * ```
50 * pdf.fitTo(data, IntegrateBins(<precision>));
51 * ```
52 * This method is especially useful when used with a simultaneous PDF, since each component will automatically be wrapped,
53 * depending on the value of `precision`:
54 * - `precision < 0.`: None of the PDFs are touched, bin sampling is off.
55 * - `precision = 0.`: Continuous PDFs that are fit to a RooDataHist are wrapped into a RooBinSamplingPdf. The target precision
56 * forwarded to the integrator is 1.E-4 (the default argument of the constructor).
57 * - `precision > 0.`: All continuous PDFs are automatically wrapped into a RooBinSamplingPdf, regardless of what data they are
58 * fit to (see next paragraph). The same `'precision'` is used for all integrators.
59 *
60 * ### Simulating a binned fit using RooDataSet
61 * Some frameworks use unbinned data (RooDataSet) to simulate binned datasets. By adding one entry for each bin centre with the
62 * appropriate weight, one can achieve the same result as fitting with RooDataHist. In this case, however, RooFit cannot
63 * auto-detect that a binned fit is running, and that an integration over the bin is desired (note that there are no bins to
64 * integrate over in this kind of dataset).
65 *
66 * In this case, `IntegrateBins(>0.)` needs to be used, and the desired binning needs to be assigned to the observable
67 * of the dataset:
68 * ```
69 * RooRealVar x("x", "x", 0., 5.);
70 * x.setBins(10);
71 *
72 * // <create dataset and model>
73 *
74 * model.fitTo(data, IntegrateBins(>0.));
75 * ```
76 *
77 * \see RooAbsPdf::fitTo()
78 * \see IntegrateBins()
79 *
80 * \note This feature is currently limited to one-dimensional PDFs.
81 *
82 *
83 * \htmlonly <style>div.image img[src="RooBinSamplingPdf_OFF.png"]{width:12cm;}</style> \endhtmlonly
84 * \htmlonly <style>div.image img[src="RooBinSamplingPdf_ON.png" ]{width:12cm;}</style> \endhtmlonly
85 * <table>
86 * <tr><th> Binned fit without %RooBinSamplingPdf <th> Binned fit with %RooBinSamplingPdf </td></tr>
87 * <tr><td> \image html RooBinSamplingPdf_OFF.png ""
88 * </td>
89 * <td> \image html RooBinSamplingPdf_ON.png ""
90 * </td></tr>
91 * </table>
92 *
93 */
94
95
96#include "RooBinSamplingPdf.h"
97
98#include "RooFitImplHelpers.h"
99#include "RooRealBinding.h"
100#include "RooRealVar.h"
101#include "RooGlobalFunc.h"
102#include "RooDataHist.h"
103
104#include "Math/Integrator.h"
105
106#include <algorithm>
107
108////////////////////////////////////////////////////////////////////////////////
109/// Construct a new RooBinSamplingPdf.
110/// \param[in] name A name to identify this object.
111/// \param[in] title Title (for e.g. plotting)
112/// \param[in] observable Observable to integrate over (the one that is binned).
113/// \param[in] inputPdf A PDF whose bins should be sampled with higher precision.
114/// \param[in] epsilon Relative precision for the integrator, which is used to sample the bins.
115/// Note that ROOT's default is to use an adaptive integrator, which in its first iteration usually reaches
116/// relative precision of 1.E-4 or better. Therefore, asking for lower precision rarely has an effect.
117RooBinSamplingPdf::RooBinSamplingPdf(const char *name, const char *title, RooAbsRealLValue& observable,
118 RooAbsPdf& inputPdf, double epsilon) :
119 RooAbsPdf(name, title),
120 _pdf("inputPdf", "Function to be converted into a PDF", this, inputPdf),
121 _observable("observable", "Observable to integrate over", this, observable, true, true),
122 _relEpsilon(epsilon) {
123 if (!_pdf->dependsOn(*_observable)) {
124 throw std::invalid_argument(std::string("RooBinSamplingPDF(") + GetName()
125 + "): The PDF " + _pdf->GetName() + " needs to depend on the observable "
126 + _observable->GetName());
127 }
128}
129
130
131 ////////////////////////////////////////////////////////////////////////////////
132 /// Copy a RooBinSamplingPdf.
133 /// \param[in] other PDF to copy.
134 /// \param[in] name Optionally rename the copy.
137 _pdf("inputPdf", this, other._pdf),
138 _observable("observable", this, other._observable),
139 _relEpsilon(other._relEpsilon) { }
140
141
142////////////////////////////////////////////////////////////////////////////////
143/// Integrate the PDF over the current bin of the observable.
145 const unsigned int bin = _observable->getBin();
146 const double low = _observable->getBinning().binLow(bin);
147 const double high = _observable->getBinning().binHigh(bin);
148
149 const double oldX = _observable->getVal();
150 double result;
151 {
152 // Important: When the integrator samples x, caching of sub-tree values needs to be off.
154 result = integrate(_normSet, low, high) / (high-low);
155 }
156
158
159 return result;
160}
161
162
163////////////////////////////////////////////////////////////////////////////////
164/// Integrate the PDF over all its bins, and return a batch with those values.
165/// \param[in,out] ctx Struct with evaluation data
167{
168 std::span<double> output = ctx.output();
169
170 // Retrieve binning, which we need to compute the probabilities
171 auto boundaries = binBoundaries();
172 auto xValues = ctx.at(_observable);
173
174 // Important: When the integrator samples x, caching of sub-tree values needs to be off.
176
177 // Now integrate PDF in each bin:
178 for (unsigned int i = 0; i < xValues.size(); ++i) {
179 const double x = xValues[i];
180 const auto upperIt = std::upper_bound(boundaries.begin(), boundaries.end(), x);
181 const unsigned int bin = std::distance(boundaries.begin(), upperIt) - 1;
182 assert(bin < boundaries.size());
183
184 output[i] = integrate(nullptr, boundaries[bin], boundaries[bin + 1]) / (boundaries[bin + 1] - boundaries[bin]);
185 }
186}
187
188
189////////////////////////////////////////////////////////////////////////////////
190/// Get the bin boundaries for the observable.
191/// These will be recomputed whenever the shape of this object is dirty.
192std::span<const double> RooBinSamplingPdf::binBoundaries() const {
193 if (isShapeDirty() || _binBoundaries.empty()) {
194 _binBoundaries.clear();
195 const RooAbsBinning& binning = _observable->getBinning(nullptr);
196 const double* boundaries = binning.array();
197
198 for (int i=0; i < binning.numBoundaries(); ++i) {
199 _binBoundaries.push_back(boundaries[i]);
200 }
201
202 assert(std::is_sorted(_binBoundaries.begin(), _binBoundaries.end()));
203
205 }
206
207 return {_binBoundaries};
208}
209
210
211////////////////////////////////////////////////////////////////////////////////
212/// Return a list of all bin boundaries, so the PDF is plotted correctly.
213/// \param[in] obs Observable to generate the boundaries for.
214/// \param[in] xlo Beginning of range to create list of boundaries for.
215/// \param[in] xhi End of range to create to create list of boundaries for.
216/// \return Pointer to a list to be deleted by caller.
217std::list<double>* RooBinSamplingPdf::binBoundaries(RooAbsRealLValue& obs, double xlo, double xhi) const {
218 if (obs.namePtr() != _observable->namePtr()) {
219 coutE(Plotting) << "RooBinSamplingPdf::binBoundaries(" << GetName() << "): observable '" << obs.GetName()
220 << "' is not the observable of this PDF ('" << _observable->GetName() << "')." << std::endl;
221 return nullptr;
222 }
223
224 auto list = new std::list<double>;
225 for (double val : binBoundaries()) {
226 if (xlo <= val && val < xhi)
227 list->push_back(val);
228 }
229
230 return list;
231}
232
233
234////////////////////////////////////////////////////////////////////////////////
235/// Return a list of all bin edges, so the PDF is plotted as a step function.
236/// \param[in] obs Observable to generate the sampling hint for.
237/// \param[in] xlo Beginning of range to create sampling hint for.
238/// \param[in] xhi End of range to create sampling hint for.
239/// \return Pointer to a list to be deleted by caller.
240std::list<double>* RooBinSamplingPdf::plotSamplingHint(RooAbsRealLValue& obs, double xlo, double xhi) const {
241 if (obs.namePtr() != _observable->namePtr()) {
242 coutE(Plotting) << "RooBinSamplingPdf::plotSamplingHint(" << GetName() << "): observable '" << obs.GetName()
243 << "' is not the observable of this PDF ('" << _observable->GetName() << "')." << std::endl;
244 return nullptr;
245 }
246
247 auto binEdges = new std::list<double>;
248 const auto& binning = obs.getBinning();
249
250 for (unsigned int bin=0, numBins = static_cast<unsigned int>(binning.numBins()); bin < numBins; ++bin) {
251 const double low = std::max(binning.binLow(bin), xlo);
252 const double high = std::min(binning.binHigh(bin), xhi);
253 const double width = high - low;
254
255 // Check if this bin is in plotting range at all
256 if (low >= high)
257 continue;
258
259 // Move support points slightly inside the bin, so step function is plotted correctly.
260 binEdges->push_back(low + 0.001 * width);
261 binEdges->push_back(high - 0.001 * width);
262 }
263
264 return binEdges;
265}
266
267
268////////////////////////////////////////////////////////////////////////////////
269/// Direct access to the unique_ptr holding the integrator that's used to sample the bins.
270/// This can be used to change options such as sampling accuracy or to entirely exchange the integrator.
271///
272/// #### Example: Use the 61-point Gauss-Kronrod integration rule
273/// ```{.cpp}
274/// ROOT::Math::IntegratorOneDimOptions intOptions = pdf.integrator()->Options();
275/// intOptions.SetNPoints(6); // 61-point integration rule
276/// intOptions.SetRelTolerance(1.E-9); // Smaller tolerance -> more subdivisions
277/// pdf.integrator()->SetOptions(intOptions);
278/// ```
279/// \note see ROOT::Math::IntegratorOneDim::SetOptions for more details on integration options.
280/// \note When RooBinSamplingPdf is loaded from files, integrator options will fall back to the default values.
281std::unique_ptr<ROOT::Math::IntegratorOneDim>& RooBinSamplingPdf::integrator() const {
282 if (!_integrator) {
283 _integrator = std::make_unique<ROOT::Math::IntegratorOneDim>(*this,
284 ROOT::Math::IntegrationOneDim::kADAPTIVE, // GSL Integrator. Will really get it only if MathMore enabled.
285 -1., _relEpsilon, // Abs epsilon = default, rel epsilon set by us.
286 0, // We don't limit the sub-intervals. Steer run time via _relEpsilon.
287 2 // This should read ROOT::Math::Integration::kGAUSS21, but this is in MathMore, so we cannot include it here.
288 );
289 }
290
291 return _integrator;
292}
293
294
295////////////////////////////////////////////////////////////////////////////////
296/// Binding used by the integrator to evaluate the PDF.
297double RooBinSamplingPdf::operator()(double x) const {
299 return _pdf;
300}
301
302
303////////////////////////////////////////////////////////////////////////////////
304/// Check once whether the wrapped PDF can integrate over the observable
305/// analytically. If so, the analytical integral code is cached so that
306/// integrate() can use the exact integral instead of the numeric integrator.
308 // Setting a named range to select the bin boundaries requires a RooRealVar.
309 // For other observable types we stick to the numeric integrator.
310 auto *observable = dynamic_cast<RooRealVar *>(&*_observable);
311 if (!observable) {
313 return;
314 }
315
316 _analyticalIntegralRangeName = std::string("_binSampling_") + GetName();
317
318 // Define the range so that PDFs that inspect it in getAnalyticalIntegral()
319 // find a valid one. The actual bin boundaries are filled in for each bin in
320 // integrate().
321 observable->setRange(_analyticalIntegralRangeName.c_str(), observable->getMin(), observable->getMax());
322
323 RooArgSet allVars{*_observable};
326
327 // Only use analytical integration if the observable is really integrated
328 // analytically. Otherwise, fall back to the numeric integrator.
329 if (_analyticalIntegralCode != 0 && !analVars.contains(*_observable)) {
331 }
332
333 if (_analyticalIntegralCode != 0) {
334 coutI(NumIntegration) << "RooBinSamplingPdf::integrate(" << GetName()
335 << "): using the analytical integral of " << _pdf->GetName()
336 << " to sample the bins instead of the numeric integrator." << std::endl;
337 }
338}
339
340
341////////////////////////////////////////////////////////////////////////////////
342/// Integrate the wrapped PDF over a single bin, with the given norm set and limits.
343/// If the wrapped PDF supports analytical integration over the observable, the
344/// exact integral is used. Otherwise, the numeric integrator is employed.
345///
346/// The result must match the numeric path, which integrates the value returned by
347/// operator(), i.e. the PDF value normalized over `normSet`. Therefore, the
348/// analytical integral is normalized over `normSet` as well via analyticalIntegralWN().
349double RooBinSamplingPdf::integrate(const RooArgSet* normSet, double low, double high) const {
350 if (_analyticalIntegralCode == -1) {
352 }
353
354 if (_analyticalIntegralCode != 0) {
355 // The analytical path is only enabled for RooRealVar observables (see
356 // initializeAnalyticalIntegral()), so this static_cast is safe.
357 static_cast<RooRealVar &>(*_observable).setRange(_analyticalIntegralRangeName.c_str(), low, high);
359 }
360
361 return integrator()->Integral(low, high);
362}
363
364
365/// Creates a wrapping RooBinSamplingPdf if appropriate.
366/// \param[in] pdf The input pdf.
367/// \param[in] data The dataset to be used in the fit, used to figure out the
368/// observables and whether the dataset is binned.
369/// \param[in] precision Precision argument for all created RooBinSamplingPdfs.
370std::unique_ptr<RooAbsPdf> RooBinSamplingPdf::create(RooAbsPdf& pdf, RooAbsData const &data, double precision) {
371 if (precision < 0.)
372 return nullptr;
373
374 std::unique_ptr<RooArgSet> funcObservables( pdf.getObservables(data) );
375 const bool oneDimAndBinned = (1 == std::count_if(funcObservables->begin(), funcObservables->end(), [](const RooAbsArg* arg) {
376 auto var = dynamic_cast<const RooRealVar*>(arg);
377 return var && var->numBins() > 1;
378 }));
379
380 if (!oneDimAndBinned) {
381 if (precision > 0.) {
382 oocoutE(&pdf, Fitting)
383 << "Integration over bins was requested, but this is currently only implemented for 1-D fits." << std::endl;
384 }
385 return nullptr;
386 }
387
388 // Find the real-valued observable. We don't care about categories.
389 auto theObs = std::find_if(funcObservables->begin(), funcObservables->end(), [](const RooAbsArg* arg){
390 return dynamic_cast<const RooAbsRealLValue*>(arg);
391 });
393
394 std::unique_ptr<RooAbsPdf> newPdf;
395
396 if (precision > 0.) {
397 // User forced integration. Let just apply it.
398 newPdf = std::make_unique<RooBinSamplingPdf>(
399 (std::string(pdf.GetName()) + "_binSampling").c_str(), pdf.GetTitle(),
400 *static_cast<RooAbsRealLValue *>(*theObs), pdf, precision);
401 } else if (dynamic_cast<RooDataHist const *>(&data) != nullptr &&
402 precision == 0. && !pdf.isBinnedDistribution(*data.get())) {
403 // User didn't forbid integration, and it seems appropriate with a
404 // RooDataHist.
405 newPdf = std::make_unique<RooBinSamplingPdf>(
406 (std::string(pdf.GetName()) + "_binSampling").c_str(), pdf.GetTitle(),
407 *static_cast<RooAbsRealLValue *>(*theObs), pdf);
408 }
409
410 return newPdf;
411}
#define coutI(a)
#define oocoutE(o, a)
#define coutE(a)
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void data
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t result
Option_t Option_t width
char name[80]
Definition TGX11.cxx:142
Disable all caches for sub-branches in an expression tree.
const_iterator begin() const
const_iterator end() const
Common abstract base class for objects that represent a value and a "shape" in RooFit.
Definition RooAbsArg.h:76
bool dependsOn(const RooAbsCollection &serverList, const RooAbsArg *ignoreArg=nullptr, bool valueOnly=false) const
Test whether we depend on (ie, are served by) any object in the specified collection.
const TNamed * namePtr() const
De-duplicated pointer to this object's name.
Definition RooAbsArg.h:482
bool isShapeDirty() const
Definition RooAbsArg.h:329
bool inhibitDirty() const
Definition RooAbsArg.cxx:99
void clearShapeDirty() const
Definition RooAbsArg.h:522
Abstract base class for RooRealVar binning definitions.
virtual Int_t numBoundaries() const =0
virtual double * array() const =0
Abstract base class for binned and unbinned datasets.
Definition RooAbsData.h:55
Abstract interface for all probability density functions.
Definition RooAbsPdf.h:32
RooArgSet const * _normSet
! Normalization set with for above integral
Definition RooAbsPdf.h:314
double analyticalIntegralWN(Int_t code, const RooArgSet *normSet, const char *rangeName=nullptr) const override
Analytical integral with normalization (see RooAbsReal::analyticalIntegralWN() for further informatio...
Abstract base class for objects that represent a real value that may appear on the left hand side of ...
Int_t getBin(const char *rangeName=nullptr) const override
virtual void setVal(double value)=0
Set the current value of the object. Needs to be overridden by implementations.
virtual const RooAbsBinning & getBinning(const char *name=nullptr, bool verbose=true, bool createOnTheFly=false, bool shared=true) const =0
Retrieve binning configuration with given name or default binning.
double getVal(const RooArgSet *normalisationSet=nullptr) const
Evaluate object.
Definition RooAbsReal.h:107
virtual Int_t getAnalyticalIntegralWN(RooArgSet &allVars, RooArgSet &analVars, const RooArgSet *normSet, const char *rangeName=nullptr) const
Variant of getAnalyticalIntegral that is also passed the normalization set that should be applied to ...
RooArgSet is a container object that can hold multiple RooAbsArg objects.
Definition RooArgSet.h:24
The RooBinSamplingPdf is supposed to be used as an adapter between a continuous PDF and a binned dist...
double integrate(const RooArgSet *normSet, double low, double high) const
Integrate the wrapped PDF over a single bin, with the given norm set and limits.
RooTemplateProxy< RooAbsPdf > _pdf
std::vector< double > _binBoundaries
! Workspace to store data for bin sampling
void doEval(RooFit::EvalContext &) const override
Integrate the PDF over all its bins, and return a batch with those values.
void initializeAnalyticalIntegral(const RooArgSet *normSet) const
Check once whether the wrapped PDF can integrate over the observable analytically.
std::unique_ptr< ROOT::Math::IntegratorOneDim > & integrator() const
Direct access to the unique_ptr holding the integrator that's used to sample the bins.
RooTemplateProxy< RooAbsRealLValue > _observable
std::string _analyticalIntegralRangeName
! Name of the range used for the per-bin analytical integration.
std::unique_ptr< ROOT::Math::IntegratorOneDim > _integrator
! Integrator used to sample bins.
double _relEpsilon
Default integrator precision.
Int_t _analyticalIntegralCode
! Analytical integral code of the wrapped pdf over the observable (-1: not yet determined,...
std::span< const double > binBoundaries() const
Get the bin boundaries for the observable.
static std::unique_ptr< RooAbsPdf > create(RooAbsPdf &pdf, RooAbsData const &data, double precision)
Creates a wrapping RooBinSamplingPdf if appropriate.
double operator()(double x) const
Binding used by the integrator to evaluate the PDF.
double evaluate() const override
Integrate the PDF over the current bin of the observable.
const RooAbsPdf & pdf() const
std::list< double > * plotSamplingHint(RooAbsRealLValue &obs, double xlo, double xhi) const override
Return a list of all bin edges, so the PDF is plotted as a step function.
const RooAbsReal & observable() const
Container class to hold N-dimensional binned data.
Definition RooDataHist.h:40
Variable that can be changed from the outside.
Definition RooRealVar.h:37
void setRange(const char *name, double min, double max, bool shared=true)
Set a fit or plotting range.
const char * GetName() const override
Returns name of object.
Definition TNamed.h:49
@ kADAPTIVE
to be used for general functions without singularities
Double_t x[n]
Definition legend1.C:17