Skip to content

Updates to Goodness of Fit#1248

Open
DennRoy wants to merge 6 commits into
cms-analysis:mainfrom
DennRoy:updates
Open

Updates to Goodness of Fit#1248
DennRoy wants to merge 6 commits into
cms-analysis:mainfrom
DennRoy:updates

Conversation

@DennRoy

@DennRoy DennRoy commented Apr 22, 2026

Copy link
Copy Markdown
Contributor
  • Updated plotting script to make nicer plots
  • Changes to the Goodness of Fit code:
    • datasetsList.reset() : To avoid random segmentation faults that may happen.
    • RooAbsPdf *cat_pdf = sim->getPdf(cat_data->GetName()) : The list of categories in datasets and binNames_ are not sorted identically, so here the CDFs of different categories may have been compared. Now, make sure the same category is taken from simulation as from data.
    • distance... : The change I made here isn't necessary, but it might be "healthier" like this to ensure we're not dividing by zero.

Summary by CodeRabbit

  • Bug Fixes

    • Improved goodness-of-fit calculations to handle edge cases and avoid invalid distance values.
  • Improvements

    • Refined plot visuals: adjusted arrow sizing, logo placement, and axis label offsets.
    • Consolidated and clarified plot text overlays and warnings; now explicitly shows underflow/overflow counts and appends observed-value formatting for out-of-range notices.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@DennRoy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 7 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6aa8d02-424b-4fe8-89ed-5f0218569961

📥 Commits

Reviewing files that changed from the base of the PR and between c5565e2 and cda4221.

📒 Files selected for processing (1)
  • scripts/plotGof.py
📝 Walkthrough

Walkthrough

Adds a DrawText(...) plotting helper to centralize overlay text (stat, p‑value, warnings, under/overflow) and updates arrow/label positioning; tweaks histogram axis offsets and CMS logo position. In GoodnessOfFit, frees transferred dataset list, selects PDFs by each dataset's name, and guards AD distance when CDF ≤0 or ≥1.

Changes

Cohort / File(s) Summary
Plotting & Visualization
scripts/plotGof.py
Added DrawText(stat, ntoys, pval, arrowrange, underflow, overflow) helper; extract underflow_count/overflow_count from histogram edge bins; replace inline TPaveText creation with helper output drawing; helper uses outer-scope obs and appends at {obs.GetX()[0]:.2f} to the observed-range warning; adjust ROOT.TArrow size/line width; enable histogram axis title/label offsets; move CMS logo Y from 0.045 to 0.095.
Goodness-of-Fit Logic & Cleanup
src/GoodnessOfFit.cc
In runKSandAD, explicitly clear datasetsList after constructing the owning datasets vector; when selecting per-category PDFs use each split dataset's GetName() (e.g., sim->getPdf(cat_data->GetName())) instead of indexing via binNames_; in EvaluateADDistance compute the AD term only when current_cdf_val is strictly between 0 and 1, otherwise set distance to 0..

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped in code to tidy text and arrows,
Counts gathered safe from under/overflow narrows,
Lists unhooked, each PDF named true,
AD checks keep math from splitting in two—
A rabbit’s nibble, now the plots all sparkle ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Updates to Goodness of Fit' is generic and vague, failing to convey the specific improvements made such as the plotting enhancements, category matching fix, or safety fixes. Consider a more descriptive title such as 'Fix category matching in GOF evaluation and improve plotting' or 'Add safety checks and category matching for Goodness of Fit'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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
Review rate limit: 0/1 reviews remaining, refill in 3 minutes and 7 seconds.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/GoodnessOfFit.cc (1)

295-304: ⚠️ Potential issue | 🟠 Major

Map qVals_ by category name, not split order.

Line 297 correctly uses the split dataset name for the PDF lookup, but Line 304 still writes to qVals_[i]. If datasets and binNames_ are ordered differently, the per-category branches created from binNames_ receive the wrong category’s statistic.

🐛 Proposed fix
-    for (unsigned i = 0; i < binNames_.size(); i++) {
-      RooAbsData *cat_data = datasets[i].get();
+    for (auto const &dataset : datasets) {
+      RooAbsData *cat_data = dataset.get();
+      auto bin_it = std::find(binNames_.begin(), binNames_.end(), cat_data->GetName());
+      if (bin_it == binNames_.end()) {
+        throw std::runtime_error(std::string("Error: missing category label for dataset ") + cat_data->GetName());
+      }
+      auto qval_index = std::distance(binNames_.begin(), bin_it);
       RooAbsPdf *cat_pdf = sim->getPdf(cat_data->GetName());
+      if (cat_pdf == nullptr) {
+        throw std::runtime_error(std::string("Error: missing PDF for category label ") + cat_data->GetName());
+      }
       std::unique_ptr<RooArgSet> observables(cat_pdf->getObservables(cat_data));
       if (observables->getSize() > 1) {
         std::cout << "Warning, KS and AD statistics are not well defined for "
                      "models with more than one observable\n";
       }
       RooRealVar *x = dynamic_cast<RooRealVar *>(observables->first());
-      qVals_[i] = EvaluateADDistance(*cat_pdf, *cat_data, *x, kolmo);
+      qVals_[qval_index] = EvaluateADDistance(*cat_pdf, *cat_data, *x, kolmo);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GoodnessOfFit.cc` around lines 295 - 304, qVals_ is being indexed by the
loop index i (qVals_[i]) which assumes binNames_ and datasets share the same
order; instead, store the per-category statistic keyed by the dataset/PDF
category name so branches get the correct value. Change the assignment so it
uses the category identifier from the split dataset (use cat_data->GetName() or
the PDF name obtained via sim->getPdf(cat_data->GetName())) as the key into
qVals_ (e.g., qVals_[categoryName] = EvaluateADDistance(...)) rather than
qVals_[i]; ensure the rest of the code that reads qVals_ expects the same key
type (string) and adjust any consumers accordingly (symbols: qVals_, binNames_,
datasets, cat_data->GetName(), sim->getPdf, EvaluateADDistance).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/plotGof.py`:
- Around line 46-85: DrawWarning currently uses overlapping NDC rectangles and
duplicates logic, uses an implicit module variable obs, mixes Draw() with
returned objects (risking PyROOT GC), and contains unreachable code; fix it by
consolidating the underflow/overflow string construction into a single block
(use warningstrings once), add a new function parameter obs_x (or obs) and use
that instead of the free variable obs.GetX()[0], move warningtext2 to a
different NDC rectangle so it doesn't overlap warningtext1, choose one
ownership/drawing convention (either call ROOT.SetOwnership(warningtext1, False)
and warningtext2 after creation and call Draw() here, or do not call Draw() and
instead return both objects e.g. [warningtext1, warningtext2] so the caller
holds references), remove the final unreachable "return None", and keep the
symbols warningtext1, warningtext2, arrowrange, underflow, overflow, DrawWarning
and ROOT.SetOwnership in mind when making the edits.

In `@src/GoodnessOfFit.cc`:
- Around line 405-410: Run clang-format on the shown hunk in GoodnessOfFit.cc to
fix spacing/indentation: reformat the assignment and if/else block around
bin_prob, current_cdf_val, last_cdf_val, distance, s_data and empirical_df so it
matches project style (consistent spaces around operators, braces on same
column, and proper indentation). Ensure the ternary logic remains unchanged
(distance = 0 when current_cdf_val >= 1.0 || current_cdf_val <= 0.0, otherwise
compute distance = s_data * pow((empirical_df - current_cdf_val), 2) /
current_cdf_val / (1. - current_cdf_val) * bin_prob) and commit the formatted
result.

---

Outside diff comments:
In `@src/GoodnessOfFit.cc`:
- Around line 295-304: qVals_ is being indexed by the loop index i (qVals_[i])
which assumes binNames_ and datasets share the same order; instead, store the
per-category statistic keyed by the dataset/PDF category name so branches get
the correct value. Change the assignment so it uses the category identifier from
the split dataset (use cat_data->GetName() or the PDF name obtained via
sim->getPdf(cat_data->GetName())) as the key into qVals_ (e.g.,
qVals_[categoryName] = EvaluateADDistance(...)) rather than qVals_[i]; ensure
the rest of the code that reads qVals_ expects the same key type (string) and
adjust any consumers accordingly (symbols: qVals_, binNames_, datasets,
cat_data->GetName(), sim->getPdf, EvaluateADDistance).
🪄 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: 3803ce15-6446-4b6e-b9fe-7e844e2eeebb

📥 Commits

Reviewing files that changed from the base of the PR and between 08a3828 and 6bec4b1.

📒 Files selected for processing (2)
  • scripts/plotGof.py
  • src/GoodnessOfFit.cc

Comment thread scripts/plotGof.py Outdated
Comment on lines +46 to +85
def DrawWarning(arrowrange, underflow, overflow):
warningtext1 = ROOT.TPaveText(0.48, 0.73, 0.60, 0.77, "NDC")
warningtext1.SetBorderSize(0)
warningtext1.SetFillStyle(0)
warningtext1.SetTextAlign(22)
warningtext1.SetTextSize(0.04)
warningtext1.SetTextColor(2)
warningtext1.SetTextFont(62)

if arrowrange and ((underflow != 0) or (overflow != 0)):
warningstrings = []
if underflow != 0:
warningstrings.append("%d underflow" % underflow)
if overflow != 0:
warningstrings.append("%d overflow" % overflow)
warningtext1.AddText(", ".join(warningstrings))
warningtext1.Draw()

warningtext2 = ROOT.TPaveText(0.48, 0.73, 0.60, 0.77, "NDC")
warningtext2.SetBorderSize(0)
warningtext2.SetFillStyle(0)
warningtext2.SetTextAlign(22)
warningtext2.SetTextSize(0.04)
warningtext2.SetTextColor(2)
warningtext2.SetTextFont(62)
warningtext2.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
return warningtext2
else:
if (underflow != 0) or (overflow != 0):
warningstrings = []
if underflow != 0:
warningstrings.append("%d underflow" % underflow)
if overflow != 0:
warningstrings.append("%d overflow" % overflow)
warningtext1.AddText(", ".join(warningstrings))
elif arrowrange:
warningtext1.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
return warningtext1
return None

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 examine the actual DrawWarning function and surrounding context
head -100 scripts/plotGof.py | tail -60

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 2128


🏁 Script executed:

# Also check the call sites of DrawWarning
rg -n "DrawWarning" scripts/plotGof.py -A 2 -B 2

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 829


🏁 Script executed:

# Look for where 'obs' is defined in the file
rg -n "^obs\s*=" scripts/plotGof.py

Repository: cms-analysis/HiggsAnalysis-CombinedLimit

Length of output: 66


🌐 Web query:

PyROOT TPaveText ownership Python garbage collection lifetime

💡 Result:

In PyROOT, TPaveText objects created in Python are owned by Python by default. When the Python reference count reaches zero, Python's garbage collector destroys the proxy, and since Python owns the underlying C++ TPaveText object, it calls the C++ destructor to delete it. Key points: - Use ROOT.SetOwnership(pavetext, False) after creation to transfer ownership to ROOT/C++ side (e.g., if drawn on a canvas/pad, which takes ownership via its primitive list). - TPaveText created via pt = ROOT.TPaveText(...) owns the TText objects returned by pt.AddText(...); they are stored in pt.fLines (TList) and deleted when pt is deleted. - If TPaveText is drawn on a TPad/TCanvas (pt.Draw), ROOT adds it to the pad's list of primitives. If the kCanDelete bit is set (default for many drawn objects), the pad owns it and deletes it on Clear or pad destruction. Otherwise, Python deletion occurs when the variable goes out of scope. - To check ownership: ROOT.GetOwnership(obj) (though not explicitly found, implied by SetOwnership usage). - Common pitfall: Python loop variables can overwrite references, triggering premature GC (e.g., for x in list: pass where x held a ROOT object). - For long-lived display: SetOwnership(pt, False) after Draw to let canvas manage lifetime, preventing double-delete or premature GC. Example safe usage: import ROOT pt = ROOT.TPaveText(0.1,0.1,0.5,0.5) pt.AddText("Hello") c = ROOT.TCanvas pt.Draw ROOT.SetOwnership(pt, False) # Canvas now owns it # pt Python ref can be discarded safely

Citations:


DrawWarning has several issues worth addressing.

  1. Overlapping NDC coordinates (visible bug). warningtext1 (line 47) and warningtext2 (line 64) use the exact same NDC rectangle (0.48, 0.73, 0.60, 0.77). When both the out-of-range arrow and non-zero under/overflow conditions occur simultaneously, the two labels render on top of each other and become unreadable. Move one of them to a distinct NDC region.

  2. PyROOT garbage collection risk. In the if arrowrange and ((underflow != 0) or (overflow != 0)) branch, warningtext1.Draw() is called inside the function, but the Python reference is then discarded when the function returns. By default, PyROOT owns the object; when the Python reference goes out of scope, the garbage collector can delete the underlying C++ object even though it was added to the pad's primitive list. This causes the drawn label to vanish from the canvas. Either return both objects (as a list) so the caller keeps references alive, or call ROOT.SetOwnership(warningtext1, False) immediately after creation to transfer ownership to the canvas. Additionally, adopt a consistent convention: don't mix calling Draw() inside the function with returning objects for the caller to draw.

  3. obs is an implicit free variable. Lines 71 and 82 reference obs.GetX()[0], but obs is not a parameter—it is picked up from module scope. Pass the observed x-value (or obs itself) as an explicit parameter to make the function self-contained and robust against future refactors.

  4. Unreachable code. Line 84 (return None) is dead code; both the if and else branches above already return.

  5. Duplicated string-building logic. The underflow/overflow string construction is repeated identically at lines 56–61 and 74–80. Compute it once before branching.

♻️ Proposed refactor addressing 1–5
-def DrawWarning(arrowrange, underflow, overflow):
-    warningtext1 = ROOT.TPaveText(0.48, 0.73, 0.60, 0.77, "NDC")
-    warningtext1.SetBorderSize(0)
-    warningtext1.SetFillStyle(0)
-    warningtext1.SetTextAlign(22)
-    warningtext1.SetTextSize(0.04)
-    warningtext1.SetTextColor(2)
-    warningtext1.SetTextFont(62)
-
-    if arrowrange and ((underflow != 0) or (overflow != 0)):
-        warningstrings = []
-        if underflow != 0:
-            warningstrings.append("%d underflow" % underflow)
-        if overflow != 0:
-            warningstrings.append("%d overflow" % overflow)
-        warningtext1.AddText(", ".join(warningstrings))
-        warningtext1.Draw()
-
-        warningtext2 = ROOT.TPaveText(0.48, 0.73, 0.60, 0.77, "NDC")
-        warningtext2.SetBorderSize(0)
-        warningtext2.SetFillStyle(0)
-        warningtext2.SetTextAlign(22)
-        warningtext2.SetTextSize(0.04)
-        warningtext2.SetTextColor(2)
-        warningtext2.SetTextFont(62)
-        warningtext2.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
-        return warningtext2
-    else:
-        if (underflow != 0) or (overflow != 0):
-            warningstrings = []
-            if underflow != 0:
-                warningstrings.append("%d underflow" % underflow)
-            if overflow != 0:
-                warningstrings.append("%d overflow" % overflow)
-            warningtext1.AddText(", ".join(warningstrings))
-        elif arrowrange:
-            warningtext1.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
-        return warningtext1
-    return None
+def _makePave(y1, y2):
+    pave = ROOT.TPaveText(0.48, y1, 0.60, y2, "NDC")
+    pave.SetBorderSize(0)
+    pave.SetFillStyle(0)
+    pave.SetTextAlign(22)
+    pave.SetTextSize(0.04)
+    pave.SetTextColor(2)
+    pave.SetTextFont(62)
+    ROOT.SetOwnership(pave, False)
+    return pave
+
+
+def DrawWarning(arrowrange, underflow, overflow, obs_x=None):
+    paves = []
+    flow_parts = []
+    if underflow != 0:
+        flow_parts.append("%d underflow" % underflow)
+    if overflow != 0:
+        flow_parts.append("%d overflow" % overflow)
+
+    if flow_parts:
+        p = _makePave(0.73, 0.77)
+        p.AddText(", ".join(flow_parts))
+        paves.append(p)
+    if arrowrange and obs_x is not None:
+        # offset in y so it does not overlap the flow warning
+        y1, y2 = (0.68, 0.72) if flow_parts else (0.73, 0.77)
+        p = _makePave(y1, y2)
+        p.AddText(f"observed value not in range, at {obs_x:.2f}")
+        paves.append(p)
+    return paves

Call sites become:

-        warningtext = DrawWarning(arrow_not_in_range, underflow_count, overflow_count)
-        if warningtext:
-            warningtext.Draw()
+        for w in DrawWarning(arrow_not_in_range, underflow_count, overflow_count, obs.GetX()[0]):
+            w.Draw()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/plotGof.py` around lines 46 - 85, DrawWarning currently uses
overlapping NDC rectangles and duplicates logic, uses an implicit module
variable obs, mixes Draw() with returned objects (risking PyROOT GC), and
contains unreachable code; fix it by consolidating the underflow/overflow string
construction into a single block (use warningstrings once), add a new function
parameter obs_x (or obs) and use that instead of the free variable
obs.GetX()[0], move warningtext2 to a different NDC rectangle so it doesn't
overlap warningtext1, choose one ownership/drawing convention (either call
ROOT.SetOwnership(warningtext1, False) and warningtext2 after creation and call
Draw() here, or do not call Draw() and instead return both objects e.g.
[warningtext1, warningtext2] so the caller holds references), remove the final
unreachable "return None", and keep the symbols warningtext1, warningtext2,
arrowrange, underflow, overflow, DrawWarning and ROOT.SetOwnership in mind when
making the edits.

Comment thread src/GoodnessOfFit.cc
@codecov

codecov Bot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.89%. Comparing base (08a3828) to head (cda4221).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/GoodnessOfFit.cc 0.00% 4 Missing ⚠️

❌ Your patch check has failed because the patch coverage (0.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    #1248      +/-   ##
==========================================
- Coverage   20.89%   20.89%   -0.01%     
==========================================
  Files         195      195              
  Lines       26310    26311       +1     
  Branches     3947     3947              
==========================================
  Hits         5498     5498              
- Misses      20812    20813       +1     
Files with missing lines Coverage Δ
src/GoodnessOfFit.cc 3.57% <0.00%> (-0.02%) ⬇️
Files with missing lines Coverage Δ
src/GoodnessOfFit.cc 3.57% <0.00%> (-0.02%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@maxgalli

Copy link
Copy Markdown
Collaborator

Hi, thanks for the contribution! could you please fix this failing test? https://github.com/cms-analysis/HiggsAnalysis-CombinedLimit/actions/runs/24781067248/job/72512036911?pr=1248
and possibly also run the clang-formatter to fix the other one

@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.

♻️ Duplicate comments (2)
scripts/plotGof.py (2)

46-106: ⚠️ Potential issue | 🟠 Major

Make DrawText self-contained by passing observed x explicitly.

DrawText still reads obs from outer scope (Line 93, Line 104). This hidden dependency is brittle and can break with refactors or reordered calls. Pass obs_x into the function and use it in both call sites.

♻️ Proposed fix
-def DrawText(stat, ntoys, pval, arrowrange, underflow, overflow):
+def DrawText(stat, ntoys, pval, arrowrange, underflow, overflow, obs_x=None):
@@
-        warningtext2.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
+        warningtext2.AddText(f"observed value not in range, at {obs_x:.2f}")
@@
-        elif arrowrange:
-            warningtext1.AddText(f"observed value not in range, at {obs.GetX()[0]:.2f}")
+        elif arrowrange and obs_x is not None:
+            warningtext1.AddText(f"observed value not in range, at {obs_x:.2f}")
@@
-        texts = DrawText(args.statistic, toy_graph.GetN(), pValue, arrow_not_in_range, underflow_count, overflow_count)
+        texts = DrawText(args.statistic, toy_graph.GetN(), pValue, arrow_not_in_range, underflow_count, overflow_count, obs.GetX()[0])
@@
-    texts = DrawText(args.statistic, toy_graph.GetN(), pValue, arrow_not_in_range, underflow_count, overflow_count)
+    texts = DrawText(args.statistic, toy_graph.GetN(), pValue, arrow_not_in_range, underflow_count, overflow_count, obs.GetX()[0])

Also applies to: 231-233, 300-302

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

In `@scripts/plotGof.py` around lines 46 - 106, DrawText currently reads obs from
outer scope (uses obs.GetX()[0]) making it brittle; change the signature of
DrawText to accept an explicit obs_x (float) parameter and replace both
occurrences of obs.GetX()[0] with that obs_x (formatted as before), then update
every call site that invokes DrawText to pass the observed x value (e.g.,
obs.GetX()[0]) into the new parameter so DrawText becomes self-contained.

230-230: ⚠️ Potential issue | 🟠 Major

Use the real histogram min edge for out-of-range detection.

Line 230 uses GetBinLowEdge(0) for the lower bound, which is the underflow bin edge (xmin - binWidth), not xmin. This can miss under-range observed values just below the plotted range. Compare against GetBinLowEdge(1) (or GetXaxis().GetXmin()) instead.

♻️ Proposed fix
-        arrow_not_in_range = (obs.GetX()[0] > toy_hist.GetBinLowEdge(args.bins + 1)) or (obs.GetX()[0] < toy_hist.GetBinLowEdge(0))
+        arrow_not_in_range = (obs.GetX()[0] > toy_hist.GetBinLowEdge(args.bins + 1)) or (obs.GetX()[0] < toy_hist.GetBinLowEdge(1))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/plotGof.py` at line 230, The out-of-range check uses the underflow
bin edge via toy_hist.GetBinLowEdge(0); change it to compare obs.GetX()[0]
against the real histogram minimum (e.g., toy_hist.GetBinLowEdge(1) or
toy_hist.GetXaxis().GetXmin()) so arrow_not_in_range correctly flags values
below the plotted range; update the expression that sets arrow_not_in_range
(which references obs.GetX()[0] and toy_hist.GetBinLowEdge(...)) to use the
correct lower bound.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/plotGof.py`:
- Around line 46-106: DrawText currently reads obs from outer scope (uses
obs.GetX()[0]) making it brittle; change the signature of DrawText to accept an
explicit obs_x (float) parameter and replace both occurrences of obs.GetX()[0]
with that obs_x (formatted as before), then update every call site that invokes
DrawText to pass the observed x value (e.g., obs.GetX()[0]) into the new
parameter so DrawText becomes self-contained.
- Line 230: The out-of-range check uses the underflow bin edge via
toy_hist.GetBinLowEdge(0); change it to compare obs.GetX()[0] against the real
histogram minimum (e.g., toy_hist.GetBinLowEdge(1) or
toy_hist.GetXaxis().GetXmin()) so arrow_not_in_range correctly flags values
below the plotted range; update the expression that sets arrow_not_in_range
(which references obs.GetX()[0] and toy_hist.GetBinLowEdge(...)) to use the
correct lower bound.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8335d28c-db53-4246-b0b9-23ddf4ee18ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3c83a1f and c5565e2.

📒 Files selected for processing (2)
  • scripts/plotGof.py
  • src/GoodnessOfFit.cc

@maxgalli maxgalli requested review from anigamova and maxgalli May 5, 2026 12:22
@maxgalli

maxgalli commented May 5, 2026

Copy link
Copy Markdown
Collaborator

IMO it looks safe enough to merge and, in the worst case scenario, correct in case something weird comes up or is spotted by other users. But would like to wait for a second check from @anigamova.

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.

2 participants