Skip to content

Fix scalar-broadcast fill after a non-inclusive axis (scikit-hep/boost-histogram#960) - #431

Merged
HDembinski merged 6 commits into
boostorg:developfrom
NJManganelli:address_960
Oct 11, 2026
Merged

HDembinski merged 6 commits into
boostorg:developfrom
NJManganelli:address_960

Conversation

@NJManganelli

Copy link
Copy Markdown
Contributor

Problem

In the vectorized fill (detail/fill_n.hpp), a scalar argument is broadcast across an
axis by computing that axis' contribution to the linear index once and adding it to
every entry. The contribution was derived from the change of the first index
(*begin_). When a previous axis had already invalidated some entries — including the
first one — the broadcast axis then invalidated all entries and silently dropped
valid data.

This surfaced downstream as scikit-hep/boost-histogram#960
("Histogram with IntCategory and StrCategory axes can be zeroed by out-of-bounds fill
values"): a non-growing category axis combined with out-of-bounds entries and a
broadcast scalar on a second axis produced a sum of zero. One-at-a-time filling was
always correct; only the array/fill_n path was affected.

Fix

Compute the axis contribution on a fresh index and add it to all entries, leaving
already-invalid entries untouched. The growing and non-growing scalar paths now share
one code path through linearize / linearize_growth, so the "delta from *begin_"
heuristic is gone entirely.

Regression tests compare the vectorized fill against the one-at-a-time reference for
scalar-broadcast-after-non-inclusive-axis, including the growing-axis, zero-point-shift,
and underflow variants (static and dynamic histograms).

Note

The reported bug is in the Python bindings, but the defect is here in core fill_n;
consuming it there is just a submodule bump. Happy to open the companion PR against
scikit-hep/boost-histogram once this lands.

🤖 Root-caused and drafted with Claude Code; reviewed by @NJManganelli.

Nick Manganelli and others added 2 commits July 14, 2026 07:43
In the vectorized fill (fill_n), a scalar argument is broadcast across an
axis by computing that axis' contribution to the linear index once and
adding it to every entry. The contribution was derived from the change of
the first index (*begin_). When a previous axis had already invalidated
some entries, including the first one, the broadcast axis then invalidated
*all* entries, silently discarding valid data. This is
scikit-hep/boost-histogram#960: a non-growing axis combined with
out-of-bounds entries and a broadcast scalar fill produced a sum of zero.

Compute the axis contribution on a fresh index instead and add it to all
entries, leaving already-invalid entries untouched. The growing and
non-growing scalar paths share the same code via linearize/linearize_growth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compare the vectorized fill against the one-at-a-time reference for scalar
broadcast after a non-inclusive axis, covering the growing-axis,
zero-point-shift, and underflow variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NJManganelli

Copy link
Copy Markdown
Contributor Author

I asked Claude to match the comment style of the repo, I don't know that it really did (and it left a comment I asked it to remove regarding a now-gone commit that templated a true/false growable axis to no real gain). I'll be ready to have Claude update based on any adversarial review, human feedback, etc.

Comment thread include/boost/histogram/detail/fill_n.hpp
Comment thread include/boost/histogram/detail/fill_n.hpp Outdated
@HDembinski

Copy link
Copy Markdown
Collaborator

@NJManganelli Looks good, please resolve the comments, though. The AI tends to not clean up after itself, always afraid to remove things. However, all API inside the detail namespace is fair game, even if public.

@henryiii

henryiii commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Btw, Claude Code added a feature about three days ago /simplify, which runs four agents looking for four classes of simplifications (reuse, simplification, efficiency, altitude), and it works really well. Removing API and comments still are two of the hardest things for AI to do, though. :) If you edit your AGENTS.md (CLAUDE.md should be symlink) to state "the detail module is private, API changes are safe inside it" or something like that, it might be a bit better.

Another thing I like is the prompt: "You are an adversarial reviewer for the new feature in this branch. Do you see any problems? Anything that could be done or written more cleanly? Can you break it?".

Anyway, this fixes #426.

@HDembinski

Copy link
Copy Markdown
Collaborator

The ponytail skill is also good.

- Remove call_2, inlining the per-element linearize calls into the
  iterable branch of the visitor
- Rename call_1 to call_impl
- Explain why the double cast in the scalar-broadcast delta is
  intentional, not superfluous
- call_1 -> dispatch_on_value_or_iterable
- call_2 -> linearize_element (per-element linearize, pointer based)
- scalar_linearize -> linearize_scalar (scalar broadcast, index by reference)
- move operator() to the top and reorder the helpers
@HDembinski
HDembinski merged commit 78e7bfd into boostorg:develop Oct 11, 2026
9 checks passed
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.

3 participants