Skip to content

Commit df4a6e2

Browse files
feat: add boilerplate sub implementation (#636)
* feat: add boilerplate sub implementation * feat: subtraction for Histograms * feat: use signed integers for integer storages * tests: add tests for subtraction * feat: support subtraction between WeightedSum views * docs: update CHANGELOG Co-authored-by: Henry Schreiner <[email protected]>
1 parent 802a87e commit df4a6e2

File tree

8 files changed

+112
-14
lines changed

8 files changed

+112
-14
lines changed

docs/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,24 @@
66

77
#### User changes
88
* Python 3.10 officially supported, with wheels.
9+
* Support subtraction on histograms [#636][]
910

1011
#### Bug fixes
1112
* Support custom setters on AxesTuple subclasses. [#627][]
1213
* Throw an error when an AxesTuple setter is the wrong length (inspired by zip strict in Python 3.10) [#627][]
1314
* Fix error thrown on comparison with axis and non-axis object [#631][]
15+
* Static typing no longer thinks `storage=` is required [#604][]
1416

17+
#### Developer changes
18+
* Support NumPy 1.21 for static type checking [#625][]
19+
* Use newer Boost 1.77 and Boost.Histogram 1.77+1 [#594][]
20+
21+
[#594]: https://github.com/scikit-hep/boost-histogram/pull/594
22+
[#604]: https://github.com/scikit-hep/boost-histogram/pull/604
23+
[#625]: https://github.com/scikit-hep/boost-histogram/pull/625
1524
[#627]: https://github.com/scikit-hep/boost-histogram/pull/627
1625
[#631]: https://github.com/scikit-hep/boost-histogram/pull/631
26+
[#636]: https://github.com/scikit-hep/boost-histogram/pull/636
1727

1828
### Version 1.1.0
1929

include/bh_python/register_histogram.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ auto register_histogram(py::module& m, const char* name, const char* desc) {
9797
def_optionally(hist,
9898
bh::detail::has_operator_rmul<histogram_t, histogram_t>{},
9999
py::self *= py::self);
100+
def_optionally(hist,
101+
bh::detail::has_operator_rsub<histogram_t, histogram_t>{},
102+
py::self -= py::self);
100103
#ifdef __clang__
101104
#pragma GCC diagnostic pop
102105
#endif

include/bh_python/storage.hpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
namespace storage {
2323

2424
// Names match Python names
25-
using int64 = bh::dense_storage<uint64_t>;
26-
using atomic_int64 = bh::dense_storage<bh::accumulators::count<uint64_t, true>>;
25+
using int64 = bh::dense_storage<int64_t>;
26+
using atomic_int64 = bh::dense_storage<bh::accumulators::count<int64_t, true>>;
2727
using double_ = bh::dense_storage<double>;
2828
using unlimited = bh::unlimited_storage<>;
2929
using weight = bh::dense_storage<accumulators::weighted_sum<double>>;
@@ -80,7 +80,7 @@ template <class Archive>
8080
void save(Archive& ar, const storage::atomic_int64& s, unsigned /* version */) {
8181
// We cannot view the memory as a numpy array, because the internal layout of
8282
// std::atomic is undefined. So no reinterpret_casts are allowed.
83-
py::array_t<std::uint64_t> a(static_cast<py::ssize_t>(s.size()));
83+
py::array_t<std::int64_t> a(static_cast<py::ssize_t>(s.size()));
8484

8585
auto in_ptr = s.begin();
8686
auto out_ptr = a.mutable_data();
@@ -191,15 +191,15 @@ struct type_caster<storage::atomic_int64::value_type> {
191191
auto ptr = PyNumber_Long(src.ptr());
192192
if(!ptr)
193193
return false;
194-
value = PyLong_AsUnsignedLongLong(ptr);
194+
value = PyLong_AsLongLong(ptr);
195195
Py_DECREF(ptr);
196196
return !PyErr_Occurred();
197197
}
198198

199199
static handle cast(storage::atomic_int64::value_type src,
200200
return_value_policy /* policy */,
201201
handle /* parent */) {
202-
return PyLong_FromUnsignedLongLong(src.value());
202+
return PyLong_FromLongLong(src.value());
203203
}
204204
};
205205
} // namespace detail

src/boost_histogram/_internal/hist.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,23 @@ def __radd__(
337337
) -> H:
338338
return self + other
339339

340+
def __sub__(
341+
self: H, other: Union["Histogram", "np.typing.NDArray[Any]", float]
342+
) -> H:
343+
result = self.copy(deep=False)
344+
return result.__isub__(other)
345+
346+
def __isub__(
347+
self: H, other: Union["Histogram", "np.typing.NDArray[Any]", float]
348+
) -> H:
349+
if isinstance(other, (int, float)) and other == 0:
350+
return self
351+
self._compute_inplace_op("__isub__", other)
352+
353+
self.axes = self._generate_axes_()
354+
355+
return self
356+
340357
# If these fail, the underlying object throws the correct error
341358
def __mul__(
342359
self: H, other: Union["Histogram", "np.typing.NDArray[Any]", float]

src/boost_histogram/_internal/view.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,14 @@ def __array_ufunc__(
130130

131131
# Addition of two views
132132
if input_0.dtype == input_1.dtype:
133-
if ufunc in {np.add}:
133+
if ufunc in {np.add, np.subtract}:
134134
ufunc(
135135
input_0["value"],
136136
input_1["value"],
137137
out=result["value"],
138138
**kwargs,
139139
)
140-
ufunc(
140+
np.add(
141141
input_0["variance"],
142142
input_1["variance"],
143143
out=result["variance"],

tests/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
import boost_histogram # noqa: F401
3+
import boost_histogram as bh
44

55

66
@pytest.fixture(params=(False, True), ids=("no_growth", "growth"))
@@ -29,3 +29,17 @@ def flow(request):
2929
)
3030
def metadata(request):
3131
return request.param
32+
33+
34+
@pytest.fixture(
35+
params=(
36+
bh.storage.Double,
37+
bh.storage.Int64,
38+
bh.storage.AtomicInt64,
39+
bh.storage.Weight,
40+
bh.storage.Unlimited,
41+
),
42+
ids=("Double", "Int64", "AtomicInt64", "Weight", "Unlimited"),
43+
)
44+
def count_storage(request):
45+
return request.param

tests/test_histogram.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,50 @@ def test_add_2d_w(flow):
403403
assert h[bh.tag.at(i), bh.tag.at(j)] == 2 * m[i][j]
404404

405405

406+
def test_sub_2d(flow, count_storage):
407+
408+
h0 = bh.Histogram(
409+
bh.axis.Integer(-1, 2, underflow=flow, overflow=flow),
410+
bh.axis.Regular(4, -2, 2, underflow=flow, overflow=flow),
411+
storage=count_storage(),
412+
)
413+
414+
h0.fill(-1, -2)
415+
h0.fill(-1, -1)
416+
h0.fill(0, 0)
417+
h0.fill(0, 1)
418+
h0.fill(1, 0)
419+
h0.fill(3, -1)
420+
h0.fill(0, -3)
421+
422+
m = h0.values(flow=True).copy()
423+
424+
if count_storage not in {bh.storage.AtomicInt64, bh.storage.Weight}:
425+
h = h0.copy()
426+
h -= h0
427+
assert h.values(flow=True) == approx(m * 0)
428+
429+
h -= h0
430+
assert h.values(flow=True) == approx(-m)
431+
432+
h2 = h0 - (h0 + h0 + h0)
433+
assert h2.values(flow=True) == approx(-2 * m)
434+
435+
h3 = h0 - h0.view(flow=True) * 4
436+
assert h3.values(flow=True) == approx(-3 * m)
437+
438+
h4 = h0.copy()
439+
h4 -= h0.view(flow=True) * 5
440+
assert h4.values(flow=True) == approx(-4 * m)
441+
442+
h5 = h0.copy()
443+
h5 -= 2
444+
assert h5.values(flow=True) == approx(m - 2)
445+
446+
h6 = h0 - 3
447+
assert h6.values(flow=True) == approx(m - 3)
448+
449+
406450
def test_repr():
407451
hrepr = """Histogram(
408452
Regular(3, 0, 1),

tests/test_views.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ def test_view_add(v):
6565
assert_allclose(v2.value, [2, 5, 4, 3])
6666
assert_allclose(v2.variance, [4, 7, 6, 5])
6767

68-
v += 2
69-
assert_allclose(v.value, [2, 5, 4, 3])
70-
assert_allclose(v.variance, [4, 7, 6, 5])
68+
v2 = v.copy()
69+
v2 += 2
70+
assert_allclose(v2.value, [2, 5, 4, 3])
71+
assert_allclose(v2.variance, [4, 7, 6, 5])
72+
73+
v2 = v + v
74+
assert_allclose(v2.value, v.value * 2)
75+
assert_allclose(v2.variance, v.variance * 2)
7176

7277

7378
def test_view_sub(v):
@@ -83,9 +88,14 @@ def test_view_sub(v):
8388
assert_allclose(v2.value, [1, -2, -1, 0])
8489
assert_allclose(v2.variance, [1, 4, 3, 2])
8590

86-
v -= 2
87-
assert_allclose(v.value, [-2, 1, 0, -1])
88-
assert_allclose(v.variance, [4, 7, 6, 5])
91+
v2 = v.copy()
92+
v2 -= 2
93+
assert_allclose(v2.value, [-2, 1, 0, -1])
94+
assert_allclose(v2.variance, [4, 7, 6, 5])
95+
96+
v2 = v - v
97+
assert_allclose(v2.value, [0, 0, 0, 0])
98+
assert_allclose(v2.variance, v.variance * 2)
8999

90100

91101
def test_view_unary(v):

0 commit comments

Comments
 (0)