Skip to content

Fix vertical morphing pdfs not tracking nuisances - #1267

Open
mroguljic wants to merge 2 commits into
cms-analysis:mainfrom
mroguljic:morph_fix
Open

mroguljic wants to merge 2 commits into
cms-analysis:mainfrom
mroguljic:morph_fix

Conversation

@mroguljic

@mroguljic mroguljic commented Sep 3, 2026 •

Copy link
Copy Markdown

Copying a workspace and then changing a copy's parameters doesn't always work for FastVerticalInterpHistPdf and FastVerticalInterpHistPdf2. Depending on the class and how the copy was made, _morphParams and/or _sentry can
still refer to the original workspace's objects, so the copy stops responding to its own parameters. The fix adds redirectServersHook to both base classes to re-point these on redirect, plus one related fix in FastVerticalInterpHistPdf2Base::initBase() (it appended fresh coefficient pointers without clearing stale ones first).

Tested by creating two workspaces that use FastVerticalInterpHistPdf and FastVerticalInterpHistPdf2

import ROOT

ROOT.gROOT.SetBatch(True)
ROOT.gSystem.Load("libHiggsAnalysisCombinedLimit")
ROOT.RooMsgService.instance().setGlobalKillBelow(ROOT.RooFit.ERROR)

x = ROOT.RooRealVar("x", "x", 0.0, 4.0)
x.setBins(4)
theta = ROOT.RooRealVar("theta", "theta", 0.0, -5.0, 5.0)


TEMPLATES = (("nom", 0.0), ("up", 0.6), ("dn", -0.2))


def th1(tag, ramp):
    h = ROOT.TH1F("h_" + tag, "", 4, 0.0, 4.0)
    for i in range(1, 5):
        h.SetBinContent(i, 1.0 + ramp * i)
    return h


hists = [th1(tag, ramp) for tag, ramp in TEMPLATES]

# --- v1  ---------------

keep = [] # RooArgList stores bare pointers and RooHistPdf does not clone its RooDataHist, so these must stay alive in python until w.import() copies them.
pdfs = ROOT.RooArgList()
for h, (tag, _) in zip(hists, TEMPLATES):
    dh = ROOT.RooDataHist("dh_" + tag, "", ROOT.RooArgList(x), h)
    p = ROOT.RooHistPdf("p_" + tag, "", ROOT.RooArgSet(x), dh)
    keep.extend([dh, p])
    pdfs.add(p)
morph_v1 = ROOT.FastVerticalInterpHistPdf("morph_v1", "", x, pdfs,
                                          ROOT.RooArgList(theta), 1.0, 1)

# --- v2  ------------------------------
tlist = ROOT.TList()
for h in hists:
    tlist.Add(h)
morph_v2 = ROOT.FastVerticalInterpHistPdf2("morph_v2", "", x, tlist,
                                           ROOT.RooArgList(theta), 1.0, 1)

w = ROOT.RooWorkspace("w")
getattr(w, "import")(morph_v1)
getattr(w, "import")(morph_v2)
w.writeToFile("demo_ws.root")

print("wrote demo_ws.root")
for name in ("morph_v1", "morph_v2"):
    print("   %-9s -> %s" % (name, w.pdf(name).ClassName()))

We then read the workspace, copy it, change the value of theta, and print the value of the first bin.

Combine v11.0.0

pdf value in bin 1, original workspace held at theta = 0

  class                        copy         -1.0        0.0        1.0        2.0
  ------------------------------------------------------------------------------
  FastVerticalInterpHistPdf    correct       0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf2   correct       0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf    cold         0.25       0.25       0.25       0.25
  FastVerticalInterpHistPdf2   cold         0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf    warm         0.25       0.25       0.25       0.25
  FastVerticalInterpHistPdf2   warm         0.25       0.25       0.25       0.25

With PR-proposed fix:

pdf value in bin 1, original workspace held at theta = 0

  class                        copy         -1.0        0.0        1.0        2.0
  ------------------------------------------------------------------------------
  FastVerticalInterpHistPdf    correct       0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf2   correct       0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf    cold         0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf2   cold         0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf    warm         0.40       0.25       0.16       0.07
  FastVerticalInterpHistPdf2   warm         0.40       0.25       0.16       0.07

The evaluation script is pasted below:

import os
import sys
import ROOT

ROOT.gROOT.SetBatch(True)
ROOT.gSystem.Load("libHiggsAnalysisCombinedLimit")
ROOT.RooMsgService.instance().setGlobalKillBelow(ROOT.RooFit.ERROR)

PDFS = ("morph_v1", "morph_v2")
THETAS = (-1.0, 0.0, 1.0, 2.0)


def value(ws, pdf, t):
    # pdf value in bin 1, after setting workspace's theta = t
    ws.var("theta").setVal(t)
    ws.var("x").setVal(0.5)    # 0.5 is the centre of bin 1; x in [0,4] with 4 bins
    return ws.pdf(pdf).getVal()


def reference():
    # what the morphing should give measured on non-copied workspace
    f = ROOT.TFile.Open("demo_ws.root")
    w = f.Get("w")
    out = {p: [value(w, p, t) for t in THETAS] for p in PDFS}
    f.Close()
    return out


def copied(warm):
    # scan the copied workspace's  theta, with the original workspace's theta left at 0
    # warm parameter runs evaluation of original workspace before copying
    f = ROOT.TFile.Open("demo_ws.root")
    original = f.Get("w")
    if warm:
        for p in PDFS:  # evaluate before copying
            value(original, p, 0.0)
    copy = ROOT.RooWorkspace(original)
    original.var("theta").setVal(0.0)
    for p in PDFS: # pin the copy at theta = 0 too
        value(copy, p, 0.0)
    out = {p: [value(copy, p, t) for t in THETAS] for p in PDFS}
    f.Close()
    return out


ref = reference()
classes = {}
f = ROOT.TFile.Open("demo_ws.root")
for p in PDFS:
    classes[p] = f.Get("w").pdf(p).ClassName()
f.Close()

print("combine build : %s" % os.environ.get("CMSSW_BASE", "?"))
print()
print("pdf value in bin 1, original workspace held at theta = 0")
print()
hdr = "  ".join("%9.1f" % t for t in THETAS)
print("  %-28s %-6s  %s" % ("class", "copy", hdr))
print("  " + "-" * (28 + 8 + len(hdr)))
for p in PDFS:
    print("  %-28s %-6s  %s" % (classes[p], "correct",
                                "  ".join("%9.2f" % v for v in ref[p])))
for warm in (False, True):
    got = copied(warm)
    for p in PDFS:
        print("  %-28s %-6s  %s"
              % (classes[p], "warm" if warm else "cold",
                 "  ".join("%9.2f" % v for v in got[p])))

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when histogram-based probability models are copied or their parameters are redirected.
    • Ensured models continue responding correctly to updated coefficient values after workspace operations.
    • Improved parameter synchronization and recalculation following server changes.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 18d6bc3f-dfab-4851-8144-9a619b0104b7

📥 Commits

Reviewing files that changed from the base of the PR and between 3a5c65d and f7cc333.

📒 Files selected for processing (1)
  • src/VerticalInterpHistPdf.cc
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/VerticalInterpHistPdf.cc

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The vertical interpolation PDF base classes now handle RooFit server redirection. They refresh coefficient references, rebuild sentry dependencies, mark state as dirty, and reset initialization where required.

Changes

RooFit server redirection

Layer / File(s) Summary
Coefficient state refresh
interface/VerticalInterpHistPdf.h, src/VerticalInterpHistPdf.cc
Both PDF base classes declare and implement redirectServersHook. The hooks update _morphParams, rebuild _sentry.deps(), mark the sentry dirty, and delegate to RooAbsPdf. FastVerticalInterpHistPdfBase also resets _init. initBase() clears _morphParams before repopulation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f7cc3

Copied workspaces now refresh interpolation-PDF parameter dependencies during server redirection, preserving nuisance-parameter responses in cold and warm copies. No current merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing vertical morphing PDFs so they continue to track nuisance parameters after workspace copying.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.89%. Comparing base (66f59f9) to head (f7cc333).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
src/VerticalInterpHistPdf.cc 50.00% 10 Missing ⚠️

❌ Your patch check has failed because the patch coverage (50.00%) 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    #1267      +/-   ##
==========================================
- Coverage   20.90%   20.89%   -0.01%     
==========================================
  Files         195      195              
  Lines       26316    26317       +1     
  Branches     3947     3945       -2     
==========================================
- Hits         5502     5500       -2     
- Misses      20814    20817       +3     
Files with missing lines Coverage Δ
interface/VerticalInterpHistPdf.h 29.23% <ø> (ø)
src/VerticalInterpHistPdf.cc 23.52% <50.00%> (+0.74%) ⬆️

... and 2 files with indirect coverage changes

Files with missing lines Coverage Δ
interface/VerticalInterpHistPdf.h 29.23% <ø> (ø)
src/VerticalInterpHistPdf.cc 23.52% <50.00%> (+0.74%) ⬆️

... and 2 files with indirect coverage changes

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

@mroguljic

Copy link
Copy Markdown
Author

I am not sure if the two failures are related to the PR. Please let me know if I should have a deeper investigation and if some changes would be needed (code coverage?).

This branch has not been deployed

No deployments
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