nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
reductions.hpp
1// toolboxes/builtin/src/reduction_helpers.hpp
2//
3// Shared infrastructure for "reduce-along-dim" style operations.
4// Used by Phase-1 stats (var/std/median/quantile/prctile/mode) and
5// any future reductions that want explicit `dim` support.
6//
7// Two layouts handled:
8// * 2D matrix (R×C, column-major): dim=1 reduces rows, dim=2 reduces cols
9// * 3D array (R×C×P, page = R*C stride): dim=1/2/3 reduces that axis
10//
11// Vectors and scalars: ignore the explicit dim and reduce all elements
12// (matching MATLAB's "first non-singleton" behavior for vectors).
13//
14// API: applyAlongDim(x, dim, F) collects each slice into a contiguous
15// scratch buffer and calls F(scratch.data(), n, out_index) → double.
16// `out_index` is the linear index into the output buffer, useful when
17// F needs to write multiple outputs per slice (e.g. mode wants both
18// value and count — but we expose that via a separate API).
19
20#pragma once
21
22#include <nk/ops/value_factory.hpp>
23
24#include <memory_resource>
25#include <nk/value/scratch.hpp>
26#include <nk/value/error.hpp>
27#include <nk/value/value.hpp>
28
29#include <algorithm>
30#include <cmath>
31#include <cstddef>
32#include <memory_resource>
33#include <stdexcept>
34#include <string>
35#include <vector>
36
37namespace nk::ops {
38
39// Return MATLAB's "first non-singleton dim" (1-based). For an all-
40// singleton input (scalar) returns 1. Used when the user omits `dim`.
41inline int firstNonSingletonDim(const Value &x)
42{
43 const auto &d = x.dims();
44 const int nd = d.ndim();
45 for (int i = 0; i < nd; ++i)
46 if (d.dim(i) > 1) return i + 1;
47 return 1;
48}
49
50// Validate user-supplied dim. dim must be a positive integer; dim
51// past the input's actual ndim is treated as a trailing-singleton
52// (returns sentinel = ndim+1 so callers know to do an identity copy).
53inline int validateDim(const Value &x, int dim, const char *fn)
54{
55 if (dim < 1)
56 throw Error(std::string(fn) + ": dim must be a positive integer",
57 0, 0, fn, "", std::string("nk:") + fn + ":badDim");
58 const int nd = x.dims().ndim();
59 if (dim > nd) {
60 // Trailing-singleton semantics — caller produces an identity copy.
61 return nd + 1;
62 }
63 return dim;
64}
65
66// Compute output shape for a reduce-along-dim operation. The reduced
67// dim becomes 1; other dims preserved.
68inline DimsArg outShapeForDim(const Value &x, int dim)
69{
70 const auto &d = x.dims();
71 DimsArg o{d.rows(), d.cols(), d.is3D() ? d.pages() : 0};
72 switch (dim) {
73 case 1: o.rows = 1; break;
74 case 2: o.cols = 1; break;
75 case 3: o.pages = (o.pages == 0) ? 0 : 1; break;
76 default: break; // sentinel: no reduction (handled by caller)
77 }
78 return o;
79}
80
81// Shape of one slice (count of elements traversed when collapsing dim).
82inline size_t sliceLenForDim(const Value &x, int dim)
83{
84 const auto &d = x.dims();
85 switch (dim) {
86 case 1: return d.rows();
87 case 2: return d.cols();
88 case 3: return d.is3D() ? d.pages() : 1;
89 default: return 1;
90 }
91}
92
93// Number of independent slices we'll produce.
94inline size_t sliceCountForDim(const Value &x, int dim)
95{
96 return x.numel() / sliceLenForDim(x, dim);
97}
98
99// Iterate every (output_index, slice_data) pair. F receives a pointer
100// to a contiguous buffer of length sliceLenForDim and writes outputs
101// via `outBuf[outIdx] = …`. Non-DOUBLE inputs are read via
102// elemAsDouble (DOUBLE keeps the contiguous-memcpy fast path).
103//
104// `mr` backs the per-call scratch buffer reused across slices. Pass
105// the same memory_resource you used for any sibling scratches in the
106// caller (typically `arena.resource()` from a per-call ScratchArena).
107//
108// NOTE: F is called once per output element. For multi-output ops
109// (like mode returning value+count) the caller must allocate two
110// MValues and capture both buffers in F's closure.
111template <typename F>
112void forEachSlice(std::pmr::memory_resource *mr,
113 const Value &x, int dim, F &&f)
114{
115 const auto &d = x.dims();
116 const size_t R = d.rows(), C = d.cols(), P = d.is3D() ? d.pages() : 1;
117 const size_t N = sliceLenForDim(x, dim);
118 const bool fastDouble = (x.type() == ValueType::DOUBLE);
119 const double *src = fastDouble ? x.doubleData() : nullptr;
120
121 ScratchVec<double> scratch(N, mr);
122
123 if (dim == 1) {
124 // Slice = a column (or column within a page). Stride = 1.
125 size_t outIdx = 0;
126 for (size_t pp = 0; pp < P; ++pp) {
127 for (size_t c = 0; c < C; ++c) {
128 const size_t base = pp * R * C + c * R;
129 if (fastDouble) {
130 std::copy(src + base, src + base + N, scratch.data());
131 } else {
132 for (size_t k = 0; k < N; ++k)
133 scratch[k] = x.elemAsDouble(base + k);
134 }
135 f(outIdx, scratch.data(), N);
136 ++outIdx;
137 }
138 }
139 } else if (dim == 2) {
140 // Slice = a row (within a page). Stride = R.
141 size_t outIdx = 0;
142 for (size_t pp = 0; pp < P; ++pp) {
143 for (size_t r = 0; r < R; ++r) {
144 if (fastDouble) {
145 for (size_t c = 0; c < C; ++c)
146 scratch[c] = src[pp * R * C + c * R + r];
147 } else {
148 for (size_t c = 0; c < C; ++c)
149 scratch[c] = x.elemAsDouble(pp * R * C + c * R + r);
150 }
151 f(outIdx, scratch.data(), N);
152 ++outIdx;
153 }
154 }
155 } else if (dim == 3) {
156 // Slice = pages at fixed (r,c). Stride = R*C. Only meaningful for 3D.
157 size_t outIdx = 0;
158 for (size_t c = 0; c < C; ++c) {
159 for (size_t r = 0; r < R; ++r) {
160 if (fastDouble) {
161 for (size_t pp = 0; pp < P; ++pp)
162 scratch[pp] = src[pp * R * C + c * R + r];
163 } else {
164 for (size_t pp = 0; pp < P; ++pp)
165 scratch[pp] = x.elemAsDouble(pp * R * C + c * R + r);
166 }
167 f(outIdx, scratch.data(), N);
168 ++outIdx;
169 }
170 }
171 }
172}
173
174// ND output-shape helper (any rank): copies all input dims, sets the
175// reduced dim to 1. dim is 1-based; dim > ndim means trailing
176// singleton (identity reduction) — caller handles separately. The
177// caller threads in `mr` (typically the same arena that backs the
178// reduction's per-slice scratch) so the shape buffer never hits the
179// global heap.
180inline ScratchVec<size_t> outShapeForDimND(std::pmr::memory_resource *mr,
181 const Value &x, int dim)
182{
183 const auto &d = x.dims();
184 ScratchVec<size_t> shape(d.ndim(), mr);
185 for (int i = 0; i < d.ndim(); ++i)
186 shape[i] = (i + 1 == dim) ? 1 : d.dim(i);
187 return shape;
188}
189
190// ND apply core: walks each axis-`redAxis` slice via stride arithmetic
191// and feeds it to F. Output is dense column-major matching the
192// reduced-dim shape. Output is always DOUBLE (matches MATLAB convention
193// for sum/mean/etc.); non-DOUBLE input is read via elemAsDouble so
194// integer / single / logical inputs reduce correctly.
195//
196// `mr` backs the per-call scratch buffer (see forEachSlice).
197template <typename F>
198void forEachSliceND(std::pmr::memory_resource *mr,
199 const Value &x, int dim, double *dst, F &&f)
200{
201 const auto &d = x.dims();
202 const int redAxis = dim - 1;
203 const size_t sliceLen = d.dim(redAxis);
204 size_t B = 1;
205 for (int i = 0; i < redAxis; ++i) B *= d.dim(i);
206 size_t O = 1;
207 for (int i = redAxis + 1; i < d.ndim(); ++i) O *= d.dim(i);
208
209 const bool fastDouble = (x.type() == ValueType::DOUBLE);
210 const double *src = fastDouble ? x.doubleData() : nullptr;
211 ScratchVec<double> scratch(sliceLen, mr);
212 if (B == 1) {
213 // Reducing axis 0 → contiguous gather.
214 if (fastDouble) {
215 for (size_t o = 0; o < O; ++o) {
216 const double *base = src + o * sliceLen;
217 std::copy(base, base + sliceLen, scratch.data());
218 dst[o] = f(o, scratch.data(), sliceLen);
219 }
220 } else {
221 for (size_t o = 0; o < O; ++o) {
222 const size_t base = o * sliceLen;
223 for (size_t k = 0; k < sliceLen; ++k)
224 scratch[k] = x.elemAsDouble(base + k);
225 dst[o] = f(o, scratch.data(), sliceLen);
226 }
227 }
228 return;
229 }
230 for (size_t o = 0; o < O; ++o) {
231 for (size_t b = 0; b < B; ++b) {
232 const size_t base = o * sliceLen * B + b;
233 if (fastDouble) {
234 for (size_t k = 0; k < sliceLen; ++k)
235 scratch[k] = src[base + k * B];
236 } else {
237 for (size_t k = 0; k < sliceLen; ++k)
238 scratch[k] = x.elemAsDouble(base + k * B);
239 }
240 const size_t outIdx = o * B + b;
241 dst[outIdx] = f(outIdx, scratch.data(), sliceLen);
242 }
243 }
244}
245
246// applyAlongDim: most common case — F maps a slice to a single double.
247// Returns an Value with the dim collapsed to 1. dim==-1 means "reduce
248// all elements to a single scalar" (vector/scalar case).
249template <typename F>
250Value applyAlongDim(const Value &x, int dim, F &&f, std::pmr::memory_resource *mr)
251{
252 // 2D/3D empty: collapse to 0×0 (legacy behaviour; MATLAB's
253 // shape-preservation for empty 2D inputs was never wired up here).
254 // ND empty (rank ≥ 4): fall through to the ND path, which produces
255 // a correctly-shaped output (e.g. sum(zeros([2,0,3,2]), 2) → 2×1×3×2).
256 if (x.isEmpty() && x.dims().ndim() < 4) {
257 return Value::matrix(0, 0, ValueType::DOUBLE, mr);
258 }
259 ScratchArena scratch(mr);
260 if (x.dims().isVector() || x.isScalar()) {
261 auto buf = ScratchVec<double>(x.numel(), &scratch);
262 if (x.type() == ValueType::DOUBLE)
263 std::copy(x.doubleData(), x.doubleData() + x.numel(), buf.data());
264 else
265 for (size_t i = 0; i < x.numel(); ++i)
266 buf[i] = x.elemAsDouble(i);
267 return Value::scalar(f(0, buf.data(), x.numel()), mr);
268 }
269 // ND fallback for rank ≥ 4. dim out-of-range was already mapped by
270 // validateDim → ndim+1 sentinel, but we may receive dim ∈ [1, ndim].
271 if (x.dims().ndim() >= 4 && dim >= 1 && dim <= x.dims().ndim()) {
272 auto shape = outShapeForDimND(&scratch, x, dim);
273 Value out = Value::matrixND(shape.data(),
274 static_cast<int>(shape.size()),
275 ValueType::DOUBLE, mr);
276 double *dst = out.doubleDataMut();
277 forEachSliceND(&scratch, x, dim, dst, std::forward<F>(f));
278 return out;
279 }
280 auto outShape = outShapeForDim(x, dim);
281 Value out = createMatrix(outShape, ValueType::DOUBLE, mr);
282 double *dst = out.doubleDataMut();
283 forEachSlice(&scratch, x, dim, [&](size_t outIdx, double *slice, size_t n) {
284 dst[outIdx] = f(outIdx, slice, n);
285 });
286 return out;
287}
288
289// Index-aware variant: F writes (value, 1-based index of source element)
290// per slice. Used by min/max with dim. Returns (values, indices).
291template <typename F>
292std::pair<Value, Value>
293applyAlongDimWithIndex(const Value &x, int dim, F &&f, std::pmr::memory_resource *mr)
294{
295 if (x.isEmpty() && x.dims().ndim() < 4) {
296 return {Value::matrix(0, 0, ValueType::DOUBLE, mr),
297 Value::matrix(0, 0, ValueType::DOUBLE, mr)};
298 }
299 ScratchArena scratch(mr);
300 if (x.dims().isVector() || x.isScalar()) {
301 auto buf = ScratchVec<double>(x.numel(), &scratch);
302 if (x.type() == ValueType::DOUBLE)
303 std::copy(x.doubleData(), x.doubleData() + x.numel(), buf.data());
304 else
305 for (size_t i = 0; i < x.numel(); ++i)
306 buf[i] = x.elemAsDouble(i);
307 double v = 0;
308 size_t idx = 0;
309 f(buf.data(), x.numel(), v, idx);
310 return {Value::scalar(v, mr),
311 Value::scalar(static_cast<double>(idx + 1), mr)};
312 }
313 // ND fallback (rank ≥ 4)
314 if (x.dims().ndim() >= 4 && dim >= 1 && dim <= x.dims().ndim()) {
315 auto shape = outShapeForDimND(&scratch, x, dim);
316 Value out = Value::matrixND(shape.data(),
317 static_cast<int>(shape.size()),
318 ValueType::DOUBLE, mr);
319 Value outIdx = Value::matrixND(shape.data(),
320 static_cast<int>(shape.size()),
321 ValueType::DOUBLE, mr);
322 double *dst = out.doubleDataMut();
323 double *dstI = outIdx.doubleDataMut();
324 forEachSliceND(&scratch, x, dim, dst,
325 [&](size_t outOff, double *slice, size_t n) {
326 double v = 0; size_t idx = 0;
327 f(slice, n, v, idx);
328 dstI[outOff] = static_cast<double>(idx + 1);
329 return v;
330 });
331 return {std::move(out), std::move(outIdx)};
332 }
333 auto outShape = outShapeForDim(x, dim);
334 Value out = createMatrix(outShape, ValueType::DOUBLE, mr);
335 Value outIdx = createMatrix(outShape, ValueType::DOUBLE, mr);
336 double *dst = out.doubleDataMut();
337 double *dstI = outIdx.doubleDataMut();
338 forEachSlice(&scratch, x, dim, [&](size_t outOff, double *slice, size_t n) {
339 double v = 0;
340 size_t idx = 0;
341 f(slice, n, v, idx);
342 dst[outOff] = v;
343 dstI[outOff] = static_cast<double>(idx + 1);
344 });
345 return {std::move(out), std::move(outIdx)};
346}
347
348// Resolve effective dim: explicit user-supplied if provided, else the
349// "first non-singleton" rule. Returns 1-based dim or 0 for "scalar".
350inline int resolveDim(const Value &x, int explicitDim, const char *fn)
351{
352 if (explicitDim > 0) return validateDim(x, explicitDim, fn);
353 return firstNonSingletonDim(x);
354}
355
356// Compact NaN-free elements to the front of [data, data+n). Returns the
357// new count k (0 <= k <= n). Used by nan* reductions: after compacting
358// they call the regular reduction kernel on [data, data+k) and decide
359// what to return when k == 0 (which depends on the function — nansum
360// returns 0, nanmean/nanvar/nanstd/nanmedian/nanmax/nanmin return NaN).
361inline size_t compactNonNan(double *data, size_t n)
362{
363 size_t w = 0;
364 for (size_t i = 0; i < n; ++i) {
365 if (!std::isnan(data[i])) {
366 if (w != i) data[w] = data[i];
367 ++w;
368 }
369 }
370 return w;
371}
372
373} // namespace nk::ops
Value dst(const Value &x, std::pmr::memory_resource *mr=nullptr)
Type-II Discrete Sine Transform.