From 7d6c3a0e42aac04fad7568c2e2276090af861f8f Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Fri, 3 Apr 2026 21:15:50 -0400 Subject: [PATCH 01/25] add python script for tests --- narf/include/tests.hpp | 2 ++ narf/tests.py | 3 +++ test/testshiftsmear.py | 11 +++++++++++ 3 files changed, 16 insertions(+) create mode 100644 narf/tests.py create mode 100644 test/testshiftsmear.py diff --git a/narf/include/tests.hpp b/narf/include/tests.hpp index 36b99d6..ed8ed04 100644 --- a/narf/include/tests.hpp +++ b/narf/include/tests.hpp @@ -1,5 +1,7 @@ #pragma once +#include "histutils.hpp" + namespace narf { ROOT::VecOps::RVec testshift() { boost::histogram::axis::regular a(100, 0., 1.); diff --git a/narf/tests.py b/narf/tests.py new file mode 100644 index 0000000..abd7731 --- /dev/null +++ b/narf/tests.py @@ -0,0 +1,3 @@ +import narf.clingutils + +narf.clingutils.Declare('#include "tests.hpp"') diff --git a/test/testshiftsmear.py b/test/testshiftsmear.py new file mode 100644 index 0000000..2ca1bfa --- /dev/null +++ b/test/testshiftsmear.py @@ -0,0 +1,11 @@ +import ROOT +import narf.tests + +res = ROOT.narf.testshift() +print(res) + +res = ROOT.narf.testshifteigen() +print(res) + +res = ROOT.narf.testshiftrw() +print(res) From e84d73a032753d4c42a80dcfb621ca121cf98933 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sat, 4 Apr 2026 04:11:07 -0400 Subject: [PATCH 02/25] Add SymMatrixAtomic --- narf/include/matrix_utils.hpp | 53 +++++++++++++++++++++++++++++++++++ narf/include/tests.hpp | 46 ++++++++++++++++++++++++++---- test/testsymmatrixatomic.py | 6 ++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 narf/include/matrix_utils.hpp create mode 100644 test/testsymmatrixatomic.py diff --git a/narf/include/matrix_utils.hpp b/narf/include/matrix_utils.hpp new file mode 100644 index 0000000..a63d2c2 --- /dev/null +++ b/narf/include/matrix_utils.hpp @@ -0,0 +1,53 @@ +#include +#include +#include + +class SymMatrixAtomic { +public: + SymMatrixAtomic() = default; + SymMatrixAtomic(std::size_t n) : n_(n), data_(n*(n+1)/2) {} + + double fetch_add(std::size_t iidx, std::size_t jidx, double val) { + std::atomic& ref = data_[packed_index(iidx, jidx)]; + return ref.fetch_add(val); + } + + void fill_row(std::size_t row, double *rowData) { + const std::size_t offset = packed_index(row, row); + std::fill(rowData, rowData + row, 0.); + std::copy(data_.begin() + offset, data_.begin() + offset + n_ - row, rowData + row); + } + +private: + +/** + * Converts (row, col) indices of a symmetric matrix into a linearized index + * for packed storage of unique elements (upper triangle, row-major). + * + * Storage layout (0-indexed, n=4 example): + * (0,0)(0,1)(0,2)(0,3) | (1,1)(1,2)(1,3) | (2,2)(2,3) | (3,3) + * 0 1 2 3 4 5 6 7 8 9 + * + * Total elements stored: n*(n+1)/2 + * + * @param row Row index (0-based) + * @param col Column index (0-based) + * @return Linearized index into packed storage array + */ + inline std::size_t packed_index(std::size_t row, std::size_t col) { + // Normalize to upper triangle: ensure row <= col + if (row > col) { + std::size_t tmp = row; + row = col; + col = tmp; + } + + // Number of elements in rows 0..(row-1): row*n - row*(row-1)/2 + // Plus offset within current row: (col - row) + return row * n_ - row * (row - 1) / 2 + (col - row); + } + + std::size_t n_; + std::vector > data_; + +}; diff --git a/narf/include/tests.hpp b/narf/include/tests.hpp index ed8ed04..801a48f 100644 --- a/narf/include/tests.hpp +++ b/narf/include/tests.hpp @@ -1,6 +1,7 @@ #pragma once #include "histutils.hpp" +#include "matrix_utils.hpp" namespace narf { ROOT::VecOps::RVec testshift() { @@ -37,18 +38,53 @@ namespace narf { void testshiftrw() { std::vector a{0, 1, 2}; std::vector b{3, 4, 5}; - + std::cout << a.front() << std::endl; std::cout << b.front() << std::endl; - + for (auto const &[ael, bel] : make_zip_view(a, b)) { ael = 0; bel = 1; } - + std::cout << a.front() << std::endl; std::cout << b.front() << std::endl; - - + + + } + + // Test SymMatrixAtomic: construction, fetch_add, fill_row, and index symmetry. + // Returns true if all checks pass. + bool testSymMatrixAtomic() { + const std::size_t n = 4; + SymMatrixAtomic mat(n); + + // Fill upper triangle: mat[i][j] = (i+1)*(j+1) for i <= j + for (std::size_t i = 0; i < n; ++i) { + for (std::size_t j = i; j < n; ++j) { + mat.fetch_add(i, j, double((i + 1) * (j + 1))); + } + } + + // Verify via fill_row: rowData[j] == (i+1)*(j+1) for j >= i, else 0 + std::vector rowData(n); + for (std::size_t i = 0; i < n; ++i) { + mat.fill_row(i, rowData.data()); + for (std::size_t j = 0; j < i; ++j) { + if (rowData[j] != 0.0) return false; + } + for (std::size_t j = i; j < n; ++j) { + if (rowData[j] != double((i + 1) * (j + 1))) return false; + } + } + + // Verify symmetry: fetch_add(i,j) and fetch_add(j,i) address the same element + SymMatrixAtomic mat2(n); + mat2.fetch_add(1, 3, 5.0); // upper triangle (1,3) + mat2.fetch_add(3, 1, 3.0); // lower triangle: should map to same element + mat2.fill_row(1, rowData.data()); + if (rowData[3] != 8.0) return false; + + return true; } } diff --git a/test/testsymmatrixatomic.py b/test/testsymmatrixatomic.py new file mode 100644 index 0000000..51d1068 --- /dev/null +++ b/test/testsymmatrixatomic.py @@ -0,0 +1,6 @@ +import ROOT +import narf.tests + +res = ROOT.narf.testSymMatrixAtomic() +print(f"testSymMatrixAtomic: {res}") +assert res, "testSymMatrixAtomic failed" From 1a85357f3187073a8a42be8652bf3b8304f3c904 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sat, 4 Apr 2026 14:11:34 -0400 Subject: [PATCH 03/25] minor improvement for SymMatrixAtomic and add initial version of SparseMatrixAtomic --- narf/include/matrix_utils.hpp | 97 ++++++++++++++++++++++++++++++++++- narf/matrix_utils.py | 3 ++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 narf/matrix_utils.py diff --git a/narf/include/matrix_utils.hpp b/narf/include/matrix_utils.hpp index a63d2c2..16793e5 100644 --- a/narf/include/matrix_utils.hpp +++ b/narf/include/matrix_utils.hpp @@ -1,6 +1,10 @@ +#pragma once + #include #include #include +#include "oneapi/tbb.h" + class SymMatrixAtomic { public: @@ -47,7 +51,98 @@ class SymMatrixAtomic { return row * n_ - row * (row - 1) / 2 + (col - row); } - std::size_t n_; + std::size_t n_{}; std::vector > data_; }; + +class SparseMatrixIndexValues { +public: + const std::vector &idxs0() const { return idxs0_; } + const std::vector &idxs1() const { return idxs1_; } + const std::vector &vals() const { return vals_; } + + void emplace_back(std::size_t idx0, std::size_t idx1, double val) { + idxs0_.emplace_back(idx0); + idxs1_.emplace_back(idx1); + vals_.emplace_back(val); + } + + void reserve(std::size_t i) { + idxs0_.reserve(i); + idxs1_.reserve(i); + vals_.reserve(i); + } + + std::size_t size() const { return vals_.size(); } + +private: + std::vector idxs0_; + std::vector idxs1_; + std::vector vals_; +}; + +class SparseMatrixAtomic { +public: + // *FIXME* this can lock on inserts and generally has poor performance for our workloads + // replace with some (custom) alternative + using map_type = tbb::concurrent_unordered_map>; + + SparseMatrixAtomic(std::size_t size0, std::size_t size1) : size0_(size0), size1_(size1), data_(size0*size1/40) {} + + std::atomic &operator() (std::size_t idx0, std::size_t idx1) { + const std::size_t i = globalidx(idx0, idx1); + auto res = data_.emplace(i, 0.); + auto &it = res.first; + return it->second; + } + + const std::atomic &operator() (std::size_t idx0, std::size_t idx1) const { + const std::size_t i = globalidx(idx0, idx1); + return data_.at(i); + } + + void fetch_add(std::size_t idx0, std::size_t idx1, double val) { + if (val != 0.) { + auto &elemval = operator()(idx0, idx1); + elemval.fetch_add(val); + } + } + + SparseMatrixIndexValues index_values() const { + SparseMatrixIndexValues res; + res.reserve(data_.size()); + + for (auto &elem : data_) { + auto is = idxs(elem.first); + res.emplace_back(is[0], is[1], elem.second); + } + + return res; + } + + void clear() { data_.clear(); } + + void reserve(std::size_t i) { data_.reserve(i); } + + std::size_t dense_size() const { return size0_*size1_; } + + map_type &data() { return data_; } + +private: + std::size_t globalidx(std::size_t idx0, std::size_t idx1) const { + return idx0*size0_ + idx1; + } + + std::array idxs(std::size_t globalidx) const { + const std::size_t idx0 = globalidx/size0_; + const std::size_t idx1 = globalidx % size0_; + + return std::array{idx0, idx1}; + } + + const std::size_t size0_; + const std::size_t size1_; + map_type data_; + +}; \ No newline at end of file diff --git a/narf/matrix_utils.py b/narf/matrix_utils.py new file mode 100644 index 0000000..47f620d --- /dev/null +++ b/narf/matrix_utils.py @@ -0,0 +1,3 @@ +import narf.clingutils + +narf.clingutils.Declare('#include "matrix_utils.hpp"') From a5d2c6dd77884159f0c9ed9ab47e127382bcc60f Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sat, 4 Apr 2026 23:59:08 -0400 Subject: [PATCH 04/25] fix deprecated storage_type access --- narf/histutils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/narf/histutils.py b/narf/histutils.py index f08514b..5655b26 100644 --- a/narf/histutils.py +++ b/narf/histutils.py @@ -114,7 +114,7 @@ def make_array_interface_view(boost_hist): underflow = [axis.traits.underflow for axis in boost_hist.axes] - acc_type = convert_storage_type(boost_hist._storage_type) + acc_type = convert_storage_type(boost_hist.storage_type) arrview = ROOT.narf.array_interface_view[acc_type, len(shape)](arr, shape, strides, underflow) return arrview @@ -132,9 +132,9 @@ def hist_to_pyroot_boost(hist_hist, tensor_rank = 0, force_atomic = False): scalar_type = ROOT.double dimensions = ROOT.Eigen.Sizes[tuple(tensor_sizes)] - if issubclass(hist_hist._storage_type, bh.storage.Double): + if issubclass(hist_hist.storage_type, bh.storage.Double): cppstoragetype = ROOT.narf.tensor_accumulator[scalar_type, dimensions] - elif issubclass(hist_hist._storage_type, bh.storage.Weight): + elif issubclass(hist_hist.storage_type, bh.storage.Weight): cppstoragetype = ROOT.narf.tensor_accumulator[ROOT.boost.histogram.accumulators.weighted_sum[scalar_type], dimensions] else: raise TypeError("Requested storage type is not supported with tensor weights currently") @@ -143,7 +143,7 @@ def hist_to_pyroot_boost(hist_hist, tensor_rank = 0, force_atomic = False): cppstoragetype = ROOT.narf.atomic_adaptor[cppstoragetype] else: python_axes = hist_hist.axes - cppstoragetype = convert_storage_type(hist_hist._storage_type, force_atomic = force_atomic) + cppstoragetype = convert_storage_type(hist_hist.storage_type, force_atomic = force_atomic) cppaxes = [ROOT.std.move(convert_axis(axis)) for axis in python_axes] From 6c27fcb3a746059f76a4c90c45a560ea0f3536ee Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 5 Apr 2026 00:00:06 -0400 Subject: [PATCH 05/25] fix constness --- narf/include/histutils.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/narf/include/histutils.hpp b/narf/include/histutils.hpp index ad4fc31..41917d9 100644 --- a/narf/include/histutils.hpp +++ b/narf/include/histutils.hpp @@ -626,7 +626,7 @@ namespace narf { public: QuantileHelper(hist_t &&resource) : base_t(std::forward(resource)) {} - boost::histogram::axis::index_type operator()(const boost::histogram::axis::traits::value_type&... args, const scalar_t &last) { + boost::histogram::axis::index_type operator()(const boost::histogram::axis::traits::value_type&... args, const scalar_t &last) const { auto const &hist = *base_t::resourceHist_; auto const &edges = narf::get_value(hist, args...).data(); @@ -652,7 +652,7 @@ namespace narf { QuantileHelperStatic(const edge_t &edges) : edges_(edges) {} - boost::histogram::axis::index_type operator() (double val) { + boost::histogram::axis::index_type operator() (double val) const { // find the quantile bin corresponding to the last argument auto const upper = std::upper_bound(edges_.begin(), edges_.end(), val); auto const iquant = std::distance(edges_.begin(), upper); From db0394fb1a70096feec46ac3d7f5620f69bc31a5 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 5 Apr 2026 00:00:27 -0400 Subject: [PATCH 06/25] make wrapper more flexible/robust --- narf/include/rdfutils.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/narf/include/rdfutils.hpp b/narf/include/rdfutils.hpp index eaddbbb..3ea29ef 100755 --- a/narf/include/rdfutils.hpp +++ b/narf/include/rdfutils.hpp @@ -8,14 +8,14 @@ namespace narf { private: Callable callable_; - using return_t = decltype(callable_(std::declval()...)); + using return_t = std::decay_t()...))>; public: DefineWrapper(const Callable &callable) : callable_(callable) {} DefineWrapper(Callable &&callable) : callable_(std::move(callable)) {} - return_t operator() (const Args&... args) const { + return_t operator() (const Args&... args) { return callable_(args...); } }; From 5a4c98d892bff02b0d3911f070c0d5fe19ad0ea6 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 5 Apr 2026 00:00:47 -0400 Subject: [PATCH 07/25] flexible column types for quantile helpers --- narf/histutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narf/histutils.py b/narf/histutils.py index 5655b26..c764a26 100644 --- a/narf/histutils.py +++ b/narf/histutils.py @@ -754,7 +754,7 @@ def define_quantile_ints(df, cols, quantile_hists): helper_cols = helper_cols_cond + [col] outname = f"{col}_iquant" - df = df.Define(outname, quanthelper, helper_cols) + df = narf.rdfutils.flexible_define(df, outname, quanthelper, helper_cols) helper_cols_cond.append(outname) quantile_axes = list(quantile_hists[-1].axes) From 7468541faa80e952ed75b85b63b7027008d4dc4d Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 5 Apr 2026 00:08:41 -0400 Subject: [PATCH 08/25] add missing include --- narf/include/utils.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/narf/include/utils.hpp b/narf/include/utils.hpp index e5eaf3b..7fa26ce 100755 --- a/narf/include/utils.hpp +++ b/narf/include/utils.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace narf { template From ff899e4a20d006911018d2639ecf4b8d33dd7ce5 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Mon, 6 Apr 2026 05:57:05 -0400 Subject: [PATCH 09/25] make range_to more flexible --- narf/include/utils.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/narf/include/utils.hpp b/narf/include/utils.hpp index 7fa26ce..fc99960 100755 --- a/narf/include/utils.hpp +++ b/narf/include/utils.hpp @@ -136,8 +136,8 @@ namespace narf { | std::views::transform([](auto const &it){ return *it; }); } - template