Skip to content

Commit f85b2f6

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 89c9763 commit f85b2f6

9 files changed

Lines changed: 284 additions & 43 deletions

File tree

README/ReleaseNotes/v642/index.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ 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 with a
77+
`RooAbsAnaConvPdf` (like `RooDecay`, `RooBDecay`, etc.), it acts as a *configuration* object that
78+
specifies which model to convolve the basis functions with, rather than as a node of the pdf's
79+
computation graph. The `RooAbsAnaConvPdf` builds its own internal basis-function convolutions from
80+
it and evaluates *those*.
81+
82+
Until now, the resolution model was nevertheless kept as a (non-value, non-shape) server of the
83+
`RooAbsAnaConvPdf`. As a side effect, importing such a pdf into a `RooWorkspace` also imported the
84+
original resolution model as a standalone workspace object, and it leaked into HS3/JSON exports,
85+
even though it played no role in the computation.
86+
87+
Starting with ROOT 6.42, a resolution model that is only used as the configuration of a
88+
`RooAbsAnaConvPdf` is no longer a server of that pdf, and is therefore not imported into the
89+
workspace on its own anymore. The model remains accessible via `RooAbsAnaConvPdf::getModel()`.
90+
91+
This is not expected to affect typical usage, since the resolution model was never part of the
92+
actual likelihood. Workspaces written with older ROOT versions are read back correctly via schema
93+
evolution.
94+
7495
## Graphics and GUI
7596

7697
## Geometry

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/src/RooResolutionModel.cxx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
* The choice of basis returned by basisCode() is guaranteed not to change
6161
* during the lifetime of a RooResolutionModel object.
6262
*
63+
* When a resolution model is passed to a RooAbsAnaConvPdf (e.g. RooDecay), it
64+
* acts as a *configuration* object rather than as a node of that pdf's
65+
* computation graph: the RooAbsAnaConvPdf builds its own internal
66+
* basis-function convolutions from it and does not keep it as a server. As of
67+
* ROOT 6.42, such a "config-only" resolution model is therefore not imported
68+
* into a RooWorkspace together with the RooAbsAnaConvPdf that consumes it. See
69+
* RooAbsAnaConvPdf for details.
70+
*
6371
*/
6472

6573
#include "TClass.h"

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)