Skip to content

Commit fcd0d45

Browse files
committed
[RF] Avoid using out-of-range values in RooDerivative
Avoid using out-of-range values for the numerical differentiation in RooDerivative. This adds three new special cases: * derivative is first order and x-value is close to limit: just use one-directional derivative already implemented in the RichardsonDerivator * higher-order derivative but function is constant within floating point precision: just return zero * higher-order derivative for non-constant function: throw error, because we don't want to risk wrong derivatives by evaluating outside the range, which will result in x-value-clipping and therefore invalid y value differences Closes #18615.
1 parent 2eff1d6 commit fcd0d45

File tree

3 files changed

+64
-38
lines changed

3 files changed

+64
-38
lines changed

roofit/roofitcore/src/RooDerivative.cxx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,60 @@ RooDerivative::~RooDerivative() = default;
9090

9191
double RooDerivative::evaluate() const
9292
{
93-
if (!_ftor) {
94-
_ftor = std::unique_ptr<RooFunctor>{_func.arg().functor(_x.arg(),RooArgSet(),_nset)};
95-
ROOT::Math::WrappedFunction<RooFunctor&> wf(*_ftor);
96-
_rd = std::make_unique<ROOT::Math::RichardsonDerivator>(wf,_eps,true);
97-
}
98-
99-
switch (_order) {
100-
case 1: return _rd->Derivative1(_x);
101-
case 2: return _rd->Derivative2(_x);
102-
case 3: return _rd->Derivative3(_x);
103-
}
104-
return 0 ;
93+
if (!_ftor) {
94+
_ftor = std::unique_ptr<RooFunctor>{_func.arg().functor(_x.arg(), RooArgSet(), _nset)};
95+
ROOT::Math::WrappedFunction<RooFunctor &> wf(*_ftor);
96+
_rd = std::make_unique<ROOT::Math::RichardsonDerivator>(wf, _eps, true);
97+
}
98+
99+
// Figure out if we are close to the variable boundaries
100+
double val = _x;
101+
auto &xVar = static_cast<RooRealVar &>(*_x);
102+
double valMin = xVar.getMin();
103+
double valMax = xVar.getMax();
104+
bool isCloseLo = val - valMin < _eps;
105+
bool isCloseHi = valMax - val < _eps;
106+
107+
// If we hit the boundary left and right, there is obviously a mistake when setting epsilon
108+
if (isCloseLo && isCloseHi) {
109+
std::stringstream errMsg;
110+
errMsg << "error in numerical derivator: 2 * epsilon is larger than the variable range!";
111+
coutE(Eval) << errMsg.str() << std::endl;
112+
throw std::runtime_error(errMsg.str());
113+
}
114+
115+
// If we are close to the variable boundary on either side
116+
if (isCloseLo || isCloseHi) {
117+
// For first-order derivatives we are good: the RichardsonDerivator can
118+
// also calculate the derivative using only forward or backward
119+
// variations:
120+
if (_order == 1) {
121+
return isCloseLo ? _rd->DerivativeForward(val) : _rd->DerivativeBackward(val);
122+
}
123+
// If the function is constant within floating point precision anyway,
124+
// we don't have a problem.
125+
const double eps = std::numeric_limits<double>::epsilon();
126+
const double yval1 = _ftor->eval(val);
127+
const double yval2 = isCloseLo ? _ftor->eval(val + _eps) : _ftor->eval(val - _eps);
128+
if (std::abs(yval2 - yval1) <= eps) {
129+
return 0.0;
130+
}
131+
132+
// Give up
133+
std::stringstream errMsg;
134+
errMsg << "error in numerical derivator: variable value is to close to limits to compute finite differences";
135+
coutE(Eval) << errMsg.str() << std::endl;
136+
throw std::runtime_error(errMsg.str());
137+
}
138+
139+
switch (_order) {
140+
case 1: return _rd->Derivative1(val);
141+
case 2: return _rd->Derivative2(val);
142+
case 3: return _rd->Derivative3(val);
143+
}
144+
return 0;
105145
}
106146

107-
108-
109147
////////////////////////////////////////////////////////////////////////////////
110148
/// Zap functor and derivator ;
111149

tutorials/roofit/roofit/rf111_derivatives.C

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,6 @@ void rf111_derivatives()
3535
RooRealVar mean("mean", "mean of gaussian", 1, -10, 10);
3636
RooRealVar sigma("sigma", "width of gaussian", 1, 0.1, 10);
3737

38-
// Ranges for plotting derivatives. They can't be the full range, because
39-
// the derivatives is calculated by variation around the central point. We
40-
// need to ensure that the varied values are still in the full range.
41-
x.setRange("plotrange", -9.95, 9.95);
42-
sigma.setRange("plotrange", 0.15, 1.95);
43-
4438
// Build gaussian pdf in terms of x,mean and sigma
4539
RooGaussian gauss("gauss", "gaussian PDF", x, mean, sigma);
4640

@@ -61,9 +55,9 @@ void rf111_derivatives()
6155
gauss.plotOn(xframe);
6256

6357
// Plot derivatives in same frame
64-
dgdx->plotOn(xframe, LineColor(kMagenta), Range("plotrange"));
65-
d2gdx2->plotOn(xframe, LineColor(kRed), Range("plotrange"));
66-
d3gdx3->plotOn(xframe, LineColor(kOrange), Range("plotrange"));
58+
dgdx->plotOn(xframe, LineColor(kMagenta));
59+
d2gdx2->plotOn(xframe, LineColor(kRed));
60+
d3gdx3->plotOn(xframe, LineColor(kOrange));
6761

6862
// C r e a t e a n d p l o t d e r i v a t i v e s w . r . t . s i g m a
6963
// ------------------------------------------------------------------------------
@@ -82,9 +76,9 @@ void rf111_derivatives()
8276
gauss.plotOn(sframe);
8377

8478
// Plot derivatives in same frame
85-
dgds->plotOn(sframe, LineColor(kMagenta), Range("plotrange"));
86-
d2gds2->plotOn(sframe, LineColor(kRed), Range("plotrange"));
87-
d3gds3->plotOn(sframe, LineColor(kOrange), Range("plotrange"));
79+
dgds->plotOn(sframe, LineColor(kMagenta));
80+
d2gds2->plotOn(sframe, LineColor(kRed));
81+
d3gds3->plotOn(sframe, LineColor(kOrange));
8882

8983
// Draw all frames on a canvas
9084
TCanvas *c = new TCanvas("rf111_derivatives", "rf111_derivatives", 800, 400);

tutorials/roofit/roofit/rf111_derivatives.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@
2525
mean = ROOT.RooRealVar("mean", "mean of gaussian", 1, -10, 10)
2626
sigma = ROOT.RooRealVar("sigma", "width of gaussian", 1, 0.1, 10)
2727

28-
# Ranges for plotting derivatives. They can't be the full range, because
29-
# the derivatives is calculated by variation around the central point. We
30-
# need to ensure that the varied values are still in the full range.
31-
x.setRange("plotrange", -9.95, 9.95)
32-
sigma.setRange("plotrange", 0.15, 1.95)
33-
3428
# Build gaussian pdf in terms of x, and sigma
3529
gauss = ROOT.RooGaussian("gauss", "gaussian PDF", x, mean, sigma)
3630

@@ -51,9 +45,9 @@
5145
gauss.plotOn(xframe)
5246

5347
# Plot derivatives in same frame
54-
dgdx.plotOn(xframe, LineColor="m", Range="plotrange")
55-
d2gdx2.plotOn(xframe, LineColor="r", Range="plotrange")
56-
d3gdx3.plotOn(xframe, LineColor="kOrange", Range="plotrange")
48+
dgdx.plotOn(xframe, LineColor="m")
49+
d2gdx2.plotOn(xframe, LineColor="r")
50+
d3gdx3.plotOn(xframe, LineColor="kOrange")
5751

5852
# Create and plot derivatives w.r.t. sigma
5953
# ------------------------------------------------------------------------------
@@ -72,9 +66,9 @@
7266
gauss.plotOn(sframe)
7367

7468
# Plot derivatives in same frame
75-
dgds.plotOn(sframe, LineColor="m", Range="plotrange")
76-
d2gds2.plotOn(sframe, LineColor="r", Range="plotrange")
77-
d3gds3.plotOn(sframe, LineColor="kOrange", Range="plotrange")
69+
dgds.plotOn(sframe, LineColor="m")
70+
d2gds2.plotOn(sframe, LineColor="r")
71+
d3gds3.plotOn(sframe, LineColor="kOrange")
7872

7973
# Draw all frames on a canvas
8074
c = ROOT.TCanvas("rf111_derivatives", "rf111_derivatives", 800, 400)

0 commit comments

Comments
 (0)