Skip to content

Commit 0ca1dda

Browse files
authored
[RF] Address the RooFormula re-indexing issue during serialization
1 parent 2254ead commit 0ca1dda

6 files changed

Lines changed: 110 additions & 8 deletions

File tree

roofit/roofitcore/src/RooFormula.cxx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,54 @@ RooArgList RooFormula::usedVariables() const {
389389
return useList;
390390
}
391391

392+
////////////////////////////////////////////////////////////////////////////////
393+
/// Reindex the formula expression to map only the variables that are actually in use.
394+
/// Return the processed formula string with the `x[i]` positional indices
395+
/// remapped to each variable's position in usedVariables() (the pruned list of
396+
/// actually-used servers) instead of the full original list. This keeps the
397+
/// persisted pair (formula string, actualDependents()) self-consistent, so a
398+
/// RooFormulaVar or RooGenericPdf survives a write/read cycle even when unused
399+
/// parameters were pruned. See https://github.com/root-project/root/issues/21371
400+
/// \return A new formula string with reindexed variable placeholders.
401+
std::string RooFormula::reindexedFormulaForUsedVars() const
402+
{
403+
const std::string processedFormula(_tFormula->GetTitle());
404+
405+
int unUsedCount = 0;
406+
std::vector<int> newIndex;
407+
newIndex.reserve(_varIsUsed.size());
408+
// Map each original index to its position among the used variables;
409+
// pruned entries get -1 and are never looked up (they don't appear in the formula).
410+
for (std::size_t i = 0; i < _varIsUsed.size(); ++i) {
411+
if (!_varIsUsed[i]) {
412+
unUsedCount++;
413+
newIndex.push_back(-1);
414+
} else {
415+
newIndex.push_back(static_cast<int>(i) - unUsedCount);
416+
}
417+
}
418+
419+
static const std::regex newOrdinalRegex("\\bx\\[([0-9]+)\\]");
420+
421+
std::string result;
422+
std::size_t lastPos = 0;
423+
result.reserve(processedFormula.size());
424+
// Single pass: rewrite every x[old] to x[newIndex[old]].
425+
for (sregex_iterator matchIt = sregex_iterator(processedFormula.begin(), processedFormula.end(), newOrdinalRegex);
426+
matchIt != sregex_iterator(); ++matchIt) {
427+
std::smatch match = *matchIt;
428+
429+
result.append(processedFormula, lastPos, match.position() - lastPos);
430+
const int oldIdx = std::stoi(match[1].str());
431+
result += "x[" + std::to_string(newIndex[oldIdx]) + "]";
432+
433+
lastPos = match.position() + match.length();
434+
}
435+
result.append(processedFormula, lastPos, std::string::npos);
436+
437+
return result;
438+
}
439+
392440
////////////////////////////////////////////////////////////////////////////////
393441
/// Change used variables to those with the same name in given list.
394442
/// \param[in] newDeps New dependents to replace the old ones.

roofit/roofitcore/src/RooFormula.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class RooFormula : public TNamed {
4949
void printMultiline(std::ostream &os, Int_t contents, bool verbose = false, TString indent = "") const;
5050

5151
std::string formulaString() const { return _tFormula ? _tFormula->GetTitle() : ""; }
52+
std::string reindexedFormulaForUsedVars() const;
5253
TFormula* getTFormula() const { return _tFormula.get(); }
5354

5455
private:

roofit/roofitcore/src/RooFormulaVar.cxx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ RooFormulaVar::RooFormulaVar(const char *name, const char *title, const char* in
8989
_value = traceEval(nullptr);
9090
} else {
9191
_formula = new RooFormula(GetName(), _formExpr, dependents, checkVariables);
92-
_formExpr = _formula->formulaString().c_str();
92+
_formExpr = _formula->reindexedFormulaForUsedVars().c_str();
9393
_actualVars.add(_formula->actualDependents());
9494
}
9595
}
@@ -112,7 +112,7 @@ RooFormulaVar::RooFormulaVar(const char *name, const char *title, const RooArgLi
112112
_value = traceEval(nullptr);
113113
} else {
114114
_formula = new RooFormula(GetName(), _formExpr, dependents, checkVariables);
115-
_formExpr = _formula->formulaString().c_str();
115+
_formExpr = _formula->reindexedFormulaForUsedVars().c_str();
116116
_actualVars.add(_formula->actualDependents());
117117
}
118118
}
@@ -132,7 +132,7 @@ RooFormulaVar::RooFormulaVar(const RooFormulaVar& other, const char* name) :
132132
}
133133
if (other._formula && other._formula->ok()) {
134134
_formula = new RooFormula(*other._formula);
135-
_formExpr = _formula->formulaString().c_str();
135+
_formExpr = _formula->reindexedFormulaForUsedVars().c_str();
136136
}
137137
}
138138

@@ -145,7 +145,7 @@ RooFormula& RooFormulaVar::getFormula() const
145145
if (!_formula) {
146146
// After being read from file, the formula object might not exist, yet:
147147
_formula = new RooFormula(GetName(), _formExpr, _actualVars);
148-
const_cast<TString&>(_formExpr) = _formula->formulaString().c_str();
148+
const_cast<TString &>(_formExpr) = _formula->reindexedFormulaForUsedVars().c_str();
149149
}
150150

151151
return *_formula;
@@ -180,7 +180,7 @@ bool RooFormulaVar::redirectServersHook(const RooAbsCollection& newServerList, b
180180
{
181181
bool error = getFormula().changeDependents(newServerList,mustReplaceAll,nameChange);
182182

183-
_formExpr = getFormula().GetTitle();
183+
_formExpr = getFormula().reindexedFormulaForUsedVars().c_str();
184184
return error || RooAbsReal::redirectServersHook(newServerList, mustReplaceAll, nameChange, isRecursive);
185185
}
186186

roofit/roofitcore/src/RooGenericPdf.cxx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ RooGenericPdf::RooGenericPdf(const char *name, const char *title, const RooArgLi
7676
_value = traceEval(nullptr);
7777
} else {
7878
_formula = new RooFormula(GetName(), _formExpr, dependents);
79-
_formExpr = _formula->formulaString().c_str();
79+
_formExpr = _formula->reindexedFormulaForUsedVars().c_str();
8080
_actualVars.add(_formula->actualDependents());
8181
}
8282
}
@@ -96,7 +96,7 @@ RooGenericPdf::RooGenericPdf(const char *name, const char *title,
9696
_value = traceEval(nullptr);
9797
} else {
9898
_formula = new RooFormula(GetName(), _formExpr, dependents);
99-
_formExpr = _formula->formulaString().c_str();
99+
_formExpr = _formula->reindexedFormulaForUsedVars().c_str();
100100
_actualVars.add(_formula->actualDependents());
101101
}
102102
}
@@ -124,7 +124,7 @@ RooFormula& RooGenericPdf::formula() const
124124
{
125125
if (!_formula) {
126126
_formula = new RooFormula(GetName(),_formExpr.Data(),_actualVars);
127-
const_cast<TString&>(_formExpr) = _formula->formulaString().c_str();
127+
const_cast<TString &>(_formExpr) = _formula->reindexedFormulaForUsedVars().c_str();
128128
}
129129
return *_formula ;
130130
}

roofit/roofitcore/test/testGenericPdf.cxx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Tests for the GenericPdf
22
// Authors: Stephan Hageboeck, CERN 05/2019
33
// Jonas Rembser, CERN 06/2022
4+
#include <TFile.h>
45

56
#include <RooArgList.h>
67
#include <RooBinning.h>
@@ -415,3 +416,28 @@ TEST(GenericPdf, BinnedBoundariesConsistentWithHistPdf)
415416
iterFormulaVar++;
416417
}
417418
}
419+
420+
// Regression test for https://github.com/root-project/root/issues/21371:
421+
// an unused parameter (b) is pruned, so the persisted @N indices must be
422+
// remapped or the formula silently mismaps after a write/read cycle.
423+
TEST(GenericPdf, SerializationWithUnusedParam)
424+
{
425+
RooWorkspace w("w");
426+
w.factory("a[2,-10,10]");
427+
w.factory("b[99,-10,10]");
428+
w.factory("c[3,-10,10]");
429+
w.factory("d[4,-10,10]");
430+
w.factory("EXPR::pdf('@0*@2+d', a, b, c, d)");
431+
432+
TString fn = "RooGenericPdfSerialization.root";
433+
w.writeToFile(fn);
434+
TFile fin(fn);
435+
RooWorkspace *w2 = nullptr;
436+
fin.GetObject("w", w2);
437+
ASSERT_NE(w2, nullptr);
438+
auto *pdf = static_cast<RooAbsReal *>(w2->pdf("pdf"));
439+
440+
// If @2 still maps to c, changing c updates pdf = a*c + d = 2*5 + 4 = 14.
441+
static_cast<RooRealVar *>(w2->var("c"))->setVal(5.0);
442+
EXPECT_DOUBLE_EQ(pdf->getVal(), 2.0 * 5.0 + 4.0);
443+
}

roofit/roofitcore/test/testRooFormula.cxx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Authors: Stephan Hageboeck, CERN 2020
33
// Jonas Rembser, CERN 2023
44
// Andrea Germinario, CERN 2025
5+
#include <TFile.h>
56

67
#include "../src/RooFormula.h"
78
#include <RooFormulaVar.h>
89
#include <RooRealVar.h>
910
#include <RooConstVar.h>
11+
#include <RooWorkspace.h>
1012

1113
#include <ROOT/TestSupport.hxx>
1214

@@ -92,6 +94,31 @@ TEST(RooFormula, UndefinedVariables)
9294
ASSERT_NO_THROW(RooFormulaVar f2("f2", "r + B + y", {r, B, y})) << "Formula with specified y must work.";
9395
}
9496

97+
// Regression test for https://github.com/root-project/root/issues/21371:
98+
// an unused parameter (b) is pruned, so the persisted @N indices must be
99+
// remapped or the formula silently mismaps after a write/read cycle.
100+
TEST(RooFormula, SerializationWithUnusedParam)
101+
{
102+
RooWorkspace w("w");
103+
w.factory("a[2,-10,10]");
104+
w.factory("b[99,-10,10]");
105+
w.factory("c[3,-10,10]");
106+
w.factory("d[4,-10,10]");
107+
w.factory("expr::f('@0*@2+d', a, b, c, d)");
108+
109+
TString fn = "RooFormulaSerialization.root";
110+
w.writeToFile(fn);
111+
TFile fin(fn);
112+
RooWorkspace *w2 = nullptr;
113+
fin.GetObject("w", w2);
114+
ASSERT_NE(w2, nullptr);
115+
auto *f = static_cast<RooAbsReal *>(w2->function("f"));
116+
117+
// If @2 still maps to c, changing c updates f = a*c + d = 2*5 + 4 = 14.
118+
static_cast<RooRealVar *>(w2->var("c"))->setVal(5.0);
119+
EXPECT_DOUBLE_EQ(f->getVal(), 2.0 * 5.0 + 4.0);
120+
}
121+
95122
TEST(RooFormula, RooConstVarSafeSubstitution)
96123
{
97124
// Check RooConst are substituted only by index

0 commit comments

Comments
 (0)