Skip to content

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

Open
NJManganelli wants to merge 2 commits into
boostorg:developfrom
NJManganelli:address_960
Open

Fix scalar-broadcast fill after a non-inclusive axis (scikit-hep/boost-histogram#960)#431
NJManganelli wants to merge 2 commits into
boostorg:developfrom
NJManganelli:address_960

Conversation

@NJManganelli

Copy link
Copy Markdown

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

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