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<T, 32> invariant. Distance / reduction / GEMM primitives carry their own
142 // float/double gates so integer NDArrays cannot reach numeric math without a compile error.
143 static_assert(std::is_arithmetic_v<T> && !std::is_same_v<T, bool>,
144 "NDArray element type must be arithmetic and not bool");
145
146 // All NDArray template instantiations share friendship so view-producing verbs can construct
147 // a result with a different rank or layout via the private BorrowedTag constructor.
148 template <class U, std::size_t M, Layout LL> friend class NDArray;
149
150public:
158 public:
163 const T *data() const { return this->m_ndarray->data() + this->m_index; }
164
165 protected:
167 std::size_t m_index;
168 std::size_t m_dim;
169
177 BaseAccessor(NDArray<T, N, Layout::Contig> *ndarray, std::size_t index, std::size_t dim)
178 : m_ndarray(ndarray), m_index(index), m_dim(dim) {}
179 };
180
185 public:
193 ConstAccessor(const NDArray<T, N, Layout::Contig> &ndarray, std::size_t index, std::size_t dim)
194 : BaseAccessor(const_cast<NDArray<T, N, Layout::Contig> *>(&ndarray), index, dim) {}
195
197 ConstAccessor(const ConstAccessor &other) = default;
198
205 ConstAccessor operator[](std::size_t index) const noexcept {
206 assert(this->m_dim < N && index < this->m_ndarray->dim(this->m_dim + 1));
207 // Contig invariant lets the chain collapse to m_index * shape[dim+1] + index at every step,
208 // matching the baseline hot-loop asm clang can vectorise into an 8x-unrolled aligned load.
209 const size_t new_index = (this->m_index * this->m_ndarray->dim(this->m_dim + 1)) + index;
210 return ConstAccessor(*this->m_ndarray, new_index, this->m_dim + 1);
211 }
212
218 operator T() const noexcept { return this->m_ndarray->flatIndex(this->m_index); }
219
225 [[nodiscard]] size_t index() const noexcept { return this->m_index; }
226 };
227
231 class Accessor : public ConstAccessor {
232 public:
240 Accessor(NDArray<T, N, Layout::Contig> &ndarray, std::size_t index, std::size_t dim)
241 : ConstAccessor(ndarray, index, dim) {}
242
249 Accessor operator[](std::size_t index) noexcept {
250 assert(this->m_dim < N && index < this->m_ndarray->dim(this->m_dim + 1));
251 const size_t new_index = (this->m_index * this->m_ndarray->dim(this->m_dim + 1)) + index;
252 return Accessor(*this->m_ndarray, new_index, this->m_dim + 1);
253 }
254
264 Accessor &operator=(T value) noexcept {
265 assert(this->m_ndarray->m_mutable && "write to read-only borrow");
266 this->m_ndarray->flatIndex(this->m_index) = value;
267 return *this;
268 }
269 };
270
279 template <Layout L2 = L>
280 requires(L2 == Layout::Contig)
281 NDArray(std::initializer_list<std::size_t> dims)
282 : m_data(nullptr), m_base(nullptr), m_offset(0), m_storage(NDArrayStorage::Owned),
283 m_mutable(true) {
284 assert(dims.size() == N);
285 std::size_t i = 0;
286 std::size_t size = 1;
287 for (auto d : dims) {
288 m_shape[i++] = d;
289 size *= d;
290 }
291 m_strides = computeContiguousStrides(m_shape);
292 m_vec.resize(size);
293 m_data = m_vec.data();
294 m_base = m_data;
295 }
296
306 template <Layout L2 = L>
307 requires(L2 == Layout::Contig)
308 explicit NDArray(std::array<std::size_t, N> shape)
309 : m_data(nullptr), m_base(nullptr), m_shape(shape), m_offset(0),
310 m_storage(NDArrayStorage::Owned), m_mutable(true) {
311 std::size_t size = 1;
312 for (std::size_t k = 0; k < N; ++k) {
313 size *= m_shape[k];
314 }
315 m_strides = computeContiguousStrides(m_shape);
316 m_vec.resize(size);
317 m_data = m_vec.data();
318 m_base = m_data;
319 }
320
321 // Storage-aware special members: Owned arrays re-seat m_data against this->m_vec (the move
322 // stole or the copy just populated it), while Borrowed arrays carry an empty m_vec and must
323 // preserve the external pointer from the source.
325 NDArray(const NDArray &other)
326 : m_vec(other.m_vec), m_shape(other.m_shape), m_strides(other.m_strides),
327 m_offset(other.m_offset), m_storage(other.m_storage), m_mutable(other.m_mutable) {
328 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
329 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
330 }
331
333 NDArray(NDArray &&other) noexcept
334 : m_vec(std::move(other.m_vec)), m_shape(other.m_shape), m_strides(other.m_strides),
335 m_offset(other.m_offset), m_storage(other.m_storage), m_mutable(other.m_mutable) {
336 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
337 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
338 other.m_data = nullptr;
339 other.m_base = nullptr;
340 }
341
343 NDArray &operator=(const NDArray &other) {
344 if (this == &other) {
345 return *this;
346 }
347 m_vec = other.m_vec;
348 m_shape = other.m_shape;
349 m_strides = other.m_strides;
350 m_offset = other.m_offset;
351 m_storage = other.m_storage;
352 m_mutable = other.m_mutable;
353 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
354 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
355 return *this;
356 }
357
359 NDArray &operator=(NDArray &&other) noexcept {
360 if (this == &other) {
361 return *this;
362 }
363 m_vec = std::move(other.m_vec);
364 m_shape = other.m_shape;
365 m_strides = other.m_strides;
366 m_offset = other.m_offset;
367 m_storage = other.m_storage;
368 m_mutable = other.m_mutable;
369 m_data = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_data;
370 m_base = (m_storage == NDArrayStorage::Owned) ? m_vec.data() : other.m_base;
371 other.m_data = nullptr;
372 other.m_base = nullptr;
373 return *this;
374 }
375
376private:
377 NDArray(clustering::detail::BorrowedTag, T *data, T *base, std::array<std::size_t, N> shape,
378 std::array<std::ptrdiff_t, N> strides, std::ptrdiff_t offset, bool isMutable) noexcept
379 : m_data(data), m_base(base), m_vec(), m_shape(shape), m_strides(strides), m_offset(offset),
380 m_storage(NDArrayStorage::Borrowed), m_mutable(isMutable) {
381 if constexpr (L == Layout::Contig) {
382 assert(offset == 0 && strides == computeContiguousStrides(shape) &&
383 "Contig NDArray requires contiguous strides and zero offset");
384 }
385 }
386
387public:
397 template <Layout L2 = L>
398 requires(L2 == Layout::Contig)
399 Accessor operator[](std::size_t index) noexcept {
400 assert(index < m_shape[0]);
401 return Accessor(*this, index, 0);
402 }
403
410 template <Layout L2 = L>
411 requires(L2 == Layout::Contig)
412 ConstAccessor operator[](std::size_t index) const noexcept {
413 assert(index < m_shape[0]);
414 return ConstAccessor(*this, index, 0);
415 }
416
424 template <class... Ix> T &operator()(Ix... ix) noexcept {
425 static_assert(sizeof...(Ix) == N, "operator() requires exactly N indices");
426 assert(m_mutable && "write to read-only borrow");
427 return m_data[computeElementOffset(std::index_sequence_for<Ix...>{}, ix...)];
428 }
429
431 template <class... Ix> const T &operator()(Ix... ix) const noexcept {
432 static_assert(sizeof...(Ix) == N, "operator() requires exactly N indices");
433 return m_data[computeElementOffset(std::index_sequence_for<Ix...>{}, ix...)];
434 }
435
442 T &flatIndex(std::size_t index) noexcept {
443 assert(m_mutable && "write to read-only borrow");
444 return m_data[index];
445 }
446
453 const T &flatIndex(std::size_t index) const noexcept { return m_data[index]; }
454
461 size_t dim(std::size_t index) const noexcept { return m_shape[index]; }
462
466 std::ptrdiff_t strideAt(std::size_t index) const noexcept { return m_strides[index]; }
467
474 [[nodiscard]] bool isContiguous() const noexcept {
475 if constexpr (L == Layout::Contig) {
476 return true;
477 } else {
478 return m_offset == 0 && m_strides == computeContiguousStrides(m_shape);
479 }
480 }
481
488 [[nodiscard]] bool isMutable() const noexcept { return m_mutable; }
489
496 [[nodiscard]] bool isOwned() const noexcept { return m_storage == NDArrayStorage::Owned; }
497
503 const T *data() const noexcept { return m_data; }
504
513 T *data() noexcept {
514 assert(m_mutable && "write to read-only borrow");
515 return m_data;
516 }
517
525 [[nodiscard]] T *baseData() const noexcept { return m_base; }
526
533 template <std::size_t A> bool isAligned() const noexcept {
534 return (reinterpret_cast<std::uintptr_t>(m_data) % A) == 0;
535 }
536
548 template <std::size_t A> T *alignedData() noexcept {
549 assert(isAligned<A>() && "alignedData<A>() requires A-byte aligned data");
550 return static_cast<T *>(__builtin_assume_aligned(m_data, A));
551 }
552
554 template <std::size_t A> const T *alignedData() const noexcept {
555 assert(isAligned<A>() && "alignedData<A>() requires A-byte aligned data");
556 return static_cast<const T *>(__builtin_assume_aligned(m_data, A));
557 }
558
568 template <Layout L2 = L>
569 requires(L2 == Layout::Contig)
570 static NDArray borrow(T *ptr, std::array<std::size_t, N> shape) noexcept {
571 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape,
572 computeContiguousStrides(shape), 0, true);
573 }
574
581 template <Layout L2 = L>
582 requires(L2 == Layout::Contig)
583 static NDArray borrow(const T *ptr, std::array<std::size_t, N> shape) noexcept {
584 auto *mutPtr = const_cast<T *>(ptr);
585 return NDArray(clustering::detail::BorrowedTag{}, mutPtr, mutPtr, shape,
586 computeContiguousStrides(shape), 0, false);
587 }
588
595 template <Layout L2 = L>
596 requires(L2 == Layout::MaybeStrided)
597 static NDArray borrow(T *ptr, std::array<std::size_t, N> shape,
598 std::array<std::ptrdiff_t, N> strides) noexcept {
599 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape, strides, 0, true);
600 }
601
603 template <Layout L2 = L>
604 requires(L2 == Layout::MaybeStrided)
605 static NDArray borrow(const T *ptr, std::array<std::size_t, N> shape,
606 std::array<std::ptrdiff_t, N> strides) noexcept {
607 auto *mutPtr = const_cast<T *>(ptr);
608 return NDArray(clustering::detail::BorrowedTag{}, mutPtr, mutPtr, shape, strides, 0, false);
609 }
610
614 template <std::size_t M = N>
615 requires(M == 1 && L == Layout::Contig)
616 static NDArray borrow1D(T *ptr, std::size_t n) noexcept {
617 return borrow(ptr, std::array<std::size_t, 1>{n});
618 }
619
621 template <std::size_t M = N>
622 requires(M == 1 && L == Layout::Contig)
623 static NDArray borrow1D(const T *ptr, std::size_t n) noexcept {
624 return borrow(ptr, std::array<std::size_t, 1>{n});
625 }
626
640 template <Layout L2 = L>
641 requires(L2 == Layout::MaybeStrided)
642 static NDArray borrowBytes(T *ptr, std::array<std::size_t, N> shape,
643 std::array<std::ptrdiff_t, N> stridesInBytes,
644 bool isMutable) noexcept {
645 std::array<std::ptrdiff_t, N> element_strides{};
646 for (std::size_t k = 0; k < N; ++k) {
647 assert(stridesInBytes[k] % static_cast<std::ptrdiff_t>(sizeof(T)) == 0 &&
648 "borrowBytes requires byte strides divisible by sizeof(T)");
649 element_strides[k] = stridesInBytes[k] / static_cast<std::ptrdiff_t>(sizeof(T));
650 }
651 return NDArray(clustering::detail::BorrowedTag{}, ptr, ptr, shape, element_strides, 0,
652 isMutable);
653 }
654
661 template <std::size_t M = N>
662 requires(M == 1 && L == Layout::Contig)
663 static NDArray fromSpan(std::span<T> s) noexcept {
664 return borrow(s.data(), std::array<std::size_t, 1>{s.size()});
665 }
666
668 template <std::size_t M = N>
669 requires(M == 1 && L == Layout::Contig)
670 static NDArray fromSpan(std::span<const T> s) noexcept {
671 return borrow(s.data(), std::array<std::size_t, 1>{s.size()});
672 }
673
681 template <std::size_t M = N>
682 requires(M == 2)
685 clustering::detail::BorrowedTag{}, m_data, m_base,
686 std::array<std::size_t, 2>{m_shape[1], m_shape[0]},
687 std::array<std::ptrdiff_t, 2>{m_strides[1], m_strides[0]}, m_offset, m_mutable);
688 }
689
691 template <std::size_t M = N>
692 requires(M == 2)
695 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
696 std::array<std::size_t, 2>{m_shape[1], m_shape[0]},
697 std::array<std::ptrdiff_t, 2>{m_strides[1], m_strides[0]}, m_offset, false);
698 }
699
706 template <std::size_t M = N>
707 requires(M > 1)
708 NDArray<T, N - 1, L> row(std::size_t i) noexcept {
709 assert(i < m_shape[0]);
710 std::array<std::size_t, N - 1> new_shape{};
711 std::array<std::ptrdiff_t, N - 1> new_strides{};
712 for (std::size_t k = 0; k + 1 < N; ++k) {
713 new_shape[k] = m_shape[k + 1];
714 new_strides[k] = m_strides[k + 1];
715 }
716 return NDArray<T, N - 1, L>(clustering::detail::BorrowedTag{},
717 m_data + m_offset + (static_cast<std::ptrdiff_t>(i) * m_strides[0]),
718 m_base, new_shape, new_strides, 0, m_mutable);
719 }
720
722 template <std::size_t M = N>
723 requires(M > 1)
724 NDArray<T, N - 1, L> row(std::size_t i) const noexcept {
725 assert(i < m_shape[0]);
726 std::array<std::size_t, N - 1> new_shape{};
727 std::array<std::ptrdiff_t, N - 1> new_strides{};
728 for (std::size_t k = 0; k + 1 < N; ++k) {
729 new_shape[k] = m_shape[k + 1];
730 new_strides[k] = m_strides[k + 1];
731 }
732 return NDArray<T, N - 1, L>(clustering::detail::BorrowedTag{},
733 const_cast<T *>(m_data) + m_offset +
734 (static_cast<std::ptrdiff_t>(i) * m_strides[0]),
735 const_cast<T *>(m_base), new_shape, new_strides, 0, false);
736 }
737
744 template <std::size_t M = N>
745 requires(M == 2)
746 NDArray<T, 1, Layout::MaybeStrided> col(std::size_t j) noexcept {
747 assert(j < m_shape[1]);
749 clustering::detail::BorrowedTag{},
750 m_data + m_offset + (static_cast<std::ptrdiff_t>(j) * m_strides[1]), m_base,
751 std::array<std::size_t, 1>{m_shape[0]}, std::array<std::ptrdiff_t, 1>{m_strides[0]}, 0,
752 m_mutable);
753 }
754
756 template <std::size_t M = N>
757 requires(M == 2)
758 NDArray<T, 1, Layout::MaybeStrided> col(std::size_t j) const noexcept {
759 assert(j < m_shape[1]);
761 clustering::detail::BorrowedTag{},
762 const_cast<T *>(m_data) + m_offset + (static_cast<std::ptrdiff_t>(j) * m_strides[1]),
763 const_cast<T *>(m_base), std::array<std::size_t, 1>{m_shape[0]},
764 std::array<std::ptrdiff_t, 1>{m_strides[0]}, 0, false);
765 }
766
773 NDArray<T, N, Layout::MaybeStrided> slice(std::size_t axis, std::size_t begin,
774 std::size_t end) noexcept {
775 assert(axis < N && begin <= end && end <= m_shape[axis]);
776 std::array<std::size_t, N> new_shape = m_shape;
777 new_shape[axis] = end - begin;
779 clustering::detail::BorrowedTag{},
780 m_data + m_offset + (static_cast<std::ptrdiff_t>(begin) * m_strides[axis]), m_base,
781 new_shape, m_strides, 0, m_mutable);
782 }
783
785 NDArray<T, N, Layout::MaybeStrided> slice(std::size_t axis, std::size_t begin,
786 std::size_t end) const noexcept {
787 assert(axis < N && begin <= end && end <= m_shape[axis]);
788 std::array<std::size_t, N> new_shape = m_shape;
789 new_shape[axis] = end - begin;
791 clustering::detail::BorrowedTag{},
792 const_cast<T *>(m_data) + m_offset + (static_cast<std::ptrdiff_t>(begin) * m_strides[axis]),
793 const_cast<T *>(m_base), new_shape, m_strides, 0, false);
794 }
795
802 NDArray<T, N, Layout::MaybeStrided> slice(const std::array<Range, N> &ranges) noexcept {
803 std::array<std::size_t, N> new_shape{};
804 std::array<std::ptrdiff_t, N> new_strides{};
805 std::ptrdiff_t advance = 0;
806 for (std::size_t k = 0; k < N; ++k) {
807 const std::size_t end = std::min(ranges[k].end, m_shape[k]);
808 const std::size_t begin = ranges[k].begin;
809 const std::ptrdiff_t step = ranges[k].step;
810 assert(begin <= end && step > 0);
811 new_shape[k] = step == 1 ? (end - begin)
812 : (end - begin + static_cast<std::size_t>(step) - 1) /
813 static_cast<std::size_t>(step);
814 new_strides[k] = m_strides[k] * step;
815 advance += static_cast<std::ptrdiff_t>(begin) * m_strides[k];
816 }
817 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{},
818 m_data + m_offset + advance, m_base, new_shape,
819 new_strides, 0, m_mutable);
820 }
821
823 NDArray<T, N, Layout::MaybeStrided> slice(const std::array<Range, N> &ranges) const noexcept {
824 std::array<std::size_t, N> new_shape{};
825 std::array<std::ptrdiff_t, N> new_strides{};
826 std::ptrdiff_t advance = 0;
827 for (std::size_t k = 0; k < N; ++k) {
828 const std::size_t end = std::min(ranges[k].end, m_shape[k]);
829 const std::size_t begin = ranges[k].begin;
830 const std::ptrdiff_t step = ranges[k].step;
831 assert(begin <= end && step > 0);
832 new_shape[k] = step == 1 ? (end - begin)
833 : (end - begin + static_cast<std::size_t>(step) - 1) /
834 static_cast<std::size_t>(step);
835 new_strides[k] = m_strides[k] * step;
836 advance += static_cast<std::ptrdiff_t>(begin) * m_strides[k];
837 }
839 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data) + m_offset + advance,
840 const_cast<T *>(m_base), new_shape, new_strides, 0, false);
841 }
842
848 NDArray<T, N, Layout::MaybeStrided> permute(const std::array<std::size_t, N> &perm) noexcept {
849 std::array<std::size_t, N> new_shape{};
850 std::array<std::ptrdiff_t, N> new_strides{};
851 for (std::size_t k = 0; k < N; ++k) {
852 assert(perm[k] < N);
853 new_shape[k] = m_shape[perm[k]];
854 new_strides[k] = m_strides[perm[k]];
855 }
856 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{}, m_data, m_base,
857 new_shape, new_strides, m_offset, m_mutable);
858 }
859
862 permute(const std::array<std::size_t, N> &perm) const noexcept {
863 std::array<std::size_t, N> new_shape{};
864 std::array<std::ptrdiff_t, N> new_strides{};
865 for (std::size_t k = 0; k < N; ++k) {
866 assert(perm[k] < N);
867 new_shape[k] = m_shape[perm[k]];
868 new_strides[k] = m_strides[perm[k]];
869 }
870 return NDArray<T, N, Layout::MaybeStrided>(clustering::detail::BorrowedTag{},
871 const_cast<T *>(m_data), const_cast<T *>(m_base),
872 new_shape, new_strides, m_offset, false);
873 }
874
885 template <std::size_t M>
886 NDArray<T, M, Layout::Contig> view(std::array<std::size_t, M> shape) noexcept {
887 assert(isContiguous() && "view<M> requires a contiguous source");
888 assert(productOfShape(shape) == numel() && "view<M> must preserve element count");
890 clustering::detail::BorrowedTag{}, m_data, m_base, shape,
891 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, m_mutable);
892 }
893
895 template <std::size_t M>
896 NDArray<T, M, Layout::Contig> view(std::array<std::size_t, M> shape) const noexcept {
897 assert(isContiguous() && "view<M> requires a contiguous source");
898 assert(productOfShape(shape) == numel() && "view<M> must preserve element count");
900 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base), shape,
901 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, false);
902 }
903
914 template <std::size_t M> NDArray<T, M, Layout::Contig> reshape(std::array<std::size_t, M> shape) {
915 assert(productOfShape(shape) == numel() && "reshape<M> must preserve element count");
916 if (isContiguous()) {
918 clustering::detail::BorrowedTag{}, m_data, m_base, shape,
919 NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, m_mutable);
920 }
921 NDArray<T, M, Layout::Contig> result(shape);
922 copyToContiguous(result.data());
923 return result;
924 }
925
927 template <std::size_t M>
928 NDArray<T, M, Layout::Contig> reshape(std::array<std::size_t, M> shape) const {
929 assert(productOfShape(shape) == numel() && "reshape<M> must preserve element count");
930 if (isContiguous()) {
932 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
933 shape, NDArray<T, M, Layout::Contig>::computeContiguousStrides(shape), 0, false);
934 }
935 NDArray<T, M, Layout::Contig> result(shape);
936 copyToContiguous(result.data());
937 return result;
938 }
939
948 if (isContiguous()) {
950 clustering::detail::BorrowedTag{}, m_data, m_base, m_shape,
951 NDArray<T, N, Layout::Contig>::computeContiguousStrides(m_shape), 0, m_mutable);
952 }
953 NDArray<T, N, Layout::Contig> result(m_shape);
954 copyToContiguous(result.data());
955 return result;
956 }
957
960 if (isContiguous()) {
962 clustering::detail::BorrowedTag{}, const_cast<T *>(m_data), const_cast<T *>(m_base),
963 m_shape, NDArray<T, N, Layout::Contig>::computeContiguousStrides(m_shape), 0, false);
964 }
965 NDArray<T, N, Layout::Contig> result(m_shape);
966 copyToContiguous(result.data());
967 return result;
968 }
969
978 NDArray<T, N, Layout::Contig> result(m_shape);
979 copyToContiguous(result.data());
980 return result;
981 }
982
993 std::string debugDump() const {
994 std::stringstream ss;
995 ss << "NDarray<" << typeid(T).name() << ", " << N << ">(";
996 for (auto d : m_shape) {
997 ss << d << ", ";
998 }
999 ss << ")\n";
1000 ss << "data: [";
1001 const std::size_t total = numel();
1002 if (total > 0) {
1003 std::array<std::size_t, N> idx{};
1004 for (std::size_t flat = 0; flat < total; ++flat) {
1005 std::ptrdiff_t off = m_offset;
1006 for (std::size_t k = 0; k < N; ++k) {
1007 off += static_cast<std::ptrdiff_t>(idx[k]) * m_strides[k];
1008 }
1009 ss << m_data[off] << ", ";
1010 for (std::size_t k = N; k-- > 0;) {
1011 if (++idx[k] < m_shape[k]) {
1012 break;
1013 }
1014 idx[k] = 0;
1015 }
1016 }
1017 }
1018 ss << "]\n";
1019 ss << "size: " << total << "\n";
1020 return ss.str();
1021 }
1022
1023 // Equality between NDArrays has three plausible semantics (element-wise, storage-identity,
1024 // deep-value); none is obviously correct, so the operator is deleted to force callers to pick
1025 // an explicit intent (@c math::arrayEqual, @c sameStorage, or an explicit shape-and-element
1026 // comparison).
1027 friend bool operator==(const NDArray &, const NDArray &) = delete;
1028 friend bool operator!=(const NDArray &, const NDArray &) = delete;
1029
1030private:
1031 static std::array<std::ptrdiff_t, N>
1032 computeContiguousStrides(const std::array<std::size_t, N> &shape) {
1033 std::array<std::ptrdiff_t, N> s{};
1034 s[N - 1] = 1;
1035 for (std::size_t k = N - 1; k > 0; --k) {
1036 s[k - 1] = s[k] * static_cast<std::ptrdiff_t>(shape[k]);
1037 }
1038 return s;
1039 }
1040
1041 template <std::size_t M>
1042 static std::size_t productOfShape(const std::array<std::size_t, M> &shape) noexcept {
1043 std::size_t size = 1;
1044 for (std::size_t k = 0; k < M; ++k) {
1045 size *= shape[k];
1046 }
1047 return size;
1048 }
1049
1050 std::size_t numel() const noexcept { return productOfShape(m_shape); }
1051
1052 // Walk this array in row-major order and write elements densely to @p dst. Fast-path
1053 // @c memcpy when already contiguous, else advance a multi-index cursor and index through
1054 // @c m_offset + sum_k idx[k] * m_strides[k]. Shared by @c reshape, @c contiguous, @c clone.
1055 void copyToContiguous(T *dst) const noexcept {
1056 const std::size_t total = numel();
1057 if (total == 0) {
1058 return;
1059 }
1060 if (isContiguous()) {
1061 std::memcpy(dst, m_data + m_offset, total * sizeof(T));
1062 return;
1063 }
1064 std::array<std::size_t, N> idx{};
1065 for (std::size_t flat = 0; flat < total; ++flat) {
1066 std::ptrdiff_t off = m_offset;
1067 for (std::size_t k = 0; k < N; ++k) {
1068 off += static_cast<std::ptrdiff_t>(idx[k]) * m_strides[k];
1069 }
1070 dst[flat] = m_data[off];
1071 for (std::size_t k = N; k-- > 0;) {
1072 if (++idx[k] < m_shape[k]) {
1073 break;
1074 }
1075 idx[k] = 0;
1076 }
1077 }
1078 }
1079
1080 template <std::size_t... Ks, class... Ix>
1081 std::size_t computeElementOffset(std::index_sequence<Ks...>, Ix... ix) const noexcept {
1082 std::ptrdiff_t off = m_offset;
1083 ((off += static_cast<std::ptrdiff_t>(ix) * m_strides[Ks]), ...);
1084 return static_cast<std::size_t>(off);
1085 }
1086
1087 T *m_data;
1088 T *m_base;
1089 std::vector<T, clustering::detail::AlignedAllocator<T, 32>> m_vec;
1090 std::array<std::size_t, N> m_shape;
1091 std::array<std::ptrdiff_t, N> m_strides;
1092 std::ptrdiff_t m_offset;
1093 NDArrayStorage m_storage;
1094 bool m_mutable;
1095};
1096
1105template <class T, std::size_t NA, Layout LA, std::size_t NB, Layout LB>
1106bool sameStorage(const NDArray<T, NA, LA> &a, const NDArray<T, NB, LB> &b) noexcept {
1107 return a.baseData() == b.baseData();
1108}
1109
1110} // namespace clustering
Provides read-write access to NDArray elements.
Definition ndarray.h:231
Accessor(NDArray< T, N, Layout::Contig > &ndarray, std::size_t index, std::size_t dim)
Constructs an Accessor for an NDArray.
Definition ndarray.h:240
Accessor & operator=(T value) noexcept
Assigns a value to the element at the accessor's position.
Definition ndarray.h:264
Accessor operator[](std::size_t index) noexcept
Provides access to the next dimension of the NDArray.
Definition ndarray.h:249
std::size_t m_index
Index in the flat representation of the array.
Definition ndarray.h:167
NDArray< T, N, Layout::Contig > * m_ndarray
Pointer to the NDArray.
Definition ndarray.h:166
std::size_t m_dim
Current dimension of the accessor.
Definition ndarray.h:168
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:177
const T * data() const
Returns a pointer to the element data.
Definition ndarray.h:163
Provides read-only access to NDArray elements.
Definition ndarray.h:184
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:193
ConstAccessor operator[](std::size_t index) const noexcept
Provides access to the next dimension of the NDArray.
Definition ndarray.h:205
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:225
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:442
NDArray< T, N, Layout::Contig > clone() const
Returns a freshly-allocated owned contiguous array with deep-copied contents.
Definition ndarray.h:977
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:453
const T * alignedData() const noexcept
Read-only overload of alignedData<A>; attaches the same alignment hint to the pointer.
Definition ndarray.h:554
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:802
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:724
NDArray(std::initializer_list< std::size_t > dims)
Constructs a contiguous owned NDArray with specified dimensions.
Definition ndarray.h:281
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:583
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:886
bool isContiguous() const noexcept
Reports whether the array's runtime layout is row-major contiguous with zero offset.
Definition ndarray.h:474
T * data() noexcept
Provides read-write access to the internal data array.
Definition ndarray.h:513
size_t dim(std::size_t index) const noexcept
Returns the size of a specific dimension of the NDArray.
Definition ndarray.h:461
NDArray< T, 2, Layout::MaybeStrided > t() noexcept
Transposes a rank-2 NDArray into a borrowed view with swapped axes.
Definition ndarray.h:683
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:746
std::ptrdiff_t strideAt(std::size_t index) const noexcept
Returns the stride (in elements) for dimension index.
Definition ndarray.h:466
static NDArray fromSpan(std::span< T > s) noexcept
Explicit std::span adapter for rank-1 borrows.
Definition ndarray.h:663
T & operator()(Ix... ix) noexcept
Direct multi-index element access via strides.
Definition ndarray.h:424
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:758
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:642
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:914
bool isOwned() const noexcept
Reports whether the array owns its underlying buffer.
Definition ndarray.h:496
static NDArray borrow1D(const T *ptr, std::size_t n) noexcept
Read-only rank-1 convenience borrow; mirrors the mutable borrow1D.
Definition ndarray.h:623
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:773
NDArray< T, 2, Layout::MaybeStrided > t() const noexcept
Read-only transpose; the returned view carries m_mutable = false.
Definition ndarray.h:693
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:823
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:708
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:333
friend class NDArray
Definition ndarray.h:148
NDArray(std::array< std::size_t, N > shape)
Constructs a contiguous owned NDArray from a runtime std::array of dimensions.
Definition ndarray.h:308
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:616
T * baseData() const noexcept
Returns the original (non-advanced) base pointer for storage-identity comparisons.
Definition ndarray.h:525
static NDArray fromSpan(std::span< const T > s) noexcept
Read-only span adapter; delegates to the read-only borrow overload.
Definition ndarray.h:670
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:605
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:785
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:570
bool isAligned() const noexcept
Tests whether data() is aligned to A bytes.
Definition ndarray.h:533
NDArray & operator=(NDArray &&other) noexcept
Move assignment; steals m_vec and re-seats m_data for owned storage.
Definition ndarray.h:359
NDArray(const NDArray &other)
Copy constructor; re-seats m_data against m_vec for owned storage.
Definition ndarray.h:325
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:928
std::string debugDump() const
Returns a formatted string representing the contents of the NDArray.
Definition ndarray.h:993
const T & operator()(Ix... ix) const noexcept
Read-only multi-index element access via strides; mirrors the mutable overload.
Definition ndarray.h:431
NDArray & operator=(const NDArray &other)
Copy assignment; re-seats m_data against m_vec for owned storage.
Definition ndarray.h:343
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:896
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:862
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:597
NDArray< T, N, Layout::Contig > contiguous()
Returns a contiguous rank-N array with the same shape, copying only when needed.
Definition ndarray.h:947
NDArray< T, N, Layout::Contig > contiguous() const
Read-only contiguous view; aliases on contiguous sources, copies otherwise.
Definition ndarray.h:959
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:848
const T * data() const noexcept
Provides read-only access to the internal data array.
Definition ndarray.h:503
T * alignedData() noexcept
Returns data() with an alignment hint of A bytes applied.
Definition ndarray.h:548
bool isMutable() const noexcept
Reports whether writes through operator(), Accessor, or flatIndex are allowed.
Definition ndarray.h:488
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:1106
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