Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README/ReleaseNotes/v642/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ maps) will now obtain different, mathematically consistent values.

## Math

### Skipping identically-vanishing second derivatives in numerical Hessian evaluation

Minuit2 objective functions can now advertise pairs of parameters whose mixed second derivative is identically zero, via the new virtual function `ROOT::Minuit2::FCNBase::SecondDerivativeAlwaysVanishes()` (with corresponding setters on `ROOT::Minuit2::FCNAdapter` and `ROOT::Minuit2::Minuit2Minimizer`).
The numerical Hessian computation in Minuit2 (`MnHesse`) skips the finite-difference evaluations for such parameter pairs, which can speed up Hesse significantly for likelihoods with many mutually independent parameters.
The parameter indices in this interface always refer to the function's own full (external) parameter space, including parameters that are fixed in the minimizer, and the advertised information must hold for all parameter values; see the `FCNBase` documentation for the full contract.

## RDataFrame

* Added `RedefinePerSample` transformation. Works similarly to `DefinePerSample`, but allows to redefine existing values
Expand All @@ -145,6 +151,13 @@ the cut instead of being selected based on `sqrt(abs(x))`.

* The `RooMinimizer::Strategy` enum has been removed. It named the Minuit strategies that are usually referred to just by integers, but caused confusion because it didn't include the unnamed "Strategy 3". Since people usually set the strategy with integer values anyway, it was decided that the simplest solution to avoid the confusion was simply to remove the `RooMinimizer::Strategy` enum

### Faster Hesse for likelihoods with many independent parameters

RooFit now analyzes the computation graph of the minimized function to find pairs of parameters that never appear in the same additive term of the likelihood, meaning their mixed second derivative is identically zero.
This information is forwarded to Minuit2, which skips the corresponding finite-difference evaluations in the numerical Hessian computation (see the Math section above).
For likelihoods with many mutually independent parameters, such as the per-channel nuisance parameters of large combined HistFactory models, this can speed up `RooMinimizer::hesse()` by 30 % or more, with results identical up to floating-point noise.
This optimization is automatic and requires no user action.

### Deprecation of the legacy evaluation backend

The `legacy` evaluation backend for likelihood and chi-square fits is deprecated and will be removed in ROOT 6.44.
Expand Down
23 changes: 23 additions & 0 deletions math/minuit2/inc/Minuit2/FCNAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,27 @@ class FCNAdapter : public FCNBase {
/// @param up New error definition value.
void SetErrorDef(double up) override { fUp = up; }

/// Indicate whether the mixed second order derivative with respect to
/// parameters i and j is identically zero, forwarding to the user-provided
/// predicate if set.
bool SecondDerivativeAlwaysVanishes(unsigned int i, unsigned int j) const override
{
return fSecondDerivAlwaysVanishesFunc ? fSecondDerivAlwaysVanishesFunc(i, j) : false;
}

/// Set the predicate advertising which mixed second derivatives are
/// identically zero.
///
/// @param f Function taking two parameter indices (in the FCN's full
/// external parameter space) and returning `true` if the
/// corresponding second derivative is zero for all parameter
/// values. It must fulfill the contract documented for
/// FCNBase::SecondDerivativeAlwaysVanishes().
void SetSecondDerivativeAlwaysVanishesFunc(std::function<bool(unsigned int, unsigned int)> f)
{
fSecondDerivAlwaysVanishesFunc = std::move(f);
}

private:
using Function = std::function<double(double const *)>;
using GradFunction = std::function<void(double const *, double *)>;
Expand All @@ -173,6 +194,8 @@ class FCNAdapter : public FCNBase {
GradFunction fGradFunc; ///< Optional gradient function.
G2Function fG2Func; ///< Optional diagonal second-derivative function.
mutable HessianFunction fHessianFunc; ///< Optional Hessian function.
/// Optional predicate for identically-vanishing second derivatives.
std::function<bool(unsigned int, unsigned int)> fSecondDerivAlwaysVanishesFunc;
};

} // namespace ROOT::Minuit2
Expand Down
20 changes: 20 additions & 0 deletions math/minuit2/inc/Minuit2/FCNBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ class FCNBase {
virtual bool HasHessian() const { return false; }

virtual bool HasG2() const { return false; }

/// Indicate whether the mixed second order derivative with respect to
/// parameters i and j is identically zero, i.e. zero for *all* parameter
/// values. This can help to avoid expensive function calls in numerical
/// Hessian evaluations (see MnHesse).
///
/// The contract for implementations:
///
/// * The indices refer to the FCN's own full (external) parameter
/// space, including any parameters that are fixed in the minimizer.
/// Callers that work with internal indices, like MnHesse, must
/// translate them via MnUserTransformation::ExtOfInt() before calling
/// this function.
/// * The result must be symmetric in i and j.
/// * Only return `true` if the second derivative vanishes for *any*
/// value of the parameters: the result may be cached and is used at
/// arbitrary points in parameter space.
/// * The diagonal (i == j) is never queried by Minuit2, so its return
/// value has no effect.
virtual bool SecondDerivativeAlwaysVanishes(unsigned int /*i*/, unsigned int /*j*/) const { return false; }
};

} // namespace ROOT::Minuit2
Expand Down
9 changes: 9 additions & 0 deletions math/minuit2/inc/Minuit2/Minuit2Minimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ class Minuit2Minimizer : public ROOT::Math::Minimizer {
/// set the function implementing Hessian computation
void SetHessianFunction(std::function<bool(std::span<const double>, double *)> hfunc) override;

/// Set a predicate advertising which mixed second derivatives of the
/// minimized function are identically zero, so that MnHesse can skip the
/// corresponding finite-difference evaluations. The predicate must
/// fulfill the contract documented for
/// FCNBase::SecondDerivativeAlwaysVanishes(); in particular, the indices
/// refer to the full (external) parameter space. Like SetHessianFunction,
/// this must be called after SetFunction.
void SetSecondDerivativeAlwaysVanishesFunc(std::function<bool(unsigned int, unsigned int)> func);

/// set free variable
bool SetVariable(unsigned int ivar, const std::string &name, double val, double step) override;

Expand Down
9 changes: 9 additions & 0 deletions math/minuit2/src/Minuit2Minimizer.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,15 @@ void Minuit2Minimizer::SetHessianFunction(std::function<bool(std::span<const dou
fcn->SetHessianFunction(hfunc);
}

void Minuit2Minimizer::SetSecondDerivativeAlwaysVanishesFunc(std::function<bool(unsigned int, unsigned int)> func)
{
// not supported for Fumili, whose FCN is not an FCNAdapter
if (fUseFumili) return;
auto fcn = static_cast<ROOT::Minuit2::FCNAdapter *>(fMinuitFCN.get());
if (!fcn) return;
fcn->SetSecondDerivativeAlwaysVanishesFunc(std::move(func));
}

namespace {

ROOT::Minuit2::MnStrategy customizedStrategy(unsigned int strategyLevel, ROOT::Math::MinimizerOptions const &options)
Expand Down
14 changes: 10 additions & 4 deletions math/minuit2/src/MnHesse.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,20 @@ MinimumState ComputeNumerical(const MnFcn &mfcn, const MinimumState &st, const M
if ((i + 1) == j || in == startParIndexOffDiagonal)
x(i) += dirin(i);

x(j) += dirin(j);

double fs1 = mfcnCaller(x);
if(!doCentralFD) {
// The FCN advertises vanishing second derivatives in its own
// (external) parameter space, but i and j are internal indices that
// exclude fixed parameters, so translate before asking.
if(mfcn.Fcn().SecondDerivativeAlwaysVanishes(trafo.ExtOfInt(i), trafo.ExtOfInt(j))) {
vhmat(i, j) = 0.;
} else if(!doCentralFD) {
x(j) += dirin(j);
double fs1 = mfcnCaller(x);
double elem = (fs1 + amin - yy(i) - yy(j)) / (dirin(i) * dirin(j));
vhmat(i, j) = elem;
x(j) -= dirin(j);
} else {
x(j) += dirin(j);
double fs1 = mfcnCaller(x);
// three more function evaluations required for central fd
x(i) -= dirin(i);
x(i) -= dirin(i);
Expand Down
134 changes: 134 additions & 0 deletions math/minuit2/test/testMinuit2.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Author: Jonas Rembser, CERN 01/2026

#include <Minuit2/FCNBase.h>
#include <Minuit2/MnHesse.h>
#include <Minuit2/MnMigrad.h>
#include <Minuit2/MnUserParameters.h>
#include <Minuit2/MnUserParameterState.h>
Expand Down Expand Up @@ -411,3 +412,136 @@ TEST(Minuit2, AnalyticalHessianLimitTransformation)
}
}
}

// ----------------------------------------------------------------------
// Tests for FCNBase::SecondDerivativeAlwaysVanishes()
// ----------------------------------------------------------------------

// Sum of two additive terms: the first couples parameters (0, 2) and the
// second couples (1, 2), so only the mixed second derivative with respect to
// the pair (0, 1) is identically zero. The full Hessian is:
//
// [ 4 0 -2]
// [ 0 4 -2]
// [-2 -2 6]
class SparseQuadraticFCN : public ROOT::Minuit2::FCNBase {
public:
double operator()(const std::vector<double> &p) const override
{
++fNCalls;
const double a = p[0];
const double b = p[1];
const double c = p[2];
// clang-format off
return (a-1)*(a-1) + (a-c)*(a-c) + (c-1)*(c-1) + (b-c)*(b-c) + (b-1)*(b-1);
// clang-format on
}

double Up() const override { return 1.0; }

mutable int fNCalls = 0;
};

// Same function, but advertising the identically-vanishing mixed second
// derivative with respect to the (external) parameter pair (0, 1).
class SparseQuadraticFCNWithInfo : public SparseQuadraticFCN {
public:
bool SecondDerivativeAlwaysVanishes(unsigned int i, unsigned int j) const override
{
return (i == 0 && j == 1) || (i == 1 && j == 0);
}
};

namespace {

/// Minimize, run MnHesse explicitly, and report the number of FCN calls that
/// the MnHesse run needed in the output parameter.
template <typename FCN>
ROOT::Minuit2::FunctionMinimum RunMigradAndHesse(const FCN &fcn, int &nHesseCalls)
{
using namespace ROOT::Minuit2;

MnUserParameters upar;
upar.Add("a", 0.5, 0.1);
upar.Add("b", 0.5, 0.1);
upar.Add("c", 0.5, 0.1);

MnMigrad migrad(fcn, upar);
FunctionMinimum min = migrad();

fcn.fNCalls = 0;
MnHesse hesse;
hesse(fcn, min);
nHesseCalls = fcn.fNCalls;
return min;
}

} // namespace

// The Hessian from MnHesse must be correct whether or not the FCN advertises
// its identically-vanishing second derivative, and advertising it must save
// function calls.
TEST(Minuit2, SecondDerivativeAlwaysVanishes)
{
using ROOT::Minuit2::FunctionMinimum;

const double tol = 1e-5;

SparseQuadraticFCN fcnDense;
int nCallsDense = 0;
const FunctionMinimum minDense = RunMigradAndHesse(fcnDense, nCallsDense);

SparseQuadraticFCNWithInfo fcnSparse;
int nCallsSparse = 0;
const FunctionMinimum minSparse = RunMigradAndHesse(fcnSparse, nCallsSparse);

for (FunctionMinimum const *min : {&minDense, &minSparse}) {
const auto &hessian = min->Error().Hessian();
ASSERT_EQ(hessian.Nrow(), 3);
EXPECT_NEAR(hessian(0, 0), 4.0, tol);
EXPECT_NEAR(hessian(1, 1), 4.0, tol);
EXPECT_NEAR(hessian(2, 2), 6.0, tol);
EXPECT_NEAR(hessian(0, 1), 0.0, tol);
EXPECT_NEAR(hessian(0, 2), -2.0, tol);
EXPECT_NEAR(hessian(1, 2), -2.0, tol);
}

// The advertised element must be skipped, not computed: zero up to the
// round-off of the covariance inversion round-trip (much smaller than
// finite-difference noise), and with fewer function calls.
EXPECT_NEAR(minSparse.Error().Hessian()(0, 1), 0.0, 1e-12);
EXPECT_LT(nCallsSparse, nCallsDense);
}

// With parameter "a" fixed, Minuit's internal parameter indices no longer
// coincide with the external ones that the FCN advertises the vanishing
// second derivative in. If MnHesse did not translate the indices via
// MnUserTransformation::ExtOfInt(), the internal pair (0, 1) = (b, c) would
// be looked up as the advertised external pair (0, 1) = (a, b), wrongly
// zeroing the genuinely non-vanishing (b, c) Hessian element.
TEST(Minuit2, SecondDerivativeAlwaysVanishesFixedParameter)
{
using namespace ROOT::Minuit2;

const double tol = 1e-5;

SparseQuadraticFCNWithInfo fcn;

MnUserParameters upar;
upar.Add("a", 1.0, 0.1);
upar.Add("b", 0.5, 0.1);
upar.Add("c", 0.5, 0.1);
upar.Fix("a");

MnMigrad migrad(fcn, upar);
FunctionMinimum min = migrad();
MnHesse hesse;
hesse(fcn, min);

// Reduced Hessian in the internal space (b, c)
const auto &hessian = min.Error().Hessian();
ASSERT_EQ(hessian.Nrow(), 2);
EXPECT_NEAR(hessian(0, 0), 4.0, tol);
EXPECT_NEAR(hessian(0, 1), -2.0, tol);
EXPECT_NEAR(hessian(1, 1), 6.0, tol);
}
4 changes: 4 additions & 0 deletions roofit/roofitcore/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,10 @@ ROOT_STANDARD_LIBRARY_PACKAGE(RooFitCore
${EXTRA_DICT_OPTS}
)

# Minuit2 is needed in the implementation of RooMinimizerFcn, to advertise
# vanishing second derivatives to Minuit2Minimizer.
target_link_libraries(RooFitCore PRIVATE Minuit2)

# The following definitions are PUBLIC so they can also be used in ROOT-internal tests

if(roofit_legacy_eval_backend)
Expand Down
2 changes: 1 addition & 1 deletion roofit/roofitcore/inc/RooConstraintSum.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class RooConstraintSum : public RooAbsReal {
RooConstraintSum(const RooConstraintSum& other, const char* name = nullptr);
TObject* clone(const char* newname=nullptr) const override { return new RooConstraintSum(*this, newname); }

const RooArgList& list() { return _set1 ; }
const RooArgList& list() const { return _set1 ; }

bool setData(RooAbsData const& data, bool cloneData=true);
/// \copydoc setData(RooAbsData const&, bool)
Expand Down
3 changes: 3 additions & 0 deletions roofit/roofitcore/inc/RooEvaluatorWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ class RooEvaluatorWrapper final : public RooAbsReal {

RooFit::Evaluator &evaluator() const { return *_evaluator; }

/// The RooFit object that this wrapper evaluates.
RooAbsReal const &topNode() const { return *_topNode; }

protected:
double evaluate() const override;

Expand Down
2 changes: 1 addition & 1 deletion roofit/roofitcore/src/RooMinimizer.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ RooMinimizer::RooMinimizer(RooAbsReal &function, Config const &cfg) : _function{
_fcn = std::make_unique<RooMinimizerFcn>(&function, this);
}
initMinimizerFcnDependentPart(function.defaultErrorLevel());
};
}

/// Initialize the part of the minimizer that is independent of the function to be minimized
void RooMinimizer::initMinimizerFirstPart()
Expand Down
Loading
Loading