nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
shape_ops.hpp
1// core/include/nk/value/shape_ops.hpp
2//
3// Pure-function helpers for ND tensor shape ops: broadcasting, strides,
4// coord-walking. Used by builtin and DSP ops that need to generalise
5// past 2D/3D. Header-only inline so the optimiser can fold the ndim
6// branches in 2D/3D fast paths.
7//
8// All routines use column-major strides (rows = innermost), matching
9// MATLAB and the existing Value layout. Trailing-singleton normalisation
10// is implicit: a missing trailing dim is treated as 1.
11
12#pragma once
13
14#include <nk/value/dims.hpp>
15
16#include <algorithm>
17#include <cstddef>
18#include <cstdint>
19
20namespace nk {
21
22// ============================================================
23// broadcastDimsND
24// ------------------------------------------------------------
25// NumPy-style implicit expansion. Aligns dims right (i.e. trailing-
26// singleton-extend the shorter shape), then per-axis: dims must match
27// or one must be 1. Output ndim = max(a.ndim, b.ndim).
28//
29// Returns true on success and writes the result Dims to out. Returns
30// false if any axis is incompatible (output left untouched).
31// ============================================================
32inline bool broadcastDimsND(const Dims &a, const Dims &b, Dims &out)
33{
34 const int na = a.ndim();
35 const int nb = b.ndim();
36 const int nOut = std::max(na, nb);
37
38 // Build the output dim vector on the stack (or fall back to small
39 // heap if you really need >32D, which isn't a thing).
40 constexpr int kMaxStack = 32;
41 size_t buf[kMaxStack];
42 if (nOut > kMaxStack) return false; // refuse pathological ranks
43
44 for (int i = 0; i < nOut; ++i) {
45 const size_t da = (i < na) ? a.dim(i) : 1;
46 const size_t db = (i < nb) ? b.dim(i) : 1;
47 if (da != db && da != 1 && db != 1) return false;
48 buf[i] = std::max(da, db);
49 }
50 out = Dims(buf, nOut);
51 return true;
52}
53
54// ============================================================
55// broadcastOffsetND
56// ------------------------------------------------------------
57// Given output coords and an operand's shape, compute the operand's
58// linear (column-major) offset under broadcasting. Operand axes that
59// are 1 collapse their coord to 0 (broadcasting); operand axes missing
60// (because operand has lower ndim than output) collapse to 0 too.
61//
62// Hot inner-loop function; kept inline. coords[] length must be ≥ outNdim.
63// ============================================================
64inline size_t broadcastOffsetND(const size_t *coords, int outNdim,
65 const Dims &operand)
66{
67 const int nOp = operand.ndim();
68 size_t off = 0;
69 size_t stride = 1;
70 for (int i = 0; i < outNdim; ++i) {
71 const size_t opDim = (i < nOp) ? operand.dim(i) : 1;
72 const size_t c = (opDim == 1) ? 0 : coords[i];
73 off += c * stride;
74 stride *= opDim;
75 }
76 return off;
77}
78
79// ============================================================
80// computeStridesColMajor
81// ------------------------------------------------------------
82// strides[0] = 1; strides[i] = strides[i-1] * d.dim(i-1).
83// Caller provides strides[] of length ≥ d.ndim().
84// ============================================================
85inline void computeStridesColMajor(const Dims &d, size_t *strides)
86{
87 const int n = d.ndim();
88 if (n == 0) return;
89 strides[0] = 1;
90 for (int i = 1; i < n; ++i)
91 strides[i] = strides[i - 1] * d.dim(i - 1);
92}
93
94// ============================================================
95// linearizeFromCoords
96// ------------------------------------------------------------
97// Linear (column-major) offset from coords[] given precomputed strides.
98// Used after computeStridesColMajor; saves the per-element multiply chain
99// in tight loops by amortising it across coordinates.
100// ============================================================
101inline size_t linearizeFromCoords(const size_t *coords, const size_t *strides,
102 int nd)
103{
104 size_t off = 0;
105 for (int i = 0; i < nd; ++i)
106 off += coords[i] * strides[i];
107 return off;
108}
109
110// ============================================================
111// incrementCoords
112// ------------------------------------------------------------
113// In-place ++coords with carry from inner (axis 0) to outer (axis nd-1).
114// Returns false when the iteration is complete (coords have wrapped past
115// the highest axis). Used by generic ND walkers in ops that don't have
116// a 2D/3D fast path.
117//
118// Pattern:
119// size_t coords[N] = {0};
120// do {
121// // ... use coords ...
122// } while (incrementCoords(coords, dims));
123// ============================================================
124inline bool incrementCoords(size_t *coords, const Dims &d)
125{
126 const int n = d.ndim();
127 for (int i = 0; i < n; ++i) {
128 if (++coords[i] < d.dim(i)) return true;
129 coords[i] = 0;
130 }
131 return false;
132}
133
134// ============================================================
135// forEachNDPair
136// ------------------------------------------------------------
137// Walk every output coordinate of a binary elementwise op under NumPy
138// broadcasting and call fn(outIdx, aOff, bOff) with the column-major
139// linear offsets into operand A and B.
140//
141// Same-shape fast path: aDims == bDims == outDims collapses to a single
142// linear loop where all three indices coincide. General path walks
143// coords with carry and uses broadcastOffsetND per operand.
144//
145// Precondition: outDims was produced by broadcastDimsND (so its ndim
146// is already ≤ kMaxRank). Fn signature: (size_t outIdx, size_t aOff,
147// size_t bOff).
148// ============================================================
149template <typename Fn>
150inline void forEachNDPair(const Dims &aDims, const Dims &bDims,
151 const Dims &outDims, Fn &&fn)
152{
153 if (aDims == bDims && aDims == outDims) {
154 const size_t n = outDims.numel();
155 for (size_t i = 0; i < n; ++i) fn(i, i, i);
156 return;
157 }
158 const int nd = outDims.ndim();
159 size_t coords[Dims::kMaxRank] = {0};
160 size_t outIdx = 0;
161 do {
162 const size_t aOff = broadcastOffsetND(coords, nd, aDims);
163 const size_t bOff = broadcastOffsetND(coords, nd, bDims);
164 fn(outIdx++, aOff, bOff);
165 } while (incrementCoords(coords, outDims));
166}
167
168// ============================================================
169// forEachOuterPage
170// ------------------------------------------------------------
171// Walk every (R×C) "page" of d, indexed by axes 2..ndim-1 in column-
172// major order (axis 2 fastest, then axis 3, ...). For each page index
173// `plin ∈ [0, outerCount)`, calls fn(plin, outerCoords) where
174// outerCoords[i] is the 0-based position along axis i+2.
175//
176// Used by ND format / disp (header per page) and by trilTriuND /
177// rot90 ND (per-page byte kernel). For ndim ≤ 2 there's exactly one
178// "page" with empty outerCoords.
179// ============================================================
180template <typename Fn>
181inline void forEachOuterPage(const Dims &d, Fn &&fn)
182{
183 const int nd = d.ndim();
184 size_t outerCount = 1;
185 for (int i = 2; i < nd; ++i) outerCount *= d.dim(i);
186 size_t outerCoords[Dims::kMaxRank] = {0};
187 for (size_t plin = 0; plin < outerCount; ++plin) {
188 fn(plin, outerCoords);
189 // Increment outer coords (axis 2..nd-1) in column-major order.
190 for (int i = 2; i < nd; ++i) {
191 if (++outerCoords[i - 2] < d.dim(i)) break;
192 outerCoords[i - 2] = 0;
193 }
194 }
195}
196
197} // namespace nk
Value db(const Value &x, const std::string &signalType="voltage", std::pmr::memory_resource *mr=nullptr)
Convert magnitude or power to decibels.