Skip to content

Add codegen to RooParametricHist#1217

Merged
anigamova merged 2 commits intocms-analysis:mainfrom
runtingt:RooParametricHist_AD
Feb 20, 2026
Merged

Add codegen to RooParametricHist#1217
anigamova merged 2 commits intocms-analysis:mainfrom
runtingt:RooParametricHist_AD

Conversation

@runtingt
Copy link
Copy Markdown
Contributor

@runtingt runtingt commented Feb 17, 2026

Refactors evaluate and analyticalIntegral to free functions such that they can be used by codegen.

Marking this as a draft as I had to pre-compute the parameter values in each bin to materialise them into a std::vector<double>. This gets around the limitation of codegen requiring stateless functions, but may lead to slowdowns when running with the default backend.

@guitargeek is there a nice way out here? Perhaps something I've missed that would allow us to keep the old behaviour (evaluating in a single bin) for both backends?

Summary by CodeRabbit

  • New Features

    • Extended code generation support for parametric histograms with comprehensive morphing capabilities.
    • Added mathematical utilities for parametric histogram operations including bin finding, morphing calculations, and integral computations.
  • Refactor

    • Refactored parametric histogram class to expose additional public accessor methods for histogram data.
    • Consolidated internal computation logic into centralized mathematical utilities for improved maintainability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This pull request extends RooFit's codegen and math infrastructure to support RooParametricHist by introducing centralized math utilities, refactoring the class's evaluation logic, and adding public data accessors while maintaining existing behavior through new utility functions.

Changes

Cohort / File(s) Summary
Codegen Infrastructure
interface/CombineCodegenImpl.h, src/CombineCodegenImpl.cxx
Adds forward declaration and implements codegenImpl and codegenIntegralImpl overloads for RooParametricHist, with support for morphing data integration into generated code context.
Math Utilities
interface/CombineMathFuncs.h
Introduces six inline helper functions for parametric histogram operations: bin-finding (parametricHistFindBin), morphing evaluation (parametricHistMorphScale, parametricMorphFunction), value evaluation (parametricHistEvaluate), and integral computation (parametricHistFullSum, parametricHistIntegral) with morphing support.
RooParametricHist Refactoring
interface/RooParametricHist.h, src/RooParametricHist.cxx
Adds twelve public accessor methods for data extraction (e.g., observable(), getParVals(), getFlattenedMorphs()); refactors internal evaluation and integral computation to delegate to centralized math utilities instead of inline implementations; removes getFullSum(), evaluateMorphFunction(), evaluatePartial(), and evaluateFull().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested labels

testAD

Suggested reviewers

  • guitargeek
  • anigamova

Poem

🐰 Hop hop, the histogram now finds its bin,
Math functions centralized from within,
Morphing scales dance through the code so clean,
Codegen support—the best we've seen!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely describes the main change: adding codegen support to RooParametricHist, which is the primary objective of the PR.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 41.62162% with 108 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.96%. Comparing base (41415b8) to head (eb35ad1).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
interface/CombineMathFuncs.h 29.72% 52 Missing ⚠️
src/CombineCodegenImpl.cxx 0.00% 37 Missing ⚠️
src/RooParametricHist.cxx 80.59% 13 Missing ⚠️
interface/RooParametricHist.h 14.28% 6 Missing ⚠️

❌ Your patch status has failed because the patch coverage (41.62%) is below the target coverage (98.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1217      +/-   ##
==========================================
+ Coverage   20.86%   20.96%   +0.10%     
==========================================
  Files         195      195              
  Lines       26217    26345     +128     
  Branches     3932     3947      +15     
==========================================
+ Hits         5469     5523      +54     
- Misses      20748    20822      +74     
Files with missing lines Coverage Δ
interface/RooParametricHist.h 35.71% <14.28%> (-14.29%) ⬇️
src/RooParametricHist.cxx 54.96% <80.59%> (+22.42%) ⬆️
src/CombineCodegenImpl.cxx 0.00% <0.00%> (ø)
interface/CombineMathFuncs.h 46.85% <29.72%> (-24.58%) ⬇️
Files with missing lines Coverage Δ
interface/RooParametricHist.h 35.71% <14.28%> (-14.29%) ⬇️
src/RooParametricHist.cxx 54.96% <80.59%> (+22.42%) ⬆️
src/CombineCodegenImpl.cxx 0.00% <0.00%> (ø)
interface/CombineMathFuncs.h 46.85% <29.72%> (-24.58%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@runtingt
Copy link
Copy Markdown
Contributor Author

runtingt commented Feb 18, 2026

@guitargeek that should be the bits from today minus the proxy problems

@runtingt runtingt force-pushed the RooParametricHist_AD branch from a24c6aa to 314d6ac Compare February 19, 2026 13:27
@runtingt runtingt force-pushed the RooParametricHist_AD branch from f25ddb4 to eb35ad1 Compare February 19, 2026 13:46
@runtingt runtingt marked this pull request as ready for review February 19, 2026 13:46
@runtingt
Copy link
Copy Markdown
Contributor Author

@anigamova could we get a testAD label on this PR please? Thanks!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
interface/CombineMathFuncs.h (1)

374-395: Near-duplicate of parametricHistMorphScale with different indexing.

parametricMorphFunction and parametricHistMorphScale compute the same morph scale but use different array indexing conventions. parametricHistMorphScale takes pre-offset pointers while this function indexes into the full flattened array. Consider unifying them to avoid divergent maintenance (e.g., the same division-by-zero risk on parVal exists here at Line 392).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/CombineMathFuncs.h` around lines 374 - 395, parametricMorphFunction
duplicates logic from parametricHistMorphScale (but uses different indexing) and
also repeats the division-by-parVal risk; refactor by extracting the common
morph-scaling loop into a single helper (e.g., computeMorphScale or similar)
that accepts either a base pointer+stride or an index-mapper so both
parametricMorphFunction and parametricHistMorphScale call the same
implementation, update parametricMorphFunction to pass the appropriate
offsets/stride instead of reimplementing the loop, and add a guard against
parVal == 0 (or handle small/zero parVal consistently with
parametricHistMorphScale) before performing (1./parVal) to eliminate the
division-by-zero risk; preserve existing parameters morphDiffs, morphSums,
nMorphs, hasMorphs when wiring to the new helper.
src/RooParametricHist.cxx (2)

214-230: Stale-cache risk: getFlattenedMorphs only flattens once, but addMorphs can mutate _diffs/_sums.

The !diffs_flat.empty() guard at Line 218 means subsequent addMorphs() calls won't be reflected. If the calling order is always setup-then-evaluate this is fine, but the cache has no invalidation mechanism. Consider clearing diffs_flat_/sums_flat_ in addMorphs() to avoid silent staleness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/RooParametricHist.cxx` around lines 214 - 230, getFlattenedMorphs
currently skips rebuilding the flattened cache when diffs_flat/sums_flat are
non-empty, but addMorphs can mutate _diffs/_sums causing a stale cache; update
addMorphs (the method that updates _diffs/_sums) to invalidate the flattened
caches by clearing any stored diffs_flat_/sums_flat (or setting a boolean dirty
flag) so that getFlattenedMorphs (which checks diffs_flat.empty()) will rebuild
after morphs are added; reference the methods/fields getFlattenedMorphs,
addMorphs, _diffs, _sums, and the cached diffs_flat_/sums_flat in your change.

205-212: Inconsistent cast: RooRealVar* here vs. RooAbsReal* in getParVals().

Line 209 uses static_cast<RooRealVar*> while the analogous getParVals() at Line 200 uses static_cast<RooAbsReal*>. Since only getVal() is needed (defined on RooAbsReal), prefer the wider type to avoid UB if a non-RooRealVar is ever added to _coeffList.

Suggested fix
-    coeffs_.push_back(static_cast<RooRealVar*>(_coeffList.at(i))->getVal());
+    coeffs_.push_back(static_cast<RooAbsReal*>(_coeffList.at(i))->getVal());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/RooParametricHist.cxx` around lines 205 - 212, The getCoeffs()
implementation casts list entries to RooRealVar* which is inconsistent with
getParVals() and can be UB if _coeffList contains non-RooRealVar objects; change
the cast in RooParametricHist::getCoeffs to static_cast<RooAbsReal*> (same type
used in getParVals()) and call getVal() on that RooAbsReal pointer when
iterating _coeffList to safely retrieve coefficient values from the list.
interface/RooParametricHist.h (1)

48-48: Add explanatory comment for public evaluate() override.

RooAbsPdf::evaluate() is conventionally protected in RooFit, but this implementation appears intentionally public given the supporting public accessors (getX(), getParVal(), getCoeffs(), etc.). A brief comment explaining this design choice would clarify intent for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/RooParametricHist.h` at line 48, The public override of Double_t
evaluate() in class RooParametricHist should have a short explanatory comment
clarifying why it is public (because this PDF exposes public accessors like
getX(), getParVal(), getCoeffs() and is intended to be evaluated directly by
external code) and noting that this deviates from the RooFit convention where
RooAbsPdf::evaluate() is protected; add that comment immediately above the
public evaluate() declaration in RooParametricHist to document the design intent
and avoid future confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/CombineMathFuncs.h`:
- Around line 325-341: Add a guard for parVal == 0 in both
parametricHistMorphScale and parametricMorphFunction to avoid division by zero:
in parametricHistMorphScale (function parametricHistMorphScale) check if parVal
== 0 at the start and return the default morphScale (1.0) immediately; in
parametricMorphFunction (function parametricMorphFunction) check if parVal == 0
and return an appropriate zero/default morph value (e.g., 0.0) immediately so no
(1.0/parVal) or other division by parVal occurs later in the function. Ensure
these checks come before any use of parVal in divisions.
- Around line 439-467: The ranged-integration branch is inconsistently
normalized: the "domain fully contained in this bin" path returns sum/fullSum
but the "upper domain boundary is in bin" path and the final fallthrough return
unnormalized sum. Make normalization consistent by computing fullSum via
parametricHistFullSum (same arguments used earlier) and dividing sum by fullSum
in the upper-bound early return (the branch with comment "Upper domain boundary
is in bin") and in the final return after the loop; keep use of parVals, N_bins,
morphCoeffs, nMorphs, morphDiffs, morphSums, smoothRegion and
parametricMorphFunction unchanged.

In `@src/CombineCodegenImpl.cxx`:
- Around line 246-271: The code currently appends a full statement (including
the trailing ";\n") to code.str() and passes that to ctx.addResult, which yields
a statement where an expression is expected; in
RooFit::Experimental::codegenImpl for RooParametricHist replace the pattern that
builds code (code and code.str()) with: generate a unique temporary variable
name, produce an assignment statement via ctx.addToCodeBody that assigns the
result of ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistEvaluate",
...) to that temp (so keep bin_i and the same argument list), then call
ctx.addResult(&arg, tempVarName) passing only the bare variable/expression (no
semicolon). Ensure you still add the bin-finding call to the body (bin_i usage)
and preserve morphs handling (diffs_flat/sums_flat) when forming the buildCall.

In `@src/RooParametricHist.cxx`:
- Around line 163-192: In RooParametricHist::evaluate(), the call to
RooFit::Detail::MathFuncs::parametricHistFindBin(bin_i...) can dereference bins
when it's empty, so move the guard that checks bins.empty() || widths.empty() to
before the parametricHistFindBin call (or assert/ensure bins and widths are
non-empty on construction), resize/prepare pars_vals_ after the empty check, and
only call parametricHistFindBin and subsequent logic when bins and widths are
valid; keep the existing logic for morphs/getFlattenedMorphs/getCoeffs unchanged
but invoked after the empty-vector guard.

---

Nitpick comments:
In `@interface/CombineMathFuncs.h`:
- Around line 374-395: parametricMorphFunction duplicates logic from
parametricHistMorphScale (but uses different indexing) and also repeats the
division-by-parVal risk; refactor by extracting the common morph-scaling loop
into a single helper (e.g., computeMorphScale or similar) that accepts either a
base pointer+stride or an index-mapper so both parametricMorphFunction and
parametricHistMorphScale call the same implementation, update
parametricMorphFunction to pass the appropriate offsets/stride instead of
reimplementing the loop, and add a guard against parVal == 0 (or handle
small/zero parVal consistently with parametricHistMorphScale) before performing
(1./parVal) to eliminate the division-by-zero risk; preserve existing parameters
morphDiffs, morphSums, nMorphs, hasMorphs when wiring to the new helper.

In `@interface/RooParametricHist.h`:
- Line 48: The public override of Double_t evaluate() in class RooParametricHist
should have a short explanatory comment clarifying why it is public (because
this PDF exposes public accessors like getX(), getParVal(), getCoeffs() and is
intended to be evaluated directly by external code) and noting that this
deviates from the RooFit convention where RooAbsPdf::evaluate() is protected;
add that comment immediately above the public evaluate() declaration in
RooParametricHist to document the design intent and avoid future confusion.

In `@src/RooParametricHist.cxx`:
- Around line 214-230: getFlattenedMorphs currently skips rebuilding the
flattened cache when diffs_flat/sums_flat are non-empty, but addMorphs can
mutate _diffs/_sums causing a stale cache; update addMorphs (the method that
updates _diffs/_sums) to invalidate the flattened caches by clearing any stored
diffs_flat_/sums_flat (or setting a boolean dirty flag) so that
getFlattenedMorphs (which checks diffs_flat.empty()) will rebuild after morphs
are added; reference the methods/fields getFlattenedMorphs, addMorphs, _diffs,
_sums, and the cached diffs_flat_/sums_flat in your change.
- Around line 205-212: The getCoeffs() implementation casts list entries to
RooRealVar* which is inconsistent with getParVals() and can be UB if _coeffList
contains non-RooRealVar objects; change the cast in RooParametricHist::getCoeffs
to static_cast<RooAbsReal*> (same type used in getParVals()) and call getVal()
on that RooAbsReal pointer when iterating _coeffList to safely retrieve
coefficient values from the list.

Comment on lines +325 to +341
inline Double_t parametricHistMorphScale(const double parVal,
const int nMorphs,
const double* morphCoeffs,
const double* morphDiffs,
const double* morphSums,
double smoothRegion) {
double morphScale = 1.0;
if (!morphDiffs || !morphSums)
return morphScale;
for (int i = 0; i < nMorphs; ++i) {
double coeff = morphCoeffs[i];
double a = 0.5 * coeff;
double b = smoothStepFunc(coeff, smoothRegion);
morphScale *= 1 + (1.0 / parVal) * a * (morphDiffs[i] + b * morphSums[i]);
}
return morphScale;
}
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

🧩 Analysis chain

🏁 Script executed:

# Search for morph-related functions and usage patterns
rg -n "parametricHistMorphScale|parametricHistEvaluate|parVal" interface/CombineMathFuncs.h -A 3 -B 1

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 3350


🏁 Script executed:

# Check if there are any guards for parVal == 0 in related code
rg -n "parVal.*==.*0|parVal.*!=" interface/CombineMathFuncs.h

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 311


🏁 Script executed:

# Look at the full parametricHistEvaluate function to understand context
sed -n '343,372p' interface/CombineMathFuncs.h

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 1386


Add guard for parVal == 0 in parametric morph functions.

The functions parametricHistMorphScale (line 338) and parametricMorphFunction (line 392) both divide by parVal without checking if it is zero. When parVal == 0, this produces inf, and subsequent calculations like (parVal * scale) / widths[bin_i] result in NaN. Add guards to both functions:

 inline Double_t parametricHistMorphScale(const double parVal,
                                          const int nMorphs,
                                          const double* morphCoeffs,
                                          const double* morphDiffs,
                                          const double* morphSums,
                                          double smoothRegion) {
   double morphScale = 1.0;
   if (!morphDiffs || !morphSums)
     return morphScale;
+  if (parVal == 0.0)
+    return morphScale;
   for (int i = 0; i < nMorphs; ++i) {

Apply the same guard to parametricMorphFunction (line 374-395) at line 390.

📝 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
inline Double_t parametricHistMorphScale(const double parVal,
const int nMorphs,
const double* morphCoeffs,
const double* morphDiffs,
const double* morphSums,
double smoothRegion) {
double morphScale = 1.0;
if (!morphDiffs || !morphSums)
return morphScale;
for (int i = 0; i < nMorphs; ++i) {
double coeff = morphCoeffs[i];
double a = 0.5 * coeff;
double b = smoothStepFunc(coeff, smoothRegion);
morphScale *= 1 + (1.0 / parVal) * a * (morphDiffs[i] + b * morphSums[i]);
}
return morphScale;
}
inline Double_t parametricHistMorphScale(const double parVal,
const int nMorphs,
const double* morphCoeffs,
const double* morphDiffs,
const double* morphSums,
double smoothRegion) {
double morphScale = 1.0;
if (!morphDiffs || !morphSums)
return morphScale;
if (parVal == 0.0)
return morphScale;
for (int i = 0; i < nMorphs; ++i) {
double coeff = morphCoeffs[i];
double a = 0.5 * coeff;
double b = smoothStepFunc(coeff, smoothRegion);
morphScale *= 1 + (1.0 / parVal) * a * (morphDiffs[i] + b * morphSums[i]);
}
return morphScale;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/CombineMathFuncs.h` around lines 325 - 341, Add a guard for parVal
== 0 in both parametricHistMorphScale and parametricMorphFunction to avoid
division by zero: in parametricHistMorphScale (function
parametricHistMorphScale) check if parVal == 0 at the start and return the
default morphScale (1.0) immediately; in parametricMorphFunction (function
parametricMorphFunction) check if parVal == 0 and return an appropriate
zero/default morph value (e.g., 0.0) immediately so no (1.0/parVal) or other
division by parVal occurs later in the function. Ensure these checks come before
any use of parVal in divisions.

Comment on lines +439 to +467
for (i = 1; i <= N_bins; i++) {
// Get maybe-morphed bin value
double binVal = parVals[i - 1] / widths[i - 1];
if (morphCoeffs != nullptr) {
binVal *= parametricMorphFunction(
i - 1, parVals[i - 1], true, nMorphs, morphCoeffs, morphDiffs, morphSums, smoothRegion);
}

if (bins[i - 1] >= xmin && bins[i] <= xmax) {
// Bin fully in integration domain
sum += (bins[i] - bins[i - 1]) * binVal;
} else if (bins[i - 1] < xmin && bins[i] > xmax) {
// Domain is fully contained in this bin
sum += (xmax - xmin) * binVal;
// Exit here, this is the last bin to be processed by construction
double fullSum = parametricHistFullSum(
parVals, N_bins, morphCoeffs != nullptr, nMorphs, morphCoeffs, morphDiffs, morphSums, smoothRegion);
return sum / fullSum;
} else if (bins[i - 1] < xmin && bins[i] <= xmax && bins[i] > xmin) {
// Lower domain boundary is in bin
sum += (bins[i] - xmin) * binVal;
} else if (bins[i - 1] >= xmin && bins[i] > xmax && bins[i - 1] < xmax) {
// Upper domain boundary is in bin
// Exit here, this is the last bin to be processed by construction
sum += (xmax - bins[i - 1]) * binVal;
return sum;
}
}
return sum;
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

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the full parametricHistIntegral function
find . -name "CombineMathFuncs.h" -o -name "CombineMathFuncs.cxx" | head -20

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 114


🏁 Script executed:

# Search for the parametricHistIntegral function to understand its full context
rg "parametricHistIntegral" --type=cpp -n -B 5 -A 60 | head -150

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 10752


🏁 Script executed:

# Search for analyticalIntegral and related implementations to check the original pattern
rg "analyticalIntegral|getFullSum" --type=cpp -n -B 3 -A 15 | head -200

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 12230


🏁 Script executed:

# Search for test cases that use parametricHistIntegral with ranges
rg -n "parametricHistIntegral|RooParametricHist" --type=cpp -B 2 -A 2 | grep -A 5 -B 5 "range\|integral"

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 5005


🏁 Script executed:

# Look for git history or comments explaining the normalization logic
rg -n "fullSum|normalize|parametricHistIntegral" --type=h -B 3 -A 3

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 7983


🏁 Script executed:

# Check if there are test files or expected behavior documentation
fd -e "test" -o -name "*Test*" -o -name "*test*" | head -20

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 314


🏁 Script executed:

# Look at the parametricHistFullSum function to understand what it computes
rg -n "parametricHistFullSum" --type=cpp -B 5 -A 15 | head -80

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 6053


Inconsistent normalization in ranged integral computation.

Line 456 normalizes the result by dividing by fullSum when the domain is fully contained within a single bin, but lines 464 and 467 return unnormalized values for all other ranged cases (including when the upper boundary falls in a bin and the general fallthrough). Both exit cases share identical comments ("this is the last bin to be processed by construction"), making this asymmetry appear unintentional. Either all ranged paths should normalize, or none should—clarify the intended behavior and make the implementation consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/CombineMathFuncs.h` around lines 439 - 467, The ranged-integration
branch is inconsistently normalized: the "domain fully contained in this bin"
path returns sum/fullSum but the "upper domain boundary is in bin" path and the
final fallthrough return unnormalized sum. Make normalization consistent by
computing fullSum via parametricHistFullSum (same arguments used earlier) and
dividing sum by fullSum in the upper-bound early return (the branch with comment
"Upper domain boundary is in bin") and in the final return after the loop; keep
use of parVals, N_bins, morphCoeffs, nMorphs, morphDiffs, morphSums,
smoothRegion and parametricMorphFunction unchanged.

Comment on lines +246 to +271
void RooFit::Experimental::codegenImpl(RooParametricHist& arg, CodegenContext& ctx) {
std::vector<double> diffs_flat;
std::vector<double> sums_flat;
if (arg.hasMorphs()) {
arg.getFlattenedMorphs(diffs_flat, sums_flat);
}

std::string xName = ctx.getResult(arg.observable());
std::stringstream bin_i;
bin_i << ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistFindBin", arg.getNBins(), arg.getBins(), xName);
std::stringstream code;
code << ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistEvaluate",
bin_i.str(),
arg.getPars(),
arg.getBins(),
arg.getNBins(),
arg.getCoeffList(),
static_cast<int>(arg.getCoeffList().size()),
arg.hasMorphs() ? diffs_flat : std::vector<double>{},
arg.hasMorphs() ? sums_flat : std::vector<double>{},
arg.getWidths(),
arg.getSmoothRegion()) +
";\n";
ctx.addToCodeBody(code.str());
ctx.addResult(&arg, code.str());
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "CombineCodegenImpl.cxx" -type f

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 112


🏁 Script executed:

# Check how other codegen implementations use addResult
ast-grep --pattern 'ctx.addResult($_, $$$)'

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 2238


🏁 Script executed:

# Also search for the specific file and examine the context around the mentioned lines
rg -n "codegenImpl.*RooParametricHist" --type cpp -A 30

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 3093


🏁 Script executed:

# Let's examine the FastVerticalInterpHistPdf2 implementation around lines 120-121
sed -n '100,140p' src/CombineCodegenImpl.cxx

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 1464


🏁 Script executed:

# Let's also check if there are other patterns where code is added to body and result separately
rg "ctx.addToCodeBody|ctx.addResult" src/CombineCodegenImpl.cxx -B 3 -A 1

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 1164


🏁 Script executed:

# Look at the exact sequence in the problematic function
sed -n '246,271p' src/CombineCodegenImpl.cxx

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 1222


addResult receives a statement (with ;\n) instead of an expression.

code.str() at Line 270 includes the trailing ";\n" appended at Line 268. Using a full statement as a codegen result will break the generated code when it's substituted into expression contexts. All other codegen implementations follow the correct pattern: FastVerticalInterpHistPdf2 (Line 121) passes just the variable name expression to addResult, while VerticalInterpPdf (Line 235–243) passes the bare buildCall result without the statement terminator.

Store the buildCall result in a temporary variable via addToCodeBody, then pass the variable name to addResult.

Proposed fix sketch
+  std::string callExpr = ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistEvaluate",
+                        bin_i.str(),
+                        arg.getPars(),
+                        arg.getBins(),
+                        arg.getNBins(),
+                        arg.getCoeffList(),
+                        static_cast<int>(arg.getCoeffList().size()),
+                        arg.hasMorphs() ? diffs_flat : std::vector<double>{},
+                        arg.hasMorphs() ? sums_flat : std::vector<double>{},
+                        arg.getWidths(),
+                        arg.getSmoothRegion());
+
+  std::string tmpVar = ctx.getTmpVarName();
   std::stringstream code;
-  code << ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistEvaluate",
-                        bin_i.str(),
-                        arg.getPars(),
-                        arg.getBins(),
-                        arg.getNBins(),
-                        arg.getCoeffList(),
-                        static_cast<int>(arg.getCoeffList().size()),
-                        arg.hasMorphs() ? diffs_flat : std::vector<double>{},
-                        arg.hasMorphs() ? sums_flat : std::vector<double>{},
-                        arg.getWidths(),
-                        arg.getSmoothRegion()) +
-              ";\n";
-  ctx.addToCodeBody(code.str());
-  ctx.addResult(&arg, code.str());
+  code << "double " << tmpVar << " = " << callExpr << ";\n";
+  ctx.addToCodeBody(code.str());
+  ctx.addResult(&arg, tmpVar);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CombineCodegenImpl.cxx` around lines 246 - 271, The code currently
appends a full statement (including the trailing ";\n") to code.str() and passes
that to ctx.addResult, which yields a statement where an expression is expected;
in RooFit::Experimental::codegenImpl for RooParametricHist replace the pattern
that builds code (code and code.str()) with: generate a unique temporary
variable name, produce an assignment statement via ctx.addToCodeBody that
assigns the result of
ctx.buildCall("RooFit::Detail::MathFuncs::parametricHistEvaluate", ...) to that
temp (so keep bin_i and the same argument list), then call ctx.addResult(&arg,
tempVarName) passing only the bare variable/expression (no semicolon). Ensure
you still add the bin-finding call to the body (bin_i usage) and preserve morphs
handling (diffs_flat/sums_flat) when forming the buildCall.

Comment on lines +163 to 192
Double_t RooParametricHist::evaluate() const {
// Find which bin we are in and lookup the parameter value
double xVal = getX();
int bin_i = RooFit::Detail::MathFuncs::parametricHistFindBin(N_bins, bins, xVal);
if (bin_i < 0)
return 0.0; // Out of range
pars_vals_.resize(N_bins);
pars_vals_[bin_i] = getParVal(bin_i);
if (bins.empty() || widths.empty())
return 0.0;
const double* bins_ptr = bins.data();
const double* widths_ptr = widths.data();
int nMorphs = _coeffList.getSize();
getFlattenedMorphs(diffs_flat_, sums_flat_);

if (_hasMorphs) {
getCoeffs();
}
size_t bin_i = std::distance(std::begin(bins), it) - 1;
RooAbsReal *retVar = (RooAbsReal*)pars.at(bin_i);

double ret = retVar->getVal();
ret /= widths[bin_i];
return ret;
return RooFit::Detail::MathFuncs::parametricHistEvaluate(bin_i,
pars_vals_.data(),
bins_ptr,
N_bins,
_hasMorphs ? coeffs_.data() : nullptr,
nMorphs,
_hasMorphs ? diffs_flat_.data() : nullptr,
_hasMorphs ? sums_flat_.data() : nullptr,
widths_ptr,
_smoothRegion);
}
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

Empty-vector guard at Lines 171–172 is too late — UB already occurred at Line 166.

parametricHistFindBin at Line 166 dereferences bins[0] and bins[N_bins]. If bins is empty, this is undefined behavior before the guard at Line 171 is reached. Move the empty check before the findBin call, or remove it if bins/widths are guaranteed non-empty after construction.

Proposed fix
 Double_t RooParametricHist::evaluate() const {
   // Find which bin we are in and lookup the parameter value
   double xVal = getX();
+  if (bins.empty() || widths.empty())
+    return 0.0;
   int bin_i = RooFit::Detail::MathFuncs::parametricHistFindBin(N_bins, bins, xVal);
   if (bin_i < 0)
     return 0.0;  // Out of range
   pars_vals_.resize(N_bins);
   pars_vals_[bin_i] = getParVal(bin_i);
-  if (bins.empty() || widths.empty())
-    return 0.0;
   const double* bins_ptr = bins.data();
📝 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
Double_t RooParametricHist::evaluate() const {
// Find which bin we are in and lookup the parameter value
double xVal = getX();
int bin_i = RooFit::Detail::MathFuncs::parametricHistFindBin(N_bins, bins, xVal);
if (bin_i < 0)
return 0.0; // Out of range
pars_vals_.resize(N_bins);
pars_vals_[bin_i] = getParVal(bin_i);
if (bins.empty() || widths.empty())
return 0.0;
const double* bins_ptr = bins.data();
const double* widths_ptr = widths.data();
int nMorphs = _coeffList.getSize();
getFlattenedMorphs(diffs_flat_, sums_flat_);
if (_hasMorphs) {
getCoeffs();
}
size_t bin_i = std::distance(std::begin(bins), it) - 1;
RooAbsReal *retVar = (RooAbsReal*)pars.at(bin_i);
double ret = retVar->getVal();
ret /= widths[bin_i];
return ret;
return RooFit::Detail::MathFuncs::parametricHistEvaluate(bin_i,
pars_vals_.data(),
bins_ptr,
N_bins,
_hasMorphs ? coeffs_.data() : nullptr,
nMorphs,
_hasMorphs ? diffs_flat_.data() : nullptr,
_hasMorphs ? sums_flat_.data() : nullptr,
widths_ptr,
_smoothRegion);
}
Double_t RooParametricHist::evaluate() const {
// Find which bin we are in and lookup the parameter value
double xVal = getX();
if (bins.empty() || widths.empty())
return 0.0;
int bin_i = RooFit::Detail::MathFuncs::parametricHistFindBin(N_bins, bins, xVal);
if (bin_i < 0)
return 0.0; // Out of range
pars_vals_.resize(N_bins);
pars_vals_[bin_i] = getParVal(bin_i);
const double* bins_ptr = bins.data();
const double* widths_ptr = widths.data();
int nMorphs = _coeffList.getSize();
getFlattenedMorphs(diffs_flat_, sums_flat_);
if (_hasMorphs) {
getCoeffs();
}
return RooFit::Detail::MathFuncs::parametricHistEvaluate(bin_i,
pars_vals_.data(),
bins_ptr,
N_bins,
_hasMorphs ? coeffs_.data() : nullptr,
nMorphs,
_hasMorphs ? diffs_flat_.data() : nullptr,
_hasMorphs ? sums_flat_.data() : nullptr,
widths_ptr,
_smoothRegion);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/RooParametricHist.cxx` around lines 163 - 192, In
RooParametricHist::evaluate(), the call to
RooFit::Detail::MathFuncs::parametricHistFindBin(bin_i...) can dereference bins
when it's empty, so move the guard that checks bins.empty() || widths.empty() to
before the parametricHistFindBin call (or assert/ensure bins and widths are
non-empty on construction), resize/prepare pars_vals_ after the empty check, and
only call parametricHistFindBin and subsequent logic when bins and widths are
valid; keep the existing logic for morphs/getFlattenedMorphs/getCoeffs unchanged
but invoked after the empty-vector guard.

Copy link
Copy Markdown
Collaborator

@guitargeek guitargeek left a comment

Choose a reason for hiding this comment

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

Great, thank you very much!

@guitargeek
Copy link
Copy Markdown
Collaborator

@runtingt @anigamova, this is ready to be merged right?

@runtingt
Copy link
Copy Markdown
Contributor Author

@runtingt @anigamova, this is ready to be merged right?

Happy from my side

@anigamova anigamova merged commit 81d3ffc into cms-analysis:main Feb 20, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants