Skip to content

Float precision difference of variable axis bins causes 'axes not mergable' exception when summing histograms #1054

@dsavoiu

Description

@dsavoiu

Dear boost-histogram developers,

I came across an issue with merging a set of histograms with what I believed to be identically-binned Variable axes. However, attempting to sum the histograms resulted in an exception stating that the axes were not mergeable.

This was confusing at first, because checking the bin edges in Python I could find no apparent difference, but under closer inspection I found that, although these were numerically close, they were not identical in terms of the underlying floating-point representation. I checked this by casting the double value to uint64 and comparing bit by bit:

import ctypes
bin(ctypes.c_uint64.from_buffer(ctypes.c_double(bin_edge)).value)

For context, the input histograms were created on different machines as part of a distributed workflow on a batch system, and were supposed to be merged on a central machine for further analysis. The exact reason why the binning turned out slightly different for some inputs is unclear, but this could be because some of the jobs ran on machines with different OS/hardware (although the Python interpreter version was identical).

I understand that, strictly speaking, this could be considered intended behavior, since the bin edges are checked strictly while merging. However, I imagine that this kind of rounding errors could occur quite frequently when using heterogeneous compute resources as is common in HEP, so it would be good to at least have some sort of tolerance when comparing the Variable binnings.

Is this something you think could be implemented? Happy to hear your thoughts.

Since this issue likely comes from the upstream boost::histogram package, I intend to open an issue there are sell. However, since I encountered this in Python and a lot of HEP users use these bindings I'm opening this issue here first for better visibility in that community and to get your feedback before taking this up at boost-histogram.

System details and steps to reproduce

OS: Linux 5.14.0-570.46.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Sep 16 03:28:23 EDT 2025 x86_64 x86_64 x86_64 GNU/Linux
Python: 3.9.23 | packaged by conda-forge | (main, Jun 4 2025, 17:57:12) [GCC 13.3.0]

import boost_histogram as bh  # bh.__version__ == '1.6.1'
import numpy as np  # np.__version__ == '2.0.2'

# first histogram
h1 = bh.Histogram(
    bh.axis.Variable([1.0, 2.0, 3.0]),
)

# second histogram with second bin edge
# set to next-highest representable floating point value
h2 = bh.Histogram(
    bh.axis.Variable([1.0, np.nextafter(2.0, np.inf), 3.0]),
)

# fill in some values
h1.fill(1.5)
h2.fill(1.5)

# try to add histograms
h_sum = h1 + h2  # raises 'axes not mergable' exception

Traceback:

File /lib/python3.9/site-packages/boost_histogram/histogram.py:512, in Histogram.__add__(self, other)
    510 def __add__(self, other: Histogram | np.typing.NDArray[Any] | float) -> Self:
    511     result = self.copy(deep=False)
--> 512     return result.__iadd__(other)

File /lib/python3.9/site-packages/boost_histogram/histogram.py:517, in Histogram.__iadd__(self, other)
    515 if isinstance(other, (int, float)) and other == 0:
    516     return self
--> 517 self._compute_inplace_op("__iadd__", other)
    519 # Addition may change the axes if they can grow
    520 self.axes = self._generate_axes_()

File /lib/python3.9/site-packages/boost_histogram/histogram.py:570, in Histogram._compute_inplace_op(self, name, other)
    565 def _compute_inplace_op(
    566     self, name: str, other: Histogram | np.typing.NDArray[Any] | float
    567 ) -> Self:
    568     # Also takes CppHistogram, but that confuses mypy because it's hard to pick out
    569     if isinstance(other, Histogram):
--> 570         getattr(self._hist, name)(other._hist)
    571     elif isinstance(other, tuple(_histograms)):
    572         getattr(self._hist, name)(other)

ValueError: axes not mergable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions