Skip to content

pyBumpHunter review with Claude Opus 4.8 #35

Description

@henryiii

Do you want me to generate fixes for any or all of these?

🤖 AI text below 🤖

I now have a thorough picture. The test suite passes (5 tests, with matplotlib/uproot installed), and I've confirmed several bugs by running code. Here's the review.

Bugs

1. BumpHunter deprecated alias passes args to the wrong parameters — confirmed broken (__init__.py:80-101)
The super().__init__(...) call is positional and was written against an older signature. Since npe_inject and sideband_width were later inserted into BumpHunter1D.__init__, every argument from position 17 on is shifted. I verified: BumpHunter(seed=42) lands 42 in npe_inject, leaves seed=None, and sets use_sideband=None and sideband_width=<Nworker default> instead of their defaults. Fix: pass everything by keyword.

2. load_state overwrites rang with weights and never restores weights (bumphunter_1dim.py:806-809, same in bumphunter_2dim.py:919-921)

if "weights" in state:
    self.rang = state["weights"]   # should be self.weights
else:
    self.rang = None               # clobbers rang loaded just above

Confirmed: after a save/load round-trip, rang becomes the weights value and weights is never set from state.

3. load_state restores flip_sig into the wrong attribute (bumphunter_1dim.py:876-879, bumphunter_2dim.py:989-992)
save_state writes state["sig_flip"] = self.flip_sig, but load_state does self.sig_flip = state["sig_flip"] — setting a dead sig_flip attribute while the real flip_sig is never loaded. Also sideband_width is never saved or loaded at all, and load_state defaults width_min to 2 while the constructor default is 1.

4. Thread-pool exceptions are silently swallowed (bumphunter_1dim.py:1110-1149, 1297-1302, 1389-1396; 2D 1419, 1507)
exe.submit(...) results are never collected, so any exception raised inside _scan_hist in a worker thread is lost and the result arrays silently keep np.empty(...) garbage. Collect the futures and call .result() (or use exe.map) so failures surface. This matters more now that CI runs free-threaded 3.14 where these truly run in parallel.

5. _scan_hist crashes on an all-zero reference (bumphunter_1dim.py:363-364)
non0 = [i for i in range(hist.size) if ref[i] > 0] then min(non0) raises ValueError: min() arg is an empty sequence if a pseudo-experiment/reference has no positive bin. Worth a guard.

6. do_pseudo=False re-run hits an array-truth ambiguity (bumphunter_1dim.py:1082, 1094)
if self.min_Pval_ar == []: works right after reset() (it's a list), but after a prior scan min_Pval_ar is an ndarray and ndarray == [] does not give a clean bool. Use len(self.min_Pval_ar) == 0 or a sentinel.

7. 2D signal_inject appears to be dead/broken (bumphunter_2dim.py:~1360-1510)
CLAUDE.md states 2D doesn't support signal injection, yet a signal_inject exists that calls self._scan_hist with 1D [:, th] indexing — inconsistent with the 2D scan signature. It's untested. Either remove it or make it raise NotImplementedError explicitly.

8. util.deprecated_arg misuses its data structure (util.py:7,33)
warned_args = defaultdict(dict) but line 33 assigns warned_args[func] = oldarg (a string), and the guard oldarg not in warned_args[func] then does a substring check. It happens to work for one deprecated arg per function but breaks the moment a function deprecates two args. Use a set keyed by (func, oldarg).

Performance

9. The window-sum loop is the main hotspot — replace with prefix sums (bumphunter_1dim.py:411-412, the # FIXME ... Without loop ?? comment, and the 2D analog)

Nref = np.array([ref[p:p+w].sum() for p in pos], dtype=float)
Nhist = np.array([hist[p:p+w].sum() for p in pos])

This is O(npe × n_widths × n_positions × w). Precompute cumref = np.concatenate([[0], ref.cumsum()]) once per histogram, then each window sum is cumref[pos+w] - cumref[pos] — vectorized and O(1) per window. This is the single biggest speedup available and removes the per-window Python loop entirely.

10. np.tile(bkg_hist, (npe,1)).transpose() is wasteful (bumphunter_1dim.py:1062-1065 etc.)
np.random.poisson broadcasts; you can pass lam=bkg_hist[:, None] (or just rely on size) instead of materializing a tiled copy.

11. [i for i in range(ref.size) if ref[i] > 0] (bumphunter_1dim.py:363, 509, 1562, 1565) — replace with np.nonzero(ref)[0]; faster and clearer.

Simplifications

12. load_state is ~120 lines of repetitive if "x" in state blocks — collapse to a defaults dict + loop, e.g. self.x = state.get("x", default). This would also have prevented bugs #2/#3.

13. Legacy-arg handling is duplicated (bumphunter_1dim.py:287-292) — the @deprecated_arg decorators already exist, but the constructor also manually does if useSideBand is not None: .... The manual block is what actually does the work; the decorators only warn. Fine to keep, but it's worth a comment, or fold the remapping into one place.

14. Trailing bare return statements throughout (reset, bump_scan, plot methods) are noise.

Modernizations

15. Use numpy.random.Generator instead of the legacy global RNG (np.random.seed / np.random.poisson, many sites). rng = np.random.default_rng(self.seed) is reproducible, thread-safer, and the recommended API. signal_inject even calls np.random.seed(self.seed) twice (1239 and 1272).

16. Make matplotlib a lazy/optional import (bumphunter_1dim.py:7-9, 2D top). It's imported at module load, so the core algorithm can't be used without matplotlib installed (I hit ModuleNotFoundError on a bare env even though plotting wasn't used). Import it inside the plot methods and move it to an optional extra.

17. Typos in the public docstring/__init__.py — "BumpHenter", "BumpHnuter", "valid of BumpHunter2D" (should be "valid for"), "Ruturns", "durring", "possition". The __init__.py module docstring also advertises only the deprecated CamelCase API (BumpScan, GetTomography, PlinInject).

18. print(...) for progress — the algorithm prints unconditionally to stdout. A verbose flag or logging would be more library-friendly (out of scope for a quick fix, but worth noting).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions