Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
ndarray.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <atomic>
6#include <cassert>
7#include <cstddef>
8#include <cstdint>
9#include <cstdlib>
10#include <cstring>
11#include <initializer_list>
12#include <memory>
13#include <new>
14#include <span>
15#include <sstream>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
21
22namespace clustering {
23namespace detail {
24
31struct BorrowedTag {};
32
44inline std::atomic<std::uint64_t> &alignedAllocCallCount() noexcept {
45 static std::atomic<std::uint64_t> counter{0};
46 return counter;
47}
48
55template <class T, std::size_t Align> class AlignedAllocator : public std::allocator<T> {
56public:
57 using value_type = T;
58 using size_type = std::size_t;
59 using pointer = T *;
60 using const_pointer = const T *;
61
62 // is_always_equal + POCMA/POCCA/POCS = true_type lets std::vector pointer-steal on move and
63 // perform a fresh allocation on copy-assign without per-instance allocator state checks.
64 using is_always_equal = std::true_type;
65 using propagate_on_container_move_assignment = std::true_type;
66 using propagate_on_container_copy_assignment = std::true_type;
67 using propagate_on_container_swap = std::true_type;
68
69 template <typename U> struct rebind {
70 using other = AlignedAllocator<U, Align>;
71 };
72
73 AlignedAllocator() noexcept = default;
74
75 template <typename U> AlignedAllocator(const AlignedAllocator<U, Align> & /*unused*/) noexcept {}
76
77 pointer allocate(size_type n) {
78 // std::aligned_alloc(Align, 0) is implementation-defined; bypass it explicitly.
79 if (n == 0) {
80 return nullptr;
81 }
82 const size_type bytes = n * sizeof(T);
83 const size_type aligned_bytes = (bytes + Align - 1) / Align * Align;
84 alignedAllocCallCount().fetch_add(1, std::memory_order_relaxed);
85 if (auto *ptr = static_cast<pointer>(std::aligned_alloc(Align, aligned_bytes))) {
86 return ptr;
87 }
88 throw std::bad_alloc();
89 }
90
91 void deallocate(pointer ptr, size_type /*n*/) noexcept { std::free(ptr); }
92};
93
94template <class T, class U, std::size_t Align>
95bool operator==(const AlignedAllocator<T, Align> &, const AlignedAllocator<U, Align> &) noexcept {
96 return true;
97}
98
99template <class T, class U, std::size_t Align>
100bool operator!=(const AlignedAllocator<T, Align> &, const AlignedAllocator<U, Align> &) noexcept {
101 return false;
102}
103
104} // namespace detail
105
109enum class NDArrayStorage : std::uint8_t { Owned, Borrowed };
110
122enum class Layout : std::uint8_t { Contig, MaybeStrided };
123
136template <class T, std::size_t N, Layout L = Layout::Contig> class NDArray {
137 static_assert(N >= 1, "NDArray rank must be >= 1");
138 // Widened from {float, double} so integer widths (signed/unsigned) are permitted as label /
139 // index / count storage. bool stays excluded: std::vector<bool> is a specialization without
140 // contiguous T-addressable storage, which would silently break data(), alignedData, and the
141 // AlignedAllocator cache-line alignment invariant. Distance / reduction / GEMM primitives
142 // carry their own float/double gates so integer NDArrays cannot reach numeric math without a
143 // compile error.
144 static_assert(std::is_arithmetic_v<T> && !std::is_same_v<T, bool>,
145 "NDArray element type must be arithmetic and not bool");
146
147 // All NDArray template instantiations share friendship so view-producing verbs can construct
148 // a result with a different rank or layout via the private BorrowedTag constructor.
149 template <class U, std::size_t M, Layout LL> friend class NDArray;
150
151public:
159 public:
164 const T *data() const { return this->m_ndarray->data() + this->m_index; }
165
166 protected:
168 std::size_t m_index;
169 std::size_t m_dim;
170
178 BaseAccessor(NDArray<T, N, Layout::Contig> *ndarray, std::size_t index, std::size_t dim)
179 : m_ndarray(ndarray), m_index(index), m_dim(dim) {}
180 };
181
186 public:
194 ConstAccessor(const NDArray<T, N, Layout::Contig> &ndarray, std::size_t index, std::size_t dim)
195 : BaseAccessor(const_cast<NDArray<T, N, Layout::Contig> *>(&ndarray), index, dim) {}
196
198 ConstAccessor(const ConstAccessor &other) = default;
199
206 ConstAccessor operator[](std::size_t index) const noexcept {
207 assert(this->m_dim < N && index < this->m_ndarray->dim(this->m_dim + 1));
208 // Contig invariant lets the chain collapse to m_index * shape[dim+1] + index at every step,
209 // matching the baseline hot-loop asm clang can vectorise into an 8x-unrolled aligned load.
210 const size_t new_index = (this->m_index * this->m_ndarray->dim(this->m_dim + 1)) + index;
211 return ConstAccessor(*this->m_ndarray, new_index, this->m_dim + 1);
212 }
213
219 operator T() const noexcept { return this->m_ndarray->flatIndex(this->m_index); }
220
226 [[nodiscard]] size_t index() const noexcept { return this->m_index; }
227 };
228
232 class Accessor : public ConstAccessor {
233 public:
241 Accessor(NDArray<T, N, Layout::Contig> &ndarray, std::size_t index, std::size_t dim)
242 : ConstAccessor(ndarray, index, dim) {}
243
250 Accessor operator[](std::size_t index) noexcept {
251 assert(this->m_dim < N && index < this->m_ndarray->dim(this->m_dim + 1));
252 const size_t new_index = (this->m_index * this->m_ndarray->dim(this->m_dim + 1)) + index;
253 return Accessor(*this->m_ndarray, new_index, this->m_dim + 1);
254 }
255
265 Accessor &operator=(T value) noexcept {
266 assert(this->m_ndarray->m_mutable && "write to read-only borrow");
267 this->m_ndarray->flatIndex(this->m_index) = value;
268 return *this;
269 }
270 };
271
280 template <Layout L2 = L>
281 requires(L2 == Layout::Contig)
282 NDArray(std::initializer_list<std::size_t> dims)
283 : m_data(nullptr), m_base(nullptr), m_offset(0), m_storage(NDArrayStorage::Owned),
284 m_mutable(true) {
285 assert(dims.size() == N);
286 std::size_t i = 0;
287 std::size_t size = 1;
288 for (auto d : dims) {
289 m_shape[i++] = d;
290 size *= d;
291 }
292 m_strides = computeContiguousStrides(m_shape);
293 m_vec.resize(size);
294 m_data = m_vec.data();
295 m_base = m_data;
296 }
297
307 template <Layout L2 = L>
308 requires(L2 == Layout::Contig)
309 explicit NDArray(std::array<std::size_t, N> shape)
310 : m_data(nullptr), m_base(nullptr), m_shape(shape), m_offset(0),
311 m_storage(NDArrayStorage::Owned), m_mutable(true) {
312 std::size_t size = 1;
313 for (std::size_t k = 0; k < N; ++k) {
314 size *= m_shape[k];
315 }
316 m_strides = computeContiguousStrides(m_shape);
317 m_vec.resize(size);
318 m_data = m_vec.data();
319 m_base = m_data;
320 }
321
322 // Storage-aware special members: Owned arrays re-seat m_data against this->m_vec (the move
323 // stole or the copy just populated it), while Borrowed arrays carry an empty m_vec and must
324 // preserve the external pointer from the source.
326 NDArray(const NDArray &other)
327 : m_vec(other.m_vec), m_shape(other.m_shape), m_strides(other.m_strides),
328 m_offset(other.m_offset), m_storage(other.m_storage), m_mutable(other.m_mutable) {
329 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
330 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
331 }
332
334 NDArray(NDArray &&other) noexcept
335 : m_vec(std::move(other.m_vec)), m_shape(other.m_shape), m_strides(other.m_strides),
336 m_offset(other.m_offset), m_storage(other.m_storage), m_mutable(other.m_mutable) {
337 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
338 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
339 other.m_data = nullptr;
340 other.m_base = nullptr;
341 }
342
344 NDArray &operator=(const NDArray &other) {
345 if (this == &other) {
346 return *this;
347 }
348 m_vec = other.m_vec;
349 m_shape = other.m_shape;
350 m_strides = other.m_strides;
351 m_offset = other.m_offset;
352 m_storage = other.m_storage;
353 m_mutable = other.m_mutable;
354 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
355 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
356 return *this;
357 }
358
360 NDArray &operator=(NDArray &&other) noexcept {
361 if (this == &other) {
362 return *this;
363 }
364 m_vec = std::move(other.m_vec);
365 m_shape = other.m_shape;
366 m_strides = other.m_strides;
367 m_offset = other.m_offset;
368 m_storage = other.m_storage;
369 m_mutable = other.m_mutable;
370 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
371 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
372 other.m_data = nullptr;
373 other.m_base = nullptr;
374 return *this;
375 }
376
377private:
378 NDArray(clustering::detail::BorrowedTag, T *data, T *base, std::array<std::size_t, N> shape,
379 std::array<std::ptrdiff_t, N> strides, std::ptrdiff_t offset, bool isMutable) noexcept
380 : m_data(data), m_base(base), m_vec(), m_shape(shape), m_strides(strides), m_offset(offset),
381 m_storage(NDArrayStorage::Borrowed), m_mutable(isMutable) {
382 if constexpr (L == Layout::Contig) {
383 assert(offset == 0 && strides == computeContiguousStrides(shape) &&
384 "Contig NDArray requires contiguous strides and zero offset");
385 }
386 }
387
388public:
398 template <Layout L2 = L>
399 requires(L2 == Layout::Contig)
400 Accessor operator[](std::size_t index) noexcept {
401 assert(index < m_shape[0]);
402 return Accessor(*this, index, 0);
403 }
404
411 template <Layout L2 = L>
412 requires(L2 == Layout::Contig)
413 ConstAccessor operator[](std::size_t index) const noexcept {
414 assert(index < m_shape[0]);
415 return ConstAccessor(*this, index, 0);
416 }
417
425 template <class... Ix> T &operator()(Ix... ix) noexcept {
426 static_assert(sizeof...(Ix) == N, "operator() requires exactly N indices");
427 assert(m_mutable && "write to read-only borrow");
428 return m_data[computeElementOffset(std::index_sequence_for<Ix...>{}, ix...)];
429 }
430
432 template <class... Ix> const T &operator()(Ix... ix) const noexcept {
433 static_assert(sizeof...(Ix) == N, "operator() requires exactly N indices");
434 return m_data[computeElementOffset(std::index_sequence_for<Ix...>{}, ix...)];
435 }
436
443 T &flatIndex(std::size_t index) noexcept {
444 assert(m_mutable && "write to read-only borrow");
445 return m_data[index];
446 }
447
454 const T &flatIndex(std::size_t index) const noexcept { return m_data[index]; }
455
462 size_t dim(std::size_t index) const noexcept { return m_shape[index]; }
463
467 std::ptrdiff_t strideAt(std::size_t index) const noexcept { return m_strides[index]; }
468
475 [[nodiscard]] bool isContiguous() const noexcept {
476 if constexpr (L == Layout::Contig) {
477 return true;
478 } else {
479 return m_offset == 0 && m_strides == computeContiguousStrides(m_shape);
480 }
481 }
482
489 [[nodiscard]] bool isMutable() const noexcept { return m_mutable; }
490
497 [[nodiscard]] bool isOwned() const noexcept { return m_storage == NDArrayStorage::Owned; }
498
504 const T *data() const noexcept { return m_data; }
505
514 T *data() noexcept {
515 assert(m_mutable && "write to read-only borrow");
516 return m_data;
517 }
518
526 [[nodiscard]] T *baseData() const noexcept { return m_base; }
527
534 template <std::size_t A> bool isAligned() const noexcept {
535 return (reinterpret_cast<std::uintptr_t>(m_data) % A) == 0;
536 }
537
549 template <std::size_t A> T *alignedData() noexcept {
550 assert(isAligned<A>() && "alignedData<A>() requires A-byte aligned data");
551 return static_cast<T *>(__builtin_assume_aligned(m_data, A));
552 }
553
555 template <std::size_t A> const T *alignedData() const noexcept {
556 assert(isAligned<A>() && "alignedData<A>() requires A-byte aligned data");
557 return static_cast<const T *>(__builtin_assume_aligned(m_data, A));
558 }
559
569 template <Layout L2 = L>
570 requires(L2 == Layout::Contig)
571 static NDArray borrow(T *ptr, std::array<std::size_t, N> shape) noexcept {
572 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape,
573 computeContiguousStrides(shape), 0, true);
574 }
575
582 template <Layout L2 = L>
583 requires(L2 == Layout::Contig)
584 static NDArray borrow(const T *ptr, std::array<std::size_t, N> shape) noexcept {
585 auto *mutPtr = const_cast<T *>(ptr);
586 return NDArray(clustering::detail::BorrowedTag{}, mutPtr, mutPtr, shape,
587 computeContiguousStrides(shape), 0, false);
588 }
589
596 template <Layout L2 = L>
597 requires(L2 == Layout::MaybeStrided)
598 static NDArray borrow(T *ptr, std::array<std::size_t, N> shape,
599 std::array<std::ptrdiff_t, N> strides) noexcept {
600 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape, strides, 0, true);
601 }
602
604 template <Layout L2 = L>
605 requires(L2 == Layout::MaybeStrided)
606 static NDArray borrow(const T *ptr, std::array<std::size_t, N> shape,
607 std::array<std::ptrdiff_t, N> strides) noexcept {
608 auto *mutPtr = const_cast<T *>(ptr);
609 return NDArray(clustering::detail::BorrowedTag{}, mutPtr, mutPtr, shape, strides, 0, false);
610 }
611
615 template <std::size_t M = N>
616 requires(M == 1 && L == Layout::Contig)
617 static NDArray borrow1D(T *ptr, std::size_t n) noexcept {
618 return borrow(ptr, std::array<std::size_t, 1>{n});
619 }
620
622 template <std::size_t M = N>
623 requires(M == 1 && L == Layout::Contig)
624 static NDArray borrow1D(const T *ptr, std::size_t n) noexcept {
625 return borrow(ptr, std::array<std::size_t, 1>{n});
626 }
627
641 template <Layout L2 = L>
642 requires(L2 == Layout::MaybeStrided)
643 static NDArray borrowBytes(T *ptr, std::array<std::size_t, N> shape,
644 std::array<std::ptrdiff_t, N> stridesInBytes,
645 bool isMutable) noexcept {
646 std::array<std::ptrdiff_t, N> element_strides{};
647 for (std::size_t k = 0; k < N; ++k) {
648 assert(stridesInBytes[k] % static_cast<std::ptrdiff_t>(sizeof(T)) == 0 &&
649 "borrowBytes requires byte strides divisible by sizeof(T)");
650 element_strides[k] = stridesInBytes[k] / static_cast<std::ptrdiff_t>(sizeof(T));
651 }
652 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape, element_strides, 0,
653 isMutable);
654 }
655
662 template <std::size_t M = N>
663 requires(M == 1 && L == Layout::Contig)
664 static NDArray fromSpan(std::span<T> s) noexcept {
665 return borrow(s.data(), std::array<std::size_t, 1>{s.size()});
666 }
667
669 template <std::size_t M = N>
670 requires(M == 1 && L == Layout::Contig)
671 static NDArray fromSpan(std::span<const T> s) noexcept {
672 return borrow(s.data(), std::array<std::size_t, 1>{s.size()});
673 }
674
682 template <std::size_t M = N>
683 requires(M == 2)
686 clustering::detail::BorrowedTag{}, m_data, m_base,
687 std::array<std::size_t, 2>{m_shape[1], m_shape[0]},
688 std::array<std::ptrdiff_t, 2>{m_strides[1], m_strides[0]}, m_offset, m_mutable);
689 }
690
692 template <std::size_t M = N>
693 requires(M == 2)
696 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
697 std::array<std::size_t, 2>{m_shape[1], m_shape[0]},
698 std::array<std::ptrdiff_t, 2>{m_strides[1], m_strides[0]}, m_offset, false);
699 }
700
707 template <std::size_t M = N>
708 requires(M > 1)
709 NDArray<T, N - 1, L> row(std::size_t i) noexcept {
710 assert(i < m_shape[0]);
711 std::array<std::size_t, N - 1> new_shape{};
712 std::array<std::ptrdiff_t, N - 1> new_strides{};
713 for (std::size_t k = 0; k + 1 < N; ++k) {
714 new_shape[k] = m_shape[k + 1];
715 new_strides[k] = m_strides[k + 1];
716 }
717 return NDArray<T, N - 1, L>(clustering::detail::BorrowedTag{},
718 m_data + m_offset + (static_cast<std::ptrdiff_t>(i) * m_strides[0]),
719 m_base, new_shape, new_strides, 0, m_mutable);
720 }
721
723 template <std::size_t M = N>
724 requires(M > 1)
725 NDArray<T, N - 1, L> row(std::size_t i) const noexcept {
726 assert(i < m_shape[0]);
727 std::array<std::size_t, N - 1> new_shape{};
728 std::array<std::ptrdiff_t, N - 1> new_strides{};
729 for (std::size_t k = 0; k + 1 < N; ++k) {
730 new_shape[k] = m_shape[k + 1];
731 new_strides[k] = m_strides[k + 1];
732 }
733 return NDArray<T, N - 1, L>(clustering::detail::BorrowedTag{},
734 const_cast<T *>(m_data) + m_offset +
735 (static_cast<std::ptrdiff_t>(i) * m_strides[0]),
736 const_cast<T *>(m_base), new_shape, new_strides, 0, false);
737 }
738
745 template <std::size_t M = N>
746 requires(M == 2)
747 NDArray<T, 1, Layout::MaybeStrided> col(std::size_t j) noexcept {
748 assert(j < m_shape[1]);
750 clustering::detail::BorrowedTag{},
751 m_data + m_offset + (static_cast<std::ptrdiff_t>(j) * m_strides[1]), m_base,
752 std::array<std::size_t, 1>{m_shape[0]}, std::array<std::ptrdiff_t, 1>{m_strides[0]}, 0,
753 m_mutable);
754 }
755
757 template <std::size_t M = N>
758 requires(M == 2)
759 NDArray<T, 1, Layout::MaybeStrided> col(std::size_t j) const noexcept {
760 assert(j < m_shape[1]);
762 clustering::detail::BorrowedTag{},
763 const_cast<T *>(m_data) + m_offset + (static_cast<std::ptrdiff_t>(j) * m_strides[1]),
764 const_cast<T *>(m_base), std::array<std::size_t, 1>{m_shape[0]},
765 std::array<std::ptrdiff_t, 1>{m_strides[0]}, 0, false);
766 }
767
774 NDArray<T, N, Layout::MaybeStrided> slice(std::size_t axis, std::size_t begin,
775 std::size_t end) noexcept {
776 assert(axis < N && begin <= end && end <= m_shape[axis]);
777 std::array<std::size_t, N> new_shape = m_shape;
778 new_shape[axis] = end - begin;
780 clustering::detail::BorrowedTag{},
781 m_data + m_offset + (static_cast<std::ptrdiff_t>(begin) * m_strides[axis]), m_base,
782 new_shape, m_strides, 0, m_mutable);
783 }
784
786 NDArray<T, N, Layout::MaybeStrided> slice(std::size_t axis, std::size_t begin,
787 std::size_t end) const noexcept {
788 assert(axis < N && begin <= end && end <= m_shape[axis]);
789 std::array<std::size_t, N> new_shape = m_shape;
790 new_shape[axis] = end - begin;
792 clustering::detail::BorrowedTag{},
793 const_cast<T *>(m_data) + m_offset + (static_cast<std::ptrdiff_t>(begin) * m_strides[axis]),
794 const_cast<T *>(m_base), new_shape, m_strides, 0, false);
795 }
796
803 NDArray<T, N, Layout::MaybeStrided> slice(const std::array<Range, N> &ranges) noexcept {
804 std::array<std::size_t, N> new_shape{};
805 std::array<std::ptrdiff_t, N> new_strides{};
806 std::ptrdiff_t advance = 0;
807 for (std::size_t k = 0; k < N; ++k) {
808 const std::size_t end = std::min(ranges[k].end, m_shape[k]);
809 const std::size_t begin = ranges[k].begin;
810 const std::ptrdiff_t step = ranges[k].step;
811 assert(begin <= end && step > 0);
812 new_shape[k] = step == 1 ? (end - begin)
813 : (end - begin + static_cast<std::size_t>(step) - 1) /
814 static_cast<std::size_t>(step);
815 new_strides[k] = m_strides[k] * step;
816 advance += static_cast<std::ptrdiff_t>(begin) * m_strides[k];
817 }
818 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{},
819 m_data + m_offset + advance, m_base, new_shape,
820 new_strides, 0, m_mutable);
821 }
822
824 NDArray<T, N, Layout::MaybeStrided> slice(const std::array<Range, N> &ranges) const noexcept {
825 std::array<std::size_t, N> new_shape{};
826 std::array<std::ptrdiff_t, N> new_strides{};
827 std::ptrdiff_t advance = 0;
828 for (std::size_t k = 0; k < N; ++k) {
829 const std::size_t end = std::min(ranges[k].end, m_shape[k]);
830 const std::size_t begin = ranges[k].begin;
831 const std::ptrdiff_t step = ranges[k].step;
832 assert(begin <= end && step > 0);
833 new_shape[k] = step == 1 ? (end - begin)
834 : (end - begin + static_cast<std::size_t>(step) - 1) /
835 static_cast<std::size_t>(step);
836 new_strides[k] = m_strides[k] * step;
837 advance += static_cast<std::ptrdiff_t>(begin) * m_strides[k];
838 }
840 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data) + m_offset + advance,
841 const_cast<T *>(m_base), new_shape, new_strides, 0, false);
842 }
843
849 NDArray<T, N, Layout::MaybeStrided> permute(const std::array<std::size_t, N> &perm) noexcept {
850 std::array<std::size_t, N> new_shape{};
851 std::array<std::ptrdiff_t, N> new_strides{};
852 for (std::size_t k = 0; k < N; ++k) {
853 assert(perm[k] < N);
854 new_shape[k] = m_shape[perm[k]];
855 new_strides[k] = m_strides[perm[k]];
856 }
857 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{}, m_data, m_base,
858 new_shape, new_strides, m_offset, m_mutable);
859 }
860
863 permute(const std::array<std::size_t, N> &perm) const noexcept {
864 std::array<std::size_t, N> new_shape{};
865 std::array<std::ptrdiff_t, N> new_strides{};
866 for (std::size_t k = 0; k < N; ++k) {
867 assert(perm[k] < N);
868 new_shape[k] = m_shape[perm[k]];
869 new_strides[k] = m_strides[perm[k]];
870 }
871 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{},
872 const_cast<T *>(m_data), const_cast<T *>(m_base),
873 new_shape, new_strides, m_offset, false);
874 }
875
886 template <std::size_t M>
887 NDArray<T, M, Layout::Contig> view(std::array<std::size_t, M> shape) noexcept {
888 assert(isContiguous() && "view<M> requires a contiguous source");
889 assert(productOfShape(shape) == numel() && "view<M> must preserve element count");
891 clustering::detail::BorrowedTag{}, m_data, m_base, shape,
892 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, m_mutable);
893 }
894
896 template <std::size_t M>
897 NDArray<T, M, Layout::Contig> view(std::array<std::size_t, M> shape) const noexcept {
898 assert(isContiguous() && "view<M> requires a contiguous source");
899 assert(productOfShape(shape) == numel() && "view<M> must preserve element count");
901 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base), shape,
902 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, false);
903 }
904
915 template <std::size_t M> NDArray<T, M, Layout::Contig> reshape(std::array<std::size_t, M> shape) {
916 assert(productOfShape(shape) == numel() && "reshape<M> must preserve element count");
917 if (isContiguous()) {
919 clustering::detail::BorrowedTag{}, m_data, m_base, shape,
920 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, m_mutable);
921 }
922 NDArray<T, M, Layout::Contig> result(shape);
923 copyToContiguous(result.data());
924 return result;
925 }
926
928 template <std::size_t M>
929 NDArray<T, M, Layout::Contig> reshape(std::array<std::size_t, M> shape) const {
930 assert(productOfShape(shape) == numel() && "reshape<M> must preserve element count");
931 if (isContiguous()) {
933 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
934 shape, NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, false);
935 }
936 NDArray<T, M, Layout::Contig> result(shape);
937 copyToContiguous(result.data());
938 return result;
939 }
940
949 if (isContiguous()) {
951 clustering::detail::BorrowedTag{}, m_data, m_base, m_shape,
952 NDArray<T, N, Layout::Contig>::computeContiguousStrides(m_shape), 0, m_mutable);
953 }
954 NDArray<T, N, Layout::Contig> result(m_shape);
955 copyToContiguous(result.data());
956 return result;
957 }
958
961 if (isContiguous()) {
963 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
964 m_shape, NDArray<T, N, Layout::Contig>::computeContiguousStrides(m_shape), 0, false);
965 }
966 NDArray<T, N, Layout::Contig> result(m_shape);
967 copyToContiguous(result.data());
968 return result;
969 }
970
979 NDArray<T, N, Layout::Contig> result(m_shape);
980 copyToContiguous(result.data());
981 return result;
982 }
983
994 std::string debugDump() const {
995 std::stringstream ss;
996 ss << "NDarray<" << typeid(T).name() << ", " << N << ">(";
997 for (auto d : m_shape) {
998 ss << d << ", ";
999 }
1000 ss << ")\n";
1001 ss << "data: [";
1002 const std::size_t total = numel();
1003 if (total > 0) {
1004 std::array<std::size_t, N> idx{};
1005 for (std::size_t flat = 0; flat < total; ++flat) {
1006 std::ptrdiff_t off = m_offset;
1007 for (std::size_t k = 0; k < N; ++k) {
1008 off += static_cast<std::ptrdiff_t>(idx[k]) * m_strides[k];
1009 }
1010 ss << m_data[off] << ", ";
1011 for (std::size_t k = N; k-- > 0;) {
1012 if (++idx[k] < m_shape[k]) {
1013 break;
1014 }
1015 idx[k] = 0;
1016 }
1017 }
1018 }
1019 ss << "]\n";
1020 ss << "size: " << total << "\n";
1021 return ss.str();
1022 }
1023
1024 // Equality between NDArrays has three plausible semantics (element-wise, storage-identity,
1025 // deep-value); none is obviously correct, so the operator is deleted to force callers to pick
1026 // an explicit intent (@c math::arrayEqual, @c sameStorage, or an explicit shape-and-element
1027 // comparison).
1028 friend bool operator==(const NDArray &, const NDArray &) = delete;
1029 friend bool operator!=(const NDArray &, const NDArray &) = delete;
1030
1031private:
1032 static std::array<std::ptrdiff_t, N>
1033 computeContiguousStrides(const std::array<std::size_t, N> &shape) {
1034 std::array<std::ptrdiff_t, N> s{};
1035 s[N - 1] = 1;
1036 for (std::size_t k = N - 1; k > 0; --k) {
1037 s[k - 1] = s[k] * static_cast<std::ptrdiff_t>(shape[k]);
1038 }
1039 return s;
1040 }
1041
1042 template <std::size_t M>
1043 static std::size_t productOfShape(const std::array<std::size_t, M> &shape) noexcept {
1044 std::size_t size = 1;
1045 for (std::size_t k = 0; k < M; ++k) {
1046 size *= shape[k];
1047 }
1048 return size;
1049 }
1050
1051 std::size_t numel() const noexcept { return productOfShape(m_shape); }
1052
1053 // Walk this array in row-major order and write elements densely to @p dst. Fast-path
1054 // @c memcpy when already contiguous, else advance a multi-index cursor and index through
1055 // @c m_offset + sum_k idx[k] * m_strides[k]. Shared by @c reshape, @c contiguous, @c clone.
1056 void copyToContiguous(T *dst) const noexcept {
1057 const std::size_t total = numel();
1058 if (total == 0) {
1059 return;
1060 }
1061 if (isContiguous()) {
1062 std::memcpy(dst, m_data + m_offset, total * sizeof(T));
1063 return;
1064 }
1065 std::array<std::size_t, N> idx{};
1066 for (std::size_t flat = 0; flat < total; ++flat) {
1067 std::ptrdiff_t off = m_offset;
1068 for (std::size_t k = 0; k < N; ++k) {
1069 off += static_cast<std::ptrdiff_t>(idx[k]) * m_strides[k];
1070 }
1071 dst[flat] = m_data[off];
1072 for (std::size_t k = N; k-- > 0;) {
1073 if (++idx[k] < m_shape[k]) {
1074 break;
1075 }
1076 idx[k] = 0;
1077 }
1078 }
1079 }
1080
1081 template <std::size_t... Ks, class... Ix>
1082 std::size_t computeElementOffset(std::index_sequence<Ks...>, Ix... ix) const noexcept {
1083 std::ptrdiff_t off = m_offset;
1084 ((off += static_cast<std::ptrdiff_t>(ix) * m_strides[Ks]), ...);
1085 return static_cast<std::size_t>(off);
1086 }
1087
1088 T *m_data;
1089 T *m_base;
1090 std::vector<T, clustering::detail::AlignedAllocator<T, 64>> m_vec;
1091 std::array<std::size_t, N> m_shape;
1092 std::array<std::ptrdiff_t, N> m_strides;
1093 std::ptrdiff_t m_offset;
1094 NDArrayStorage m_storage;
1095 bool m_mutable;
1096};
1097
1106template <class T, std::size_t NA, Layout LA, std::size_t NB, Layout LB>
1107bool sameStorage(const NDArray<T, NA, LA> &a, const NDArray<T, NB, LB> &b) noexcept {
1108 return a.baseData() == b.baseData();
1109}
1110
1111} // namespace clustering
Provides read-write access to NDArray elements.
Definition ndarray.h:232
Accessor(NDArray< T, N, Layout::Contig > &ndarray, std::size_t index, std::size_t dim)
Constructs an Accessor for an NDArray.
Definition ndarray.h:241
Accessor & operator=(T value) noexcept
Assigns a value to the element at the accessor's position.
Definition ndarray.h:265
Accessor operator[](std::size_t index) noexcept
Provides access to the next dimension of the NDArray.
Definition ndarray.h:250
std::size_t m_index
Index in the flat representation of the array.
Definition ndarray.h:168
NDArray< T, N, Layout::Contig > * m_ndarray
Pointer to the NDArray.
Definition ndarray.h:167
std::size_t m_dim
Current dimension of the accessor.
Definition ndarray.h:169
BaseAccessor(NDArray< T, N, Layout::Contig > *ndarray, std::size_t index, std::size_t dim)
Constructs a BaseAccessor for a given NDArray, index, and dimension.
Definition ndarray.h:178
const T * data() const
Returns a pointer to the element data.
Definition ndarray.h:164
Provides read-only access to NDArray elements.
Definition ndarray.h:185
ConstAccessor(const NDArray< T, N, Layout::Contig > &ndarray, std::size_t index, std::size_t dim)
Constructs a ConstAccessor for a constant NDArray.
Definition ndarray.h:194
ConstAccessor operator[](std::size_t index) const noexcept
Provides access to the next dimension of the NDArray.
Definition ndarray.h:206
ConstAccessor(const ConstAccessor &other)=default
Defaulted copy constructor; accessors are lightweight and trivially copyable.
size_t index() const noexcept
Returns the flat index in the NDArray corresponding to the accessor.
Definition ndarray.h:226
Represents a multidimensional array (NDArray) of a fixed number of dimensions N and element type T.
Definition ndarray.h:136
T & flatIndex(std::size_t index) noexcept
Provides direct access to the flat underlying array at a specific index.
Definition ndarray.h:443
NDArray< T, N, Layout::Contig > clone() const
Returns a freshly-allocated owned contiguous array with deep-copied contents.
Definition ndarray.h:978
const T & flatIndex(std::size_t index) const noexcept
Provides read-only access to the flat underlying array at a specific index.
Definition ndarray.h:454
const T * alignedData() const noexcept
Read-only overload of alignedData<A>; attaches the same alignment hint to the pointer.
Definition ndarray.h:555
NDArray< T, N, Layout::MaybeStrided > slice(const std::array< Range, N > &ranges) noexcept
Borrowed multi-axis slice; each Range applies to its corresponding axis.
Definition ndarray.h:803
NDArray< T, N - 1, L > row(std::size_t i) const noexcept
Read-only row view; mirrors the mutable overload and flips m_mutable off.
Definition ndarray.h:725
NDArray(std::initializer_list< std::size_t > dims)
Constructs a contiguous owned NDArray with specified dimensions.
Definition ndarray.h:282
static NDArray borrow(const T *ptr, std::array< std::size_t, N > shape) noexcept
Borrows a read-only contiguous buffer as an NDArray.
Definition ndarray.h:584
NDArray< T, M, Layout::Contig > view(std::array< std::size_t, M > shape) noexcept
Returns a borrowed contiguous rank-M view over the same buffer with shape shape.
Definition ndarray.h:887
bool isContiguous() const noexcept
Reports whether the array's runtime layout is row-major contiguous with zero offset.
Definition ndarray.h:475
T * data() noexcept
Provides read-write access to the internal data array.
Definition ndarray.h:514
size_t dim(std::size_t index) const noexcept
Returns the size of a specific dimension of the NDArray.
Definition ndarray.h:462
NDArray< T, 2, Layout::MaybeStrided > t() noexcept
Transposes a rank-2 NDArray into a borrowed view with swapped axes.
Definition ndarray.h:684
NDArray< T, 1, Layout::MaybeStrided > col(std::size_t j) noexcept
Returns a borrowed rank-1 view of column j of a rank-2 array.
Definition ndarray.h:747
std::ptrdiff_t strideAt(std::size_t index) const noexcept
Returns the stride (in elements) for dimension index.
Definition ndarray.h:467
static NDArray fromSpan(std::span< T > s) noexcept
Explicit std::span adapter for rank-1 borrows.
Definition ndarray.h:664
T & operator()(Ix... ix) noexcept
Direct multi-index element access via strides.
Definition ndarray.h:425
NDArray< T, 1, Layout::MaybeStrided > col(std::size_t j) const noexcept
Read-only column view; mirrors the mutable overload and flips m_mutable off.
Definition ndarray.h:759
static NDArray borrowBytes(T *ptr, std::array< std::size_t, N > shape, std::array< std::ptrdiff_t, N > stridesInBytes, bool isMutable) noexcept
Borrow a buffer whose strides are expressed in bytes (NumPy's convention).
Definition ndarray.h:643
NDArray< T, M, Layout::Contig > reshape(std::array< std::size_t, M > shape)
Returns a contiguous rank-M array with shape shape, copying only when needed.
Definition ndarray.h:915
bool isOwned() const noexcept
Reports whether the array owns its underlying buffer.
Definition ndarray.h:497
static NDArray borrow1D(const T *ptr, std::size_t n) noexcept
Read-only rank-1 convenience borrow; mirrors the mutable borrow1D.
Definition ndarray.h:624
NDArray< T, N, Layout::MaybeStrided > slice(std::size_t axis, std::size_t begin, std::size_t end) noexcept
Borrowed half-open slice along a single axis.
Definition ndarray.h:774
NDArray< T, 2, Layout::MaybeStrided > t() const noexcept
Read-only transpose; the returned view carries m_mutable = false.
Definition ndarray.h:694
NDArray< T, N, Layout::MaybeStrided > slice(const std::array< Range, N > &ranges) const noexcept
Read-only multi-axis slice; mirrors the mutable overload with m_mutable = false.
Definition ndarray.h:824
friend bool operator==(const NDArray &, const NDArray &)=delete
NDArray< T, N - 1, L > row(std::size_t i) noexcept
Returns a borrowed view of row i with the leading dimension dropped.
Definition ndarray.h:709
friend bool operator!=(const NDArray &, const NDArray &)=delete
NDArray(NDArray &&other) noexcept
Move constructor; steals m_vec and re-seats m_data for owned storage.
Definition ndarray.h:334
friend class NDArray
Definition ndarray.h:149
NDArray(std::array< std::size_t, N > shape)
Constructs a contiguous owned NDArray from a runtime std::array of dimensions.
Definition ndarray.h:309
static NDArray borrow1D(T *ptr, std::size_t n) noexcept
Rank-1 convenience borrow; avoids the std::array<size_t, 1>{n} boilerplate.
Definition ndarray.h:617
T * baseData() const noexcept
Returns the original (non-advanced) base pointer for storage-identity comparisons.
Definition ndarray.h:526
static NDArray fromSpan(std::span< const T > s) noexcept
Read-only span adapter; delegates to the read-only borrow overload.
Definition ndarray.h:671
static NDArray borrow(const T *ptr, std::array< std::size_t, N > shape, std::array< std::ptrdiff_t, N > strides) noexcept
Read-only strided borrow; flips m_mutable off so writes through the view assert.
Definition ndarray.h:606
NDArray< T, N, Layout::MaybeStrided > slice(std::size_t axis, std::size_t begin, std::size_t end) const noexcept
Read-only single-axis slice; mirrors the mutable overload with m_mutable = false.
Definition ndarray.h:786
static NDArray borrow(T *ptr, std::array< std::size_t, N > shape) noexcept
Borrows a contiguous buffer as an NDArray without taking ownership.
Definition ndarray.h:571
bool isAligned() const noexcept
Tests whether data() is aligned to A bytes.
Definition ndarray.h:534
NDArray & operator=(NDArray &&other) noexcept
Move assignment; steals m_vec and re-seats m_data for owned storage.
Definition ndarray.h:360
NDArray(const NDArray &other)
Copy constructor; re-seats m_data against m_vec for owned storage.
Definition ndarray.h:326
NDArray< T, M, Layout::Contig > reshape(std::array< std::size_t, M > shape) const
Read-only rank-M reshape; aliases on contiguous sources, copies otherwise.
Definition ndarray.h:929
std::string debugDump() const
Returns a formatted string representing the contents of the NDArray.
Definition ndarray.h:994
const T & operator()(Ix... ix) const noexcept
Read-only multi-index element access via strides; mirrors the mutable overload.
Definition ndarray.h:432
NDArray & operator=(const NDArray &other)
Copy assignment; re-seats m_data against m_vec for owned storage.
Definition ndarray.h:344
NDArray< T, M, Layout::Contig > view(std::array< std::size_t, M > shape) const noexcept
Read-only rank-M view; mirrors the mutable overload with m_mutable = false.
Definition ndarray.h:897
NDArray< T, N, Layout::MaybeStrided > permute(const std::array< std::size_t, N > &perm) const noexcept
Read-only permuted view; mirrors the mutable overload with m_mutable = false.
Definition ndarray.h:863
static NDArray borrow(T *ptr, std::array< std::size_t, N > shape, std::array< std::ptrdiff_t, N > strides) noexcept
Borrows a strided buffer as an NDArray without taking ownership.
Definition ndarray.h:598
NDArray< T, N, Layout::Contig > contiguous()
Returns a contiguous rank-N array with the same shape, copying only when needed.
Definition ndarray.h:948
NDArray< T, N, Layout::Contig > contiguous() const
Read-only contiguous view; aliases on contiguous sources, copies otherwise.
Definition ndarray.h:960
NDArray< T, N, Layout::MaybeStrided > permute(const std::array< std::size_t, N > &perm) noexcept
Borrowed view with axes reordered by perm.
Definition ndarray.h:849
const T * data() const noexcept
Provides read-only access to the internal data array.
Definition ndarray.h:504
T * alignedData() noexcept
Returns data() with an alignment hint of A bytes applied.
Definition ndarray.h:549
bool isMutable() const noexcept
Reports whether writes through operator(), Accessor, or flatIndex are allowed.
Definition ndarray.h:489
bool operator!=(const AlignedAllocator< T, Align > &, const AlignedAllocator< U, Align > &) noexcept
Definition ndarray.h:100
std::atomic< std::uint64_t > & alignedAllocCallCount() noexcept
Process-global counter of non-empty AlignedAllocator::allocate calls.
Definition ndarray.h:44
bool operator==(const AlignedAllocator< T, Align > &, const AlignedAllocator< U, Align > &) noexcept
Definition ndarray.h:95
bool sameStorage(const NDArray< T, NA, LA > &a, const NDArray< T, NB, LB > &b) noexcept
Returns true when a and b share the same underlying allocation.
Definition ndarray.h:1107
NDArrayStorage
Tag indicating whether an NDArray owns its buffer or borrows memory from elsewhere.
Definition ndarray.h:109
Layout
Compile-time layout tag for NDArray.
Definition ndarray.h:122