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).
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/uprootinstalled), and I've confirmed several bugs by running code. Here's the review.Bugs
1.
BumpHunterdeprecated 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. Sincenpe_injectandsideband_widthwere later inserted intoBumpHunter1D.__init__, every argument from position 17 on is shifted. I verified:BumpHunter(seed=42)lands42innpe_inject, leavesseed=None, and setsuse_sideband=Noneandsideband_width=<Nworker default>instead of their defaults. Fix: pass everything by keyword.2.
load_stateoverwritesrangwithweightsand never restoresweights(bumphunter_1dim.py:806-809, same inbumphunter_2dim.py:919-921)Confirmed: after a save/load round-trip,
rangbecomes the weights value andweightsis never set from state.3.
load_staterestoresflip_siginto the wrong attribute (bumphunter_1dim.py:876-879,bumphunter_2dim.py:989-992)save_statewritesstate["sig_flip"] = self.flip_sig, butload_statedoesself.sig_flip = state["sig_flip"]— setting a deadsig_flipattribute while the realflip_sigis never loaded. Alsosideband_widthis never saved or loaded at all, andload_statedefaultswidth_minto2while the constructor default is1.4. Thread-pool exceptions are silently swallowed (
bumphunter_1dim.py:1110-1149,1297-1302,1389-1396; 2D1419,1507)exe.submit(...)results are never collected, so any exception raised inside_scan_histin a worker thread is lost and the result arrays silently keepnp.empty(...)garbage. Collect the futures and call.result()(or useexe.map) so failures surface. This matters more now that CI runs free-threaded 3.14 where these truly run in parallel.5.
_scan_histcrashes on an all-zero reference (bumphunter_1dim.py:363-364)non0 = [i for i in range(hist.size) if ref[i] > 0]thenmin(non0)raisesValueError: min() arg is an empty sequenceif a pseudo-experiment/reference has no positive bin. Worth a guard.6.
do_pseudo=Falsere-run hits an array-truth ambiguity (bumphunter_1dim.py:1082,1094)if self.min_Pval_ar == []:works right afterreset()(it's a list), but after a prior scanmin_Pval_aris an ndarray andndarray == []does not give a clean bool. Uselen(self.min_Pval_ar) == 0or a sentinel.7. 2D
signal_injectappears to be dead/broken (bumphunter_2dim.py:~1360-1510)CLAUDE.md states 2D doesn't support signal injection, yet a
signal_injectexists that callsself._scan_histwith 1D[:, th]indexing — inconsistent with the 2D scan signature. It's untested. Either remove it or make it raiseNotImplementedErrorexplicitly.8.
util.deprecated_argmisuses its data structure (util.py:7,33)warned_args = defaultdict(dict)but line 33 assignswarned_args[func] = oldarg(a string), and the guardoldarg 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 asetkeyed 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)This is O(npe × n_widths × n_positions × w). Precompute
cumref = np.concatenate([[0], ref.cumsum()])once per histogram, then each window sum iscumref[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-1065etc.)np.random.poissonbroadcasts; you can passlam=bkg_hist[:, None](or just rely onsize) 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 withnp.nonzero(ref)[0]; faster and clearer.Simplifications
12.
load_stateis ~120 lines of repetitiveif "x" in stateblocks — 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_argdecorators already exist, but the constructor also manually doesif 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
returnstatements throughout (reset,bump_scan, plot methods) are noise.Modernizations
15. Use
numpy.random.Generatorinstead 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_injecteven callsnp.random.seed(self.seed)twice (1239and1272).16. Make
matplotliba 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 hitModuleNotFoundErroron 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__.pymodule docstring also advertises only the deprecated CamelCase API (BumpScan,GetTomography,PlinInject).18.
print(...)for progress — the algorithm prints unconditionally to stdout. Averboseflag orloggingwould be more library-friendly (out of scope for a quick fix, but worth noting).