Skip to content

Schema evolution for RooBernsteinFast (with custom streamers)#1251

Closed
maxgalli wants to merge 1 commit into
cms-analysis:mainfrom
maxgalli:roo_bernstein_deprecation_2
Closed

Schema evolution for RooBernsteinFast (with custom streamers)#1251
maxgalli wants to merge 1 commit into
cms-analysis:mainfrom
maxgalli:roo_bernstein_deprecation_2

Conversation

@maxgalli

@maxgalli maxgalli commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Replaces #1250.
Since the original implementation using I/O customization rules in the xml file was giving segfaults difficult to debug, I helped myself with Claude and implemented the schema evolution using custom streamers [1]. More details about the reason for this and tests can be found in the above-mentioned PR.

[1] https://root.cern/manual/io/#schema-evolution-with-custom-streamers

Summary by CodeRabbit

  • Improvements
    • RooBernsteinFast now maintains full backward compatibility with objects and files saved in previous software versions, ensuring seamless restoration and data integrity across version updates.

Review Change Stack

Since the implementation using a linkdef file was triggering segfaults difficult to understand, we implement schema evolution for this class using custom streamers [1].
The main operation consists in prepending 1.0 to the coefficient list, since in ROOT RooBernstein's the first term has to be provided specifically, whereas in the Combine
version 1.0 was hardcoded.

[1] https://root.cern/manual/io/#schema-evolution-with-custom-streamers
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

RooBernsteinFast was refactored from a standalone RooAbsPdf subclass into a lightweight template wrapper inheriting from RooBernstein. The constructor now delegates to RooBernstein after transforming the coefficient list via a new buildFullList helper that prepends a constant 1.0. A custom Streamer provides backward compatibility for reading v1 persisted objects by restoring the missing constant coefficient. The implementation file was simplified to minimal instantiation logic.

Changes

RooBernsteinFast Refactoring

Layer / File(s) Summary
Class refactoring and constructor delegation
interface/RooBernsteinFast.h
RooBernsteinFast now inherits from RooBernstein instead of RooAbsPdf. The constructor delegates to RooBernstein via a new private static buildFullList helper that prepends a constant coefficient (1.0) to user-supplied coefficients. The ROOT class version was updated to 2. Explicit copy constructor, clone(), destructor, and method overrides were removed in favor of inherited implementations.
Backward compatibility Streamer for v1 objects
interface/RooBernsteinFast.h
A custom Streamer template specialization handles reading old v1 persisted RooBernsteinFast objects. On read, it detects version < 2, accesses the _coefList member via reflection, and if its length matches N, prepends the missing constant 1.0 by rewriting the proxy contents to restore RooBernstein's expected N+1 coefficient layout. On write, it delegates to the base class.
Implementation file cleanup
src/RooBernsteinFast.cc
The implementation file was reduced to a single include, changed namespace directives from using namespace std; to using namespace RooFit;, removed obsolete includes and header comments, and removed the templateClassImp(RooBernsteinCoeffs) instantiation while retaining templateClassImp(RooBernsteinFast).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • RooBernsteinFast obsolete (?) #1249: Directly addresses the refactoring of RooBernsteinFast to delegate to RooBernstein rather than maintaining a separate RooAbsPdf implementation, resolving concerns about class redundancy and consolidating Bernstein polynomial functionality.

Poem

🐰 A template once stood alone and proud,
But now it whispers to its parent cloud.
With buildFullList's magic wand so bright,
Old coefficients merge with constant light—
Streamer stitches history, v1 to v2 true,
One elegant wrapper does what many did do! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Schema evolution for RooBernsteinFast (with custom streamers)' directly and clearly describes the main change: implementing schema evolution using custom streamers for the RooBernsteinFast class, which is the core purpose of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@interface/RooBernsteinFast.h`:
- Around line 11-23: The RooBernsteinFast<N> class must reintroduce a copy
constructor and override clone() so copies preserve the RooBernsteinFast<N>
dynamic type; add a copy ctor (e.g., RooBernsteinFast(const RooBernsteinFast&
other)) and implement RooAbsArg* clone(const char* newname) const override to
return new RooBernsteinFast<N>(*this) (with renamed instance if needed),
ensuring the class-specific Streamer/IO behavior is retained instead of falling
back to RooBernstein::clone; target the RooBernsteinFast template class and its
clone/copy ctor symbols when making the change.
- Around line 16-21: The constructor RooBernsteinFast currently takes
RooAbsReal& but immediately dynamic_casts to RooAbsRealLValue&, risking
std::bad_cast; change the constructor signature to accept RooAbsRealLValue&
(i.e., RooBernsteinFast(const char* name, const char* title, RooAbsRealLValue&
x, const RooArgList& coefList)) and remove the dynamic_cast in the initializer
where RooBernstein is called (use x directly) so the API requires an lvalue
observable and avoids the runtime cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be2eb4b5-e9e2-4dde-b889-e766344d1541

📥 Commits

Reviewing files that changed from the base of the PR and between da0a2f4 and 306a840.

📒 Files selected for processing (2)
  • interface/RooBernsteinFast.h
  • src/RooBernsteinFast.cc

Comment on lines +11 to +23
template<int N> class RooBernsteinFast : public RooBernstein {
public:

RooBernsteinFast() = default;


RooBernsteinFast() {}

RooBernsteinFast(const char *name, const char *title,
RooAbsReal& x, const RooArgList& coefList) :
RooAbsPdf(name, title),
_x("x", "Dependent", this, x),
_coefList("coefList","List of coefficients",this)
{
_coefList.add(coefList);


//precompute coefficients for integral
for (int ipow=0; ipow<=N; ++ipow) {
_rvector(ipow) = 1.0/((double)ipow+1.0);
}
RooAbsReal& x, const RooArgList& coefList) :
RooBernstein(name, title,
dynamic_cast<RooAbsRealLValue&>(x),
buildFullList(coefList))
{}

//precompute coefficients for conversion from bernstein basis to power basis
for (int ibern=0; ibern<=N; ++ibern) {
for (int ipow=0; ipow<ibern; ++ipow) {
_cmatrix(ipow,ibern) = 0.;
}
for (int ipow=ibern; ipow<=N; ++ipow) {
_cmatrix(ipow, ibern) = pow(-1.,ipow-ibern)*TMath::Binomial(N,ipow)*TMath::Binomial(ipow,ibern);
}
}

}

ClassDefOverride(RooBernsteinFast, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate the RooBernstein class definition
find . -name "*.h" -o -name "*.cpp" | head -20

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 638


🏁 Script executed:

# Search for RooBernstein class definition
rg "class RooBernstein" -A 5

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 419


🏁 Script executed:

# Look for clone() method in RooBernstein
rg "clone\(" --type-list | head -5
rg -t cpp -t h "clone\(" -B 2 -A 2 | grep -E "(RooBernstein|clone)" | head -30

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 3479


🏁 Script executed:

# Find where RooBernstein might be defined
fd "RooBernstein" --type f

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 137


🏁 Script executed:

# Check the implementation file
cat -n src/RooBernsteinFast.cc

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 223


🏁 Script executed:

# Search for usages of RooBernsteinFast to understand workflow
rg "RooBernsteinFast" -B 2 -A 2

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 3462


🏁 Script executed:

# Check if there are other templated RooFit classes and how they handle clone()
rg "template.*class.*Roo" -A 15 interface/ | head -80

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 2182


Restore clone() so copies stay RooBernsteinFast<N>.

Dropping the override here means RooFit copy/import paths will fall back to RooBernstein::clone(), which produces a plain RooBernstein. Since RooBernsteinFast is registered for ROOT I/O persistence with custom Streamer logic for schema migration, type preservation is critical—returning the base type breaks type fidelity during cloning and serialization. Reintroducing the copy ctor + clone() override keeps the derived type intact, consistent with patterns used elsewhere in the codebase (e.g., FastTemplateFunc_t).

Possible localized fix
 public:
   RooBernsteinFast() = default;
+
+  RooBernsteinFast(const RooBernsteinFast& other, const char *name = nullptr)
+      : RooBernstein(other, name) {}
 
   RooBernsteinFast(const char *name, const char *title,
                    RooAbsReal& x, const RooArgList& coefList) :
     RooBernstein(name, title,
                  dynamic_cast<RooAbsRealLValue&>(x),
                  buildFullList(coefList))
   {}
+
+  TObject *clone(const char *newname) const override {
+    return new RooBernsteinFast(*this, newname);
+  }
 
   ClassDefOverride(RooBernsteinFast, 2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template<int N> class RooBernsteinFast : public RooBernstein {
public:
RooBernsteinFast() = default;
RooBernsteinFast() {}
RooBernsteinFast(const char *name, const char *title,
RooAbsReal& x, const RooArgList& coefList) :
RooAbsPdf(name, title),
_x("x", "Dependent", this, x),
_coefList("coefList","List of coefficients",this)
{
_coefList.add(coefList);
//precompute coefficients for integral
for (int ipow=0; ipow<=N; ++ipow) {
_rvector(ipow) = 1.0/((double)ipow+1.0);
}
RooAbsReal& x, const RooArgList& coefList) :
RooBernstein(name, title,
dynamic_cast<RooAbsRealLValue&>(x),
buildFullList(coefList))
{}
//precompute coefficients for conversion from bernstein basis to power basis
for (int ibern=0; ibern<=N; ++ibern) {
for (int ipow=0; ipow<ibern; ++ipow) {
_cmatrix(ipow,ibern) = 0.;
}
for (int ipow=ibern; ipow<=N; ++ipow) {
_cmatrix(ipow, ibern) = pow(-1.,ipow-ibern)*TMath::Binomial(N,ipow)*TMath::Binomial(ipow,ibern);
}
}
}
ClassDefOverride(RooBernsteinFast, 2)
template<int N> class RooBernsteinFast : public RooBernstein {
public:
RooBernsteinFast() = default;
RooBernsteinFast(const RooBernsteinFast& other, const char *name = nullptr)
: RooBernstein(other, name) {}
RooBernsteinFast(const char *name, const char *title,
RooAbsReal& x, const RooArgList& coefList) :
RooBernstein(name, title,
dynamic_cast<RooAbsRealLValue&>(x),
buildFullList(coefList))
{}
TObject *clone(const char *newname) const override {
return new RooBernsteinFast(*this, newname);
}
ClassDefOverride(RooBernsteinFast, 2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interface/RooBernsteinFast.h` around lines 11 - 23, The RooBernsteinFast<N>
class must reintroduce a copy constructor and override clone() so copies
preserve the RooBernsteinFast<N> dynamic type; add a copy ctor (e.g.,
RooBernsteinFast(const RooBernsteinFast& other)) and implement RooAbsArg*
clone(const char* newname) const override to return new
RooBernsteinFast<N>(*this) (with renamed instance if needed), ensuring the
class-specific Streamer/IO behavior is retained instead of falling back to
RooBernstein::clone; target the RooBernsteinFast template class and its
clone/copy ctor symbols when making the change.

Comment on lines 16 to +21
RooBernsteinFast(const char *name, const char *title,
RooAbsReal& x, const RooArgList& coefList) :
RooAbsPdf(name, title),
_x("x", "Dependent", this, x),
_coefList("coefList","List of coefficients",this)
{
_coefList.add(coefList);


//precompute coefficients for integral
for (int ipow=0; ipow<=N; ++ipow) {
_rvector(ipow) = 1.0/((double)ipow+1.0);
}
RooAbsReal& x, const RooArgList& coefList) :
RooBernstein(name, title,
dynamic_cast<RooAbsRealLValue&>(x),
buildFullList(coefList))
{}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "RooBernsteinFast.h" -type f

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 115


🏁 Script executed:

find . -name "RooBernstein.h" -type f

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 66


🏁 Script executed:

cat -n ./interface/RooBernsteinFast.h

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 2698


🏁 Script executed:

rg "RooBernsteinFast\s*<" --type cpp --type h -A 3 -B 1

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 399


🏁 Script executed:

rg "RooBernsteinFast\(" --type cpp --type h -B 2 -A 5

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 666


🏁 Script executed:

find . -name "*.cc" -o -name "*.cpp" | head -20

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 574


🏁 Script executed:

rg "new RooBernsteinFast|make.*RooBernsteinFast" --type cpp

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 66


🏁 Script executed:

rg "RooBernsteinFast" --type cpp --type h

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 1091


🏁 Script executed:

cat -n ./src/RooBernsteinFast.cc

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 223


Tighten the observable type instead of failing with std::bad_cast.

This constructor accepts any RooAbsReal&, but Line 19 immediately requires RooAbsRealLValue&. That turns an API mismatch into a runtime failure for callers that pass a derived expression rather than an lvalue observable. Change the signature to RooAbsRealLValue& and remove the unnecessary dynamic_cast.

Suggested fix
   RooBernsteinFast(const char *name, const char *title,
-                   RooAbsReal& x, const RooArgList& coefList) :
+                   RooAbsRealLValue& x, const RooArgList& coefList) :
     RooBernstein(name, title,
-                 dynamic_cast<RooAbsRealLValue&>(x),
+                 x,
                  buildFullList(coefList))
   {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RooBernsteinFast(const char *name, const char *title,
RooAbsReal& x, const RooArgList& coefList) :
RooAbsPdf(name, title),
_x("x", "Dependent", this, x),
_coefList("coefList","List of coefficients",this)
{
_coefList.add(coefList);
//precompute coefficients for integral
for (int ipow=0; ipow<=N; ++ipow) {
_rvector(ipow) = 1.0/((double)ipow+1.0);
}
RooAbsReal& x, const RooArgList& coefList) :
RooBernstein(name, title,
dynamic_cast<RooAbsRealLValue&>(x),
buildFullList(coefList))
{}
RooBernsteinFast(const char *name, const char *title,
RooAbsRealLValue& x, const RooArgList& coefList) :
RooBernstein(name, title,
x,
buildFullList(coefList))
{}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interface/RooBernsteinFast.h` around lines 16 - 21, The constructor
RooBernsteinFast currently takes RooAbsReal& but immediately dynamic_casts to
RooAbsRealLValue&, risking std::bad_cast; change the constructor signature to
accept RooAbsRealLValue& (i.e., RooBernsteinFast(const char* name, const char*
title, RooAbsRealLValue& x, const RooArgList& coefList)) and remove the
dynamic_cast in the initializer where RooBernstein is called (use x directly) so
the API requires an lvalue observable and avoids the runtime cast.

@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 20.81%. Comparing base (da0a2f4) to head (306a840).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1251      +/-   ##
==========================================
- Coverage   20.90%   20.81%   -0.09%     
==========================================
  Files         195      195              
  Lines       26316    26276      -40     
  Branches     3947     3938       -9     
==========================================
- Hits         5502     5470      -32     
+ Misses      20814    20806       -8     
Files with missing lines Coverage Δ
interface/RooBernsteinFast.h 100.00% <100.00%> (+21.42%) ⬆️

... and 1 file with indirect coverage changes

Files with missing lines Coverage Δ
interface/RooBernsteinFast.h 100.00% <100.00%> (+21.42%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@maxgalli maxgalli marked this pull request as draft May 12, 2026 09:50
@maxgalli

Copy link
Copy Markdown
Collaborator Author

Closing as it doesn't work as intended

@maxgalli maxgalli closed this May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant