Skip to content

Commit 9a275ee

Browse files
committed
[RF] Don't keep the resolution model as a server of RooAbsAnaConvPdf
The resolution model passed to a RooAbsAnaConvPdf (RooDecay, RooBDecay, ...) is only a *configuration* object: it specifies which model to convolve the basis functions with, and the pdf evaluates its own internal convolutions, not the model itself. Yet it was kept as a (non-value, non-shape) server, which polluted the computation graph and dragged the model into RooWorkspace imports and HS3/JSON exports for no reason. It also broke the HS3 round-trip, since the internal convolution objects leaked into the JSON with non-conforming names. The model is now an owned, non-server member instead. This deliberately mirrors how RooResolutionModel already manages its basis function: a raw owned pointer (_basis / _ownBasis) that is not a server and whose redirection is forwarded in redirectServersHook(). See RooResolutionModel.{h,cxx}. The HS3 exporter now lists its real dependents explicitly (t, tau, model) instead of auto-exporting servers. This follows up on 478ad2b, which noted that the model server could not simply be removed without a server/proxy desync; removing the proxy entirely avoids that desync by construction. Schema evolution from class version 3 to 4 is handled with the standard RooFit mechanism: a versioned `#pragma read` rule in LinkDef.h recovers the model pointer from the old RooRealProxy, and a RooAbsAnaConvPdf:: ioStreamerPass2() override severs the stale server link once the graph is live. This is the same two-stage pattern already used for the v5->v6 proxy-list migration (RooAbsArg::ioStreamerPass2) and for the read rules of RooProduct / RooSuperCategory in roofitcore/inc/LinkDef.h. A unit test reads back a v3 workspace fixture and checks the server structure, getModel() and the evaluated values. Closes #20245. 🤖 Done with the help of [Claude Code](https://claude.com/claude-code) (Claude Opus 4.8)
1 parent a622764 commit 9a275ee

8 files changed

Lines changed: 271 additions & 44 deletions

File tree

README/ReleaseNotes/v642/index.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ Users are strongly encouraged to switch to the vectorized CPU backend if they ar
7171

7272
If the vectorized backend does not work for a given use case, **please report it by opening an issue on the ROOT GitHub repository**.
7373

74+
### Resolution models are no longer imported into the workspace as standalone objects
75+
76+
When a `RooResolutionModel` (like `RooGaussModel` or `RooTruthModel`) is used wi th a
77+
`RooAbsAnaConvPdf` (like `RooDecay`, `RooBDecay`, etc.), it acts as a *configuration* object that specifies which model to convolve the basis functions with, rather than as a nod
78+
e of the pdf's computation graph.
79+
The `RooAbsAnaConvPdf` builds its own internal basis-function convolutions from it and evaluates *those*.
80+
81+
Until now, the resolution model was nevertheless kept as a (non-value, non-shape) server of the `RooAbsAnaConvPdf`.
82+
As a side effect, importing such a pdf into a `RooWorkspace` also imported the original resolution model as a standalone workspace object, and it leaked into HS3/JSON exports, even though it played no role in the computation.
83+
84+
Starting with ROOT 6.42, a resolution model that is only used as the configurati on of a `RooAbsAnaConvPdf` is no longer a server of that pdf, and is therefore not imported into the
85+
workspace on its own anymore. The model remains accessible via `RooAbsAnaConvPdf::getModel()`.
86+
87+
This is not expected to affect typical usage, since the resolution model was never part of the actual likelihood. Workspaces written with older ROOT versions are read back correctly via schema evolution.
88+
7489
## Graphics and GUI
7590

7691
## Geometry
@@ -83,4 +98,4 @@ If the vectorized backend does not work for a given use case, **please report it
8398

8499
The version of the following packages has been updated:
85100

86-
- xrootd: 5.9.5
101+
- xrootd: 5.9.5

roofit/hs3/src/JSONFactories_RooFitCore.cxx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,12 @@ class RooPoissonStreamer : public RooFit::JSONIO::Exporter {
921921
class RooDecayStreamer : public RooFit::JSONIO::Exporter {
922922
public:
923923
std::string const &key() const override;
924-
bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
924+
// The RooDecay servers are the internal resModel-times-basis convolutions,
925+
// which are implementation details that should not leak into the JSON. We
926+
// only export the actual dependents (t, tau and the original resolution
927+
// model) explicitly.
928+
bool autoExportDependants() const override { return false; }
929+
bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *func, JSONNode &elem) const override
925930
{
926931
auto *pdf = static_cast<const RooDecay *>(func);
927932
elem["type"] << key();
@@ -930,6 +935,10 @@ class RooDecayStreamer : public RooFit::JSONIO::Exporter {
930935
elem["resolutionModel"] << pdf->getModel().GetName();
931936
elem["decayType"] << pdf->getDecayType();
932937

938+
tool->queueExport(pdf->getT());
939+
tool->queueExport(pdf->getTau());
940+
tool->queueExport(pdf->getModel());
941+
933942
return true;
934943
}
935944
};

roofit/roofitcore/inc/LinkDef.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@
9393
#pragma link C++ class RooDirItem+ ;
9494
#pragma link C++ class RooDLLSignificanceMCSModule+ ;
9595
#pragma link C++ class RooAbsAnaConvPdf+ ;
96+
// Schema evolution: up to version 3, the original resolution model was stored
97+
// in a RooRealProxy (evolved to RooTemplateProxy<RooAbsReal>) and was a server
98+
// of the pdf. As of version 4 it is an owned, non-server RooResolutionModel*.
99+
// Recover the model pointer from the old proxy here; the stale server link is
100+
// cleaned up afterwards in RooAbsAnaConvPdf::ioStreamerPass2().
101+
#pragma read sourceClass="RooAbsAnaConvPdf" targetClass="RooAbsAnaConvPdf" version="[-3]" \
102+
include="RooResolutionModel.h" \
103+
source="RooTemplateProxy<RooAbsReal> _model" target="_model,_ownModel" \
104+
code="{ _model = static_cast<RooResolutionModel*>(onfile._model.absArg()); _ownModel = false; }"
96105
#pragma link C++ class RooAddPdf+ ;
97106
#pragma link C++ class RooEfficiency+ ;
98107
#pragma link C++ class RooEffProd+ ;

roofit/roofitcore/inc/RooAbsAnaConvPdf.h

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
#include "RooAICRegistry.h"
2525
#include "RooObjCacheManager.h"
2626
#include "RooAbsCacheElement.h"
27+
#include "RooResolutionModel.h"
2728

28-
class RooResolutionModel ;
2929
class RooRealVar ;
3030
class RooConvGenContext ;
3131

@@ -81,7 +81,10 @@ class RooAbsAnaConvPdf : public RooAbsPdf {
8181
}
8282

8383
/// Get the resolution model.
84-
RooAbsReal const &getModel() const { return _model.arg(); }
84+
/// Note that the resolution model is only a configuration object specifying
85+
/// the model to convolve the basis functions with; it is not a server of this
86+
/// pdf and is not part of its computation graph (see the class documentation).
87+
RooAbsReal const &getModel() const { return *_model; }
8588

8689
std::unique_ptr<RooAbsArg> compileForNormSet(RooArgSet const &normSet, RooFit::Detail::CompileContext & ctx) const override;
8790

@@ -92,11 +95,24 @@ class RooAbsAnaConvPdf : public RooAbsPdf {
9295

9396
double evaluate() const override ;
9497

98+
bool redirectServersHook(const RooAbsCollection &newServerList, bool mustReplaceAll, bool nameChange,
99+
bool isRecursive) override;
100+
101+
void ioStreamerPass2() override;
102+
95103
void makeCoefVarList(RooArgList&) const ;
96104

97105
friend class RooConvGenContext ;
98106

99-
RooRealProxy _model ; ///< Original model
107+
// The original resolution model is intentionally *not* a server of the
108+
// RooAbsAnaConvPdf: it is only used to build the convolutions (stored in
109+
// _convSet) and for some operations like generation. Keeping it as a server
110+
// would pollute the computation graph (and any RooWorkspace or JSON export)
111+
// with an object that is never evaluated. It is owned and kept in sync with
112+
// the rest of the graph via redirectServersHook(), analogous to how
113+
// RooResolutionModel handles its basis function.
114+
RooResolutionModel *_model = nullptr; ///< Original resolution model (not a server)
115+
bool _ownModel = false; ///< Flag indicating ownership of _model
100116
RooRealProxy _convVar ; ///< Convolution variable
101117

102118
RooArgSet* parseIntegrationRequest(const RooArgSet& intSet, Int_t& coefCode, RooArgSet* analVars=nullptr) const ;
@@ -120,7 +136,7 @@ class RooAbsAnaConvPdf : public RooAbsPdf {
120136

121137
mutable RooAICRegistry _codeReg ; ///<! Registry of analytical integration codes
122138

123-
ClassDefOverride(RooAbsAnaConvPdf,3) // Abstract Composite Convoluted PDF
139+
ClassDefOverride(RooAbsAnaConvPdf, 4) // Abstract Composite Convoluted PDF
124140
};
125141

126142
#endif

roofit/roofitcore/src/RooAbsAnaConvPdf.cxx

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
///
3434
/// Classes derived from RooResolutionModel implement
3535
/// \f[
36-
/// R_k(x,\bar{b},\bar{c}) = \int \mathrm{basis}_k(x', \bar{b}) \cdot \mathrm{resModel}(x-x',\bar{c}) \; \mathrm{d}x',
36+
/// R_k(x,\bar{b},\bar{c}) = \int \mathrm{basis}_k(x', \bar{b}) \cdot \mathrm{resModel}(x-x',\bar{c}) \;
37+
/// \mathrm{d}x',
3738
/// \f]
3839
///
3940
/// which RooAbsAnaConvPdf uses to construct the pdf for [ Phys (x) R ] :
@@ -59,6 +60,40 @@
5960
/// advertise the coefficient integration capabilities and implement them respectively.
6061
/// Please see RooAbsPdf for additional details. Advertised analytical integrals must be
6162
/// valid for all coefficients.
63+
///
64+
/// ### The resolution model is a configuration object, not a graph node
65+
///
66+
/// The resolution model passed to the constructor is **not** a node of the
67+
/// computation graph of the RooAbsAnaConvPdf. It is never evaluated directly;
68+
/// it only serves as a *configuration* object that specifies which resolution
69+
/// model should be convolved with the basis functions. From it, the
70+
/// RooAbsAnaConvPdf builds its own internal \f$ \mathrm{basis}_k \otimes
71+
/// \mathrm{resModel} \f$ convolution objects (one per declared basis function),
72+
/// and it is *those* convolutions that are the actual value servers of the pdf
73+
/// and that get evaluated.
74+
///
75+
/// Consequently, the resolution model itself is not a server of the
76+
/// RooAbsAnaConvPdf. It remains accessible via getModel() (for example for
77+
/// serialization), but it does not appear in the pdf's `servers()` list, in
78+
/// `getParameters()` / `getVariables()`, or in the printed computation graph.
79+
///
80+
/// \note **Behavior change in ROOT 6.42:** in earlier releases the resolution
81+
/// model was kept as a (non-value, non-shape) server of the RooAbsAnaConvPdf.
82+
/// As a side effect, importing a RooAbsAnaConvPdf into a RooWorkspace also
83+
/// dragged the original resolution model into the workspace (and into HS3/JSON
84+
/// exports), even though it played no role in the computation. As of ROOT 6.42
85+
/// this is no longer the case: a resolution model that is only used as the
86+
/// configuration of a RooAbsAnaConvPdf is not imported into the workspace on
87+
/// its own anymore.
88+
///
89+
/// Objects written with older ROOT versions are read back correctly via schema
90+
/// evolution: the resolution model is dropped as a server, so the *pdf's*
91+
/// computation graph is the same as for a freshly constructed one. Note,
92+
/// however, that if such an old file already stored the resolution model as a
93+
/// standalone RooWorkspace member (which used to happen on import), that member
94+
/// is *not* retroactively removed from the workspace on read-back -- it simply
95+
/// is no longer wired into the RooAbsAnaConvPdf. Only newly created workspaces
96+
/// are guaranteed to be free of the standalone resolution model.
6297

6398
#include "RooAbsAnaConvPdf.h"
6499

@@ -93,33 +128,37 @@ RooAbsAnaConvPdf::RooAbsAnaConvPdf() :
93128
/// Constructor. The supplied resolution model must be constructed with the same
94129
/// convoluted variable as this physics model ('convVar')
95130

96-
RooAbsAnaConvPdf::RooAbsAnaConvPdf(const char *name, const char *title,
97-
const RooResolutionModel& model, RooRealVar& cVar) :
98-
RooAbsPdf(name,title), _isCopy(false),
99-
_model("!model","Original resolution model",this,(RooResolutionModel&)model,false,false),
100-
_convVar("!convVar","Convolution variable",this,cVar,false,false),
101-
_convSet("!convSet","Set of resModel X basisFunc convolutions",this),
102-
_coefNormMgr(this,10),
103-
_codeReg(10)
131+
RooAbsAnaConvPdf::RooAbsAnaConvPdf(const char *name, const char *title, const RooResolutionModel &model,
132+
RooRealVar &cVar)
133+
: RooAbsPdf(name, title),
134+
_isCopy(false),
135+
_model{static_cast<RooResolutionModel *>(model.clone(model.GetName()))},
136+
_ownModel{true},
137+
_convVar("!convVar", "Convolution variable", this, cVar, false, false),
138+
_convSet("!convSet", "Set of resModel X basisFunc convolutions", this),
139+
_coefNormMgr(this, 10),
140+
_codeReg(10)
104141
{
105-
_model.absArg()->setAttribute("NOCacheAndTrack") ;
142+
_model->setAttribute("NOCacheAndTrack");
106143
}
107144

108145

109146

110147
////////////////////////////////////////////////////////////////////////////////
111148

112-
RooAbsAnaConvPdf::RooAbsAnaConvPdf(const RooAbsAnaConvPdf& other, const char* name) :
113-
RooAbsPdf(other,name), _isCopy(true),
114-
_model("!model",this,other._model),
115-
_convVar("!convVar",this,other._convVar),
116-
_convSet("!convSet",this,other._convSet),
117-
_coefNormMgr(other._coefNormMgr,this),
118-
_codeReg(other._codeReg)
149+
RooAbsAnaConvPdf::RooAbsAnaConvPdf(const RooAbsAnaConvPdf &other, const char *name)
150+
: RooAbsPdf(other, name),
151+
_isCopy(true),
152+
_model{other._model ? static_cast<RooResolutionModel *>(other._model->clone(other._model->GetName())) : nullptr},
153+
_ownModel{true},
154+
_convVar("!convVar", this, other._convVar),
155+
_convSet("!convSet", this, other._convSet),
156+
_coefNormMgr(other._coefNormMgr, this),
157+
_codeReg(other._codeReg)
119158
{
120159
// Copy constructor
121-
if (_model.absArg()) {
122-
_model.absArg()->setAttribute("NOCacheAndTrack") ;
160+
if (_model) {
161+
_model->setAttribute("NOCacheAndTrack");
123162
}
124163
other._basisList.snapshot(_basisList);
125164
}
@@ -140,8 +179,53 @@ RooAbsAnaConvPdf::~RooAbsAnaConvPdf()
140179
}
141180
}
142181

182+
if (_ownModel) {
183+
delete _model;
184+
}
143185
}
144186

187+
////////////////////////////////////////////////////////////////////////////////
188+
/// Forward server redirection to the original resolution model. The resolution
189+
/// model is not a server of this pdf (it is only used to build the convolutions
190+
/// and for generation), so it is not redirected by the standard machinery. We
191+
/// keep its servers in sync here, analogous to RooResolutionModel forwarding
192+
/// the redirection to its basis function.
193+
194+
bool RooAbsAnaConvPdf::redirectServersHook(const RooAbsCollection &newServerList, bool mustReplaceAll, bool nameChange,
195+
bool isRecursive)
196+
{
197+
if (_model) {
198+
// Pass mustReplaceAll=false: the model may legitimately reference servers
199+
// that are not part of this particular redirection, and it is never
200+
// evaluated as part of the computation graph anyway.
201+
_model->redirectServers(newServerList, false, nameChange);
202+
}
203+
204+
return RooAbsPdf::redirectServersHook(newServerList, mustReplaceAll, nameChange, isRecursive);
205+
}
206+
207+
////////////////////////////////////////////////////////////////////////////////
208+
/// Second-pass schema evolution, called by the RooWorkspace after reading.
209+
///
210+
/// In class version <= 3, the original resolution model was held in a
211+
/// RooRealProxy and was therefore a (non-value, non-shape) server of this pdf.
212+
/// The schema evolution read rule (see LinkDef.h) already recovered the model
213+
/// pointer into _model, but the stale server link is also restored from the
214+
/// file via the RooAbsArg server list. We remove it here, once the full graph
215+
/// is live, so that an object read from an old file has the same clean server
216+
/// structure as a freshly constructed one. This is safe because there is no
217+
/// longer a proxy that could resurrect the server link on copy.
218+
219+
void RooAbsAnaConvPdf::ioStreamerPass2()
220+
{
221+
RooAbsPdf::ioStreamerPass2();
222+
223+
// Force removal (the link may have been added with a reference count > 1):
224+
// the model must be completely severed from the server list.
225+
if (_model && findServer(*_model)) {
226+
removeServer(*_model, true);
227+
}
228+
}
145229

146230
////////////////////////////////////////////////////////////////////////////////
147231
/// Declare a basis function for use in this physics model. The string expression
@@ -165,11 +249,10 @@ Int_t RooAbsAnaConvPdf::declareBasis(const char* expression, const RooArgList& p
165249
}
166250

167251
// Resolution model must support declared basis
168-
if (!(static_cast<RooResolutionModel*>(_model.absArg()))->isBasisSupported(expression)) {
169-
coutE(InputArguments) << "RooAbsAnaConvPdf::declareBasis(" << GetName() << "): resolution model "
170-
<< _model.absArg()->GetName()
171-
<< " doesn't support basis function " << expression << std::endl ;
172-
return -1 ;
252+
if (!_model->isBasisSupported(expression)) {
253+
coutE(InputArguments) << "RooAbsAnaConvPdf::declareBasis(" << GetName() << "): resolution model "
254+
<< _model->GetName() << " doesn't support basis function " << expression << std::endl;
255+
return -1;
173256
}
174257

175258
// Instantiate basis function
@@ -188,7 +271,7 @@ Int_t RooAbsAnaConvPdf::declareBasis(const char* expression, const RooArgList& p
188271
basisFunc->setOperMode(operMode()) ;
189272

190273
// Instantiate resModel x basisFunc convolution
191-
RooAbsReal* conv = static_cast<RooResolutionModel*>(_model.absArg())->convolution(basisFunc.get(),this);
274+
RooAbsReal *conv = _model->convolution(basisFunc.get(), this);
192275
_basisList.addOwned(std::move(basisFunc));
193276
if (!conv) {
194277
coutE(InputArguments) << "RooAbsAnaConvPdf::declareBasis(" << GetName() << "): unable to construct convolution with basis function '"
@@ -229,14 +312,15 @@ bool RooAbsAnaConvPdf::changeModel(const RooResolutionModel& newModel)
229312
_convSet.removeAll() ;
230313
_convSet.addOwned(std::move(newConvSet));
231314

232-
const std::string attrib = std::string("ORIGNAME:") + _model->GetName();
233-
const bool oldAttrib = newModel.getAttribute(attrib.c_str());
234-
const_cast<RooResolutionModel&>(newModel).setAttribute(attrib.c_str());
235-
236-
redirectServers(RooArgSet{newModel}, false, true);
237-
238-
// reset temporary attribute for server redirection
239-
const_cast<RooResolutionModel&>(newModel).setAttribute(attrib.c_str(), oldAttrib);
315+
// Replace the stored original resolution model. Since it is not a server of
316+
// this pdf, it cannot (and need not) be redirected via redirectServers(): we
317+
// simply own a fresh clone of the new model.
318+
if (_ownModel) {
319+
delete _model;
320+
}
321+
_model = static_cast<RooResolutionModel *>(newModel.clone(newModel.GetName()));
322+
_ownModel = true;
323+
_model->setAttribute("NOCacheAndTrack");
240324

241325
return false ;
242326
}
@@ -255,7 +339,7 @@ RooAbsGenContext* RooAbsAnaConvPdf::genContext(const RooArgSet &vars, const RooD
255339
const RooArgSet* auxProto, bool verbose) const
256340
{
257341
// Check if the resolution model specifies a special context to be used.
258-
RooResolutionModel* conv = dynamic_cast<RooResolutionModel*>(_model.absArg());
342+
RooResolutionModel *conv = _model;
259343
assert(conv);
260344

261345
std::unique_ptr<RooArgSet> modelDep {_model->getObservables(&vars)};
@@ -297,9 +381,8 @@ bool RooAbsAnaConvPdf::isDirectGenSafe(const RooAbsArg& arg) const
297381
{
298382

299383
// All direct generation of convolution arg if model is truth model
300-
if (!TString(_convVar.absArg()->GetName()).CompareTo(arg.GetName()) &&
301-
dynamic_cast<RooTruthModel*>(_model.absArg())) {
302-
return true ;
384+
if (!TString(_convVar.absArg()->GetName()).CompareTo(arg.GetName()) && dynamic_cast<RooTruthModel *>(_model)) {
385+
return true;
303386
}
304387

305388
return RooAbsPdf::isDirectGenSafe(arg) ;

roofit/roofitcore/test/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ ROOT_ADD_GTEST(testRooMinimizer testRooMinimizer.cxx LIBRARIES RooFitCore RooFit
8686
ROOT_ADD_GTEST(testRooMulti testRooMulti.cxx LIBRARIES RooFitCore RooFit)
8787
ROOT_ADD_GTEST(testRooRombergIntegrator testRooRombergIntegrator.cxx LIBRARIES MathCore RooFitCore)
8888
ROOT_ADD_GTEST(testRooSimultaneous testRooSimultaneous.cxx LIBRARIES RooFitCore RooFit)
89-
ROOT_ADD_GTEST(testRooTruthModel testRooTruthModel.cxx LIBRARIES RooFitCore RooFit)
89+
ROOT_ADD_GTEST(testRooTruthModel testRooTruthModel.cxx LIBRARIES RooFitCore RooFit
90+
COPY_TO_BUILDDIR ${CMAKE_CURRENT_SOURCE_DIR}/rooAbsAnaConvPdf_classV3.root)
9091

9192
if (roofit_multiprocess)
9293
ROOT_ADD_GTEST(testTestStatisticsPlot TestStatistics/testPlot.cxx LIBRARIES RooFitMultiProcess RooFitCore RooFit
10.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)