Skip to content

[Python][UHI] Fix bugs and inconsistencies with the UHI specification for TH1* - #24081

Open
siliataider wants to merge 17 commits into
root-project:masterfrom
siliataider:uhi
Open

siliataider wants to merge 17 commits into
root-project:masterfrom
siliataider:uhi

Conversation

@siliataider

@siliataider siliataider commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

This Pull request:

Changes:

Fixes bugs and inconsistencies with the UHI specification in the UHI pythonizations of TH1 and derived classes, to keep them consistent with the upcoming UHI support for the new ROOT histograms.

Each commit fixes one bug and adds a test that fails without it. Each commit message shows a small before/after example.

@siliataider siliataider self-assigned this Nov 10, 2026
@siliataider siliataider changed the title [Python][UHI] Address bugs in the sum action implementation [Python][UHI] Fix bugs and inconsistencies with the UHI specification Nov 10, 2026
@siliataider
siliataider marked this pull request as draft November 10, 2026 13:12
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Test Results

    22 files      22 suites   3d 12h 22m 46s ⏱️
 3 883 tests  3 881 ✅ 0 💤 2 ❌
75 998 runs  75 996 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit 6f41876.

♻️ This comment has been updated with latest results.

@siliataider siliataider changed the title [Python][UHI] Fix bugs and inconsistencies with the UHI specification [Python][UHI] Fix bugs and inconsistencies with the UHI specification for TH1* Nov 11, 2026
The UHI sum action is the builtin `sum` but only `ROOT.uhi.sum` was normalized to `slice(None, None, sum)` in `_compute_uhi_index`.

h2 = TH2D with 2x2 bins [[1, 2], [3, 4]]

                  Before                                     After
h2[sum, 0:1]      TypeError: Unsupported index type: float   [4]
h2[::sum, 0:1]    [4]                                        [4]

Assisted-by: Claude Opus 5.5
The integral bounds were read from the `stop` of the raw user index, which is not a slice for `h[sum]`, tuple or dict indices.

h = TH1D with 3 bins [1, 2, 3]

                                  Before                                        After
h[::sum]                          6                                             6
h[sum]                            AttributeError: no attribute 'stop'           6
h[(slice(None, None, sum),)]      AttributeError: no attribute 'stop'           6
h[{0: slice(None, None, sum)}]    AttributeError: no attribute 'stop'           6

Assisted-by: Claude Opus 5.5
…d 3D histograms

The sum over all axes of 2D and 3D histograms called `Integral()` without bounds, which excludes the flow bins.

h = TH2D with 3x3 bins, filled at (1.5, 1.5), (-1, 1.5) in the x underflow and (1.5, 5) in the y overflow

                            Before   After
h[::sum, ::sum]             1        3
h[0:len:sum, 0:len:sum]     1        1
h[1, ::sum]                 1        2

Assisted-by: Claude Opus 5.5
… the sum action

`ProjectionX` reuses and refills the histogram with the same name (`h_px`) found in `gDirectory`, so each projection overwrote the previous one.

h = TH2D with 4x2 bins [[1, 2], [3, 4], [5, 6], [7, 8]], px = h[:, ::sum], then px_part = h[1:3, ::sum]

                          Before          After
px                        [3 7 11 15]     [3 7 11 15]
px_part                   [7 11]          [7 11]
px after px_part          [7 11]          [3 7 11 15]

Assisted-by: Claude Opus 5.5
…or their base class

Once `Slice<TH1D>` exists, cppyy uses it for a `TProfile` too, and copying a `TProfile` into a `TH1D` is fatal.

h = TH1D, p = TProfile

                        Before                                                       After
p[1:3]                  TProfile                                                     TProfile
h[1:3], then p[1:3]     Fatal in <TProfile::Copy>: Cannot copy a TProfile in a TH1D  TProfile

Assisted-by: Claude Opus 5.5
…histograms

Each cut bin should land in the flow bin of the axis it was cut from. Before, they all piled into two corner cells.
Below the result of h[1:2, :]

        original                   slicing BEFORE         slicing AFTER
   of | 1 | 1 1 1 | 1            of |  0 | 0 | 11        of | 2 | 1 | 2
   3  | 1 | 1 1 1 | 1            3  |  0 | 1 |  0        3  | 2 | 1 | 2
   2  | 1 | 1 1 1 | 1            2  |  0 | 1 |  0        2  | 2 | 1 | 2
   1  | 1 | 1 1 1 | 1            1  |  0 | 1 |  0        1  | 2 | 1 | 2
   uf | 1 | 1 1 1 | 1            uf | 11 | 0 |  0        uf | 2 | 1 | 2
       uf | 1 2 3 | of               uf | 1 | of            uf | 1 | of

Assisted-by: Claude Opus 5.5
The Sumw2 array was not sliced, `SetBins` only truncated it to the size of the slice, which keeps the first bins.

h = TH1D with 4 bins and Sumw2, one entry of weight 1, 2, 3, 4 in each bin

                        Before       After
h.variances()           [1 4 9 16]   [1 4 9 16]
h[1:3].variances()      [1 4]        [4 9]

Assisted-by: Claude Opus 5.5
Only -1 was mapped to the last bin, other negative indices were passed to ROOT as negative bin numbers.

h = TH1D with 5 bins [10, 11, 12, 13, 14]

          Before   After
h[-1]     14       14
h[-2]     0        13
h[-5]     0        10
h[-6]     0        IndexError

Assisted-by: Claude Opus 5.5
`loc` changed `self` and returned it, so every `loc` call registers offsets:
Expression	   Before	                After
h[x]	           2	                        2
h[x + 1]	   3	                        3
h[x] again	   3, and x.offset is now 1	2

Assisted-by: Claude Opus 5.5
`FindBin` grows the axis if the histogram is extendable, `FindFixBin` is a simple lookup.

                                Before	                             After
Histogram before reading	[0, 10], [0 1 2 3 4 5 6 7 8 9]	     same
h[loc(100)]	                0.0	                             0.0 (the overflow bin)
Histogram after reading	        [0, 160], [45 0 0 0 0 0 0 0 0 0]     [0, 10], [0 1 2 3 4 5 6 7 8 9]

Assisted-by: Claude Opus 5.5
The counts were computed as values^2 / Sumw2, but the Sumw2 array of a profile holds the sum of w*y^2 of each bin.

p = TProfile with 2 bins, filled with y = (1, 3) in bin 0 and y = (2, 2, 5) in bin 1

               Before          After
p.values()     [2 3]           [2 3]
p.counts()     [0.4 0.27]      [2 3]

Assisted-by: Claude Opus 5.5
…files

The variances were the Sumw2 array, which holds the sum of w*y^2 of each bin for a profile.

p = TProfile with 2 bins, filled with y = (1, 3) in bin 0 and y = (2, 2, 5) in bin 1

                  Before      After
p.values()        [2 3]       [2 3]
p.variances()     [10 33]     [1 1]

Assisted-by: Claude Opus 5.5
…ontents

The bin entries of profiles were not sliced, so the sliced bin contents were divided by the entries of the first bins.

p = TProfile with 4 bins, filled with y = 1 in bin 0, y = (2, 4) in bin 1, y = 6 in bin 2 and y = 8 in bin 3

                       Before     After
p.values()             [1 3 6 8]  [1 3 6 8]
p[1:3].values()        [6 3]      [3 6]
p[1:3].counts()        [1 2]      [2 1]

Assisted-by: Claude Opus 5.5
`lower` and `upper` were the edges of the range selected with `SetRange`, while `bins` was the number of bins of the whole axis.

h = TH1D with 10 bins in [0, 10], h.GetXaxis().SetRange(3, 5)

             Before                        After
axes[0]      lower 2, upper 5, bins 10     lower 0, upper 10, bins 10

Assisted-by: Claude Opus 5.5
Axes with variable bin sizes were serialized as regular axes between their first and last edges.

h = TH1D with 3 bins of edges [0, 1, 5, 10]

             Before                                After
axes[0]      regular, lower 0, upper 10, bins 3    variable, edges [0, 1, 5, 10]

Assisted-by: Claude Opus 5.5
The values were serialized with the flow bins, but the variances without.

h = TH1D with 2 bins and Sumw2, filled with weight 2 in the underflow and weight 3 in bin 0

                         Before        After
storage["values"]        [2 3 0 0]     [2 3 0 0]
storage["variances"]     [9 0]         [4 9 0 0]

Assisted-by: Claude Opus 5.5
A profile always has a Sumw2 array (the sum of w*y^2), so every profile was serialized as a `weighted_mean` storage.

p = TProfile with 2 bins, filled with y = (1, 3) in bin 0 and y = (2, 2, 5) in bin 1

                           Before                        After
type                       weighted_mean                 mean
variances                  [nan 1 1 nan]                 [0 2 3 0]
counts                     -                             [0 2 3 0]
sum_of_weights             [2 3] (the means)             -
sum_of_weights_squared     [0 10 33 0] (sum of y^2)      -

Assisted-by: Claude Opus 5.5
@siliataider
siliataider marked this pull request as ready for review November 11, 2026 17:13

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant