nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
helpers.hpp
1#pragma once
2
3// NOTE: helpers.hpp is engine-free — it uses only the Value substrate + ops
4// factories, never Engine/CallContext. Keeping it off <nk/core/engine.hpp>
5// is what lets toolbox compute files include it without dragging in the engine.
6#include <nk/value/value.hpp>
7#include <nk/ops/value_factory.hpp> // DimsArg/createMatrix/createForDims/createLike (now in nk::ops)
8#include <nk/value/scratch.hpp>
9#include <nk/value/shape_ops.hpp>
10
11#include <algorithm>
12#include <cmath>
13#include <complex>
14#include <limits>
15#include <memory_resource>
16#include <stdexcept>
17#include <type_traits>
18#include <utility>
19
20namespace nk {
21
22// ============================================================
23// Helper: promote pair to complex if needed
24// ============================================================
25inline std::pair<Value, Value> promoteToComplex(const Value &a, const Value &b, std::pmr::memory_resource *mr)
26{
27 Value ca = a, cb = b;
28 if (a.isComplex() && !b.isComplex())
29 cb.promoteToComplex(mr);
30 else if (!a.isComplex() && b.isComplex())
31 ca.promoteToComplex(mr);
32 return {ca, cb};
33}
34
35// ============================================================
36// Implicit expansion (broadcasting) check
37// Returns true if dims are compatible, sets outR/outC to result size.
38// Rule: each dim must match or one of them must be 1.
39// ============================================================
40inline bool broadcastDims(size_t ar, size_t ac, size_t br, size_t bc,
41 size_t &outR, size_t &outC)
42{
43 if (ar != br && ar != 1 && br != 1) return false;
44 if (ac != bc && ac != 1 && bc != 1) return false;
45 outR = std::max(ar, br);
46 outC = std::max(ac, bc);
47 return true;
48}
49
50// 3D variant — each axis must match or be 1. A 2D operand is treated as
51// having pages = 1 (caller passes ap = 1 for 2D inputs).
52inline bool broadcastDims3D(size_t ar, size_t ac, size_t ap,
53 size_t br, size_t bc, size_t bp,
54 size_t &outR, size_t &outC, size_t &outP)
55{
56 if (ar != br && ar != 1 && br != 1) return false;
57 if (ac != bc && ac != 1 && bc != 1) return false;
58 if (ap != bp && ap != 1 && bp != 1) return false;
59 outR = std::max(ar, br);
60 outC = std::max(ac, bc);
61 outP = std::max(ap, bp);
62 return true;
63}
64
65// Linearise (r, c, p) into a column-major + page-stride buffer offset,
66// applying broadcast (an axis of 1 collapses its index to 0). Used by
67// the 3D broadcast loops in elementwise{Double,Complex} and compareImpl.
68inline size_t broadcastOffset3D(size_t r, size_t c, size_t p,
69 size_t aR, size_t aC, size_t aP)
70{
71 const size_t rr = (aR == 1) ? 0 : r;
72 const size_t cc = (aC == 1) ? 0 : c;
73 const size_t pp = (aP == 1) ? 0 : p;
74 (void)aP; // strides are R*C per page regardless
75 return pp * aR * aC + cc * aR + rr;
76}
77
78// ============================================================
79// Shape helpers (must precede the elementwise templates below —
80// template bodies reference these as non-dependent names and the
81// two-phase lookup rule requires them to be visible at template
82// definition time, not at instantiation).
83// ============================================================
84
85// The Value-shape factories moved to the L0.5 ops layer
86// (<nk/ops/value_factory.hpp>). Re-export them into nk:: so the ~90
87// existing unqualified `createMatrix` / `createForDims` / `createLike` / DimsArg
88// call sites (here and in the toolboxes) are unchanged. ops owns the one
89// definition; toolboxes going pure will include ops/value_factory.hpp directly.
90using ops::DimsArg;
91using ops::createMatrix;
92using ops::createForDims;
93using ops::createLike;
94
95// abs() for an integer-typed Value: MATLAB keeps the integer class and
96// SATURATES, so abs(intmin) -> intmax (abs(int8(-128))=127 int8); unsigned
97// types are returned unchanged. Reading via elemAsDouble + clamp to the
98// type range handles every signed/unsigned width uniformly (|intmin| would
99// overflow the type, the clamp catches it). Caller guards isIntegerType().
100inline Value absIntegerSaturate(const Value &x, std::pmr::memory_resource *mr)
101{
102 Value r = createLike(x, x.type(), mr);
103 const size_t n = x.numel();
104 auto fill = [&](auto *dst) {
105 using T = std::remove_pointer_t<std::decay_t<decltype(dst)>>;
106 const double lo = static_cast<double>(std::numeric_limits<T>::min());
107 const double hi = static_cast<double>(std::numeric_limits<T>::max());
108 for (size_t i = 0; i < n; ++i) {
109 double v = std::fabs(x.elemAsDouble(i));
110 if (v < lo) v = lo;
111 else if (v > hi) v = hi;
112 dst[i] = static_cast<T>(v);
113 }
114 };
115 switch (x.type()) {
116 case ValueType::INT8: fill(r.int8DataMut()); break;
117 case ValueType::INT16: fill(r.int16DataMut()); break;
118 case ValueType::INT32: fill(r.int32DataMut()); break;
119 case ValueType::INT64: fill(r.int64DataMut()); break;
120 case ValueType::UINT8: fill(r.uint8DataMut()); break;
121 case ValueType::UINT16: fill(r.uint16DataMut()); break;
122 case ValueType::UINT32: fill(r.uint32DataMut()); break;
123 case ValueType::UINT64: fill(r.uint64DataMut()); break;
124 default: break;
125 }
126 return r;
127}
128
129// Cast a numeric Value to an integer class by per-element static_cast with
130// saturation. Used to restore the integer class after computing through the
131// double path (e.g. sort of an integer array; mod/rem of integers). Values
132// that are already exact in-range integers round-trip exactly. Reads via
133// elemAsDouble so a scalar-stored result is handled too. `vt` must be an
134// integer ValueType.
135inline Value doubleToIntegerExact(const Value &d, ValueType vt,
136 std::pmr::memory_resource *mr)
137{
138 Value r = createForDims(d.dims(), vt, mr);
139 const size_t n = d.numel();
140 auto fill = [&](auto *dst) {
141 using T = std::remove_pointer_t<std::decay_t<decltype(dst)>>;
142 const double lo = static_cast<double>(std::numeric_limits<T>::min());
143 const double hi = static_cast<double>(std::numeric_limits<T>::max());
144 for (size_t i = 0; i < n; ++i) {
145 double v = d.elemAsDouble(i);
146 if (v < lo) v = lo;
147 else if (v > hi) v = hi;
148 dst[i] = static_cast<T>(v);
149 }
150 };
151 switch (vt) {
152 case ValueType::INT8: fill(r.int8DataMut()); break;
153 case ValueType::INT16: fill(r.int16DataMut()); break;
154 case ValueType::INT32: fill(r.int32DataMut()); break;
155 case ValueType::INT64: fill(r.int64DataMut()); break;
156 case ValueType::UINT8: fill(r.uint8DataMut()); break;
157 case ValueType::UINT16: fill(r.uint16DataMut()); break;
158 case ValueType::UINT32: fill(r.uint32DataMut()); break;
159 case ValueType::UINT64: fill(r.uint64DataMut()); break;
160 default: break;
161 }
162 return r;
163}
164
165// Convert any numeric Value to a DOUBLE Value (element-wise via elemAsDouble,
166// so integer arrays — which lack a doubleData() buffer — are handled). A
167// DOUBLE input is returned as-is (shares storage). Used to route integer
168// operands through double-only elementwise helpers, then cast the result back.
169inline Value toDoubleValue(const Value &x, std::pmr::memory_resource *mr)
170{
171 if (x.type() == ValueType::DOUBLE) return x;
172 Value r = createForDims(x.dims(), ValueType::DOUBLE, mr);
173 double *dst = r.doubleDataMut();
174 const size_t n = x.numel();
175 for (size_t i = 0; i < n; ++i) dst[i] = x.elemAsDouble(i);
176 return r;
177}
178
179// Exact same-class copy of an integer-typed Value (raw typed copy, so int64
180// values above 2^53 are preserved). Used for operations that are the
181// IDENTITY on integers but keep the class (floor/ceil/round/fix in MATLAB).
182// Caller guards isIntegerType().
183inline Value copyIntegerSameClass(const Value &x, std::pmr::memory_resource *mr)
184{
185 Value r = createLike(x, x.type(), mr);
186 const size_t n = x.numel();
187 switch (x.type()) {
188 case ValueType::INT8: std::copy(x.int8Data(), x.int8Data() + n, r.int8DataMut()); break;
189 case ValueType::INT16: std::copy(x.int16Data(), x.int16Data() + n, r.int16DataMut()); break;
190 case ValueType::INT32: std::copy(x.int32Data(), x.int32Data() + n, r.int32DataMut()); break;
191 case ValueType::INT64: std::copy(x.int64Data(), x.int64Data() + n, r.int64DataMut()); break;
192 case ValueType::UINT8: std::copy(x.uint8Data(), x.uint8Data() + n, r.uint8DataMut()); break;
193 case ValueType::UINT16: std::copy(x.uint16Data(), x.uint16Data() + n, r.uint16DataMut()); break;
194 case ValueType::UINT32: std::copy(x.uint32Data(), x.uint32Data() + n, r.uint32DataMut()); break;
195 case ValueType::UINT64: std::copy(x.uint64Data(), x.uint64Data() + n, r.uint64DataMut()); break;
196 default: break;
197 }
198 return r;
199}
200
201// Shape-preserving empty result for a binary op where at least one
202// operand is empty. The non-scalar operand contributes the output
203// shape; if both are non-scalar the dims must match, otherwise throw.
204// `outType` is chosen by the caller based on its promotion rules.
205inline Value emptyResultForBinary(const Value &a, const Value &b,
206 ValueType outType, std::pmr::memory_resource *mr)
207{
208 const bool aShaper = !a.isScalar();
209 const bool bShaper = !b.isScalar();
210 if (aShaper && bShaper && a.dims() != b.dims())
211 throw std::runtime_error("Matrix dimensions must agree. a=[" + std::to_string(a.dims().rows()) + "x" + std::to_string(a.dims().cols()) + "], b=[" + std::to_string(b.dims().rows()) + "x" + std::to_string(b.dims().cols()) + "] (line 211)");
212 const Value &shape = aShaper ? a : (bShaper ? b : a);
213 return createLike(shape, outType, mr);
214}
215
216// Pick the arithmetic output type for a pair of operands using
217// MATLAB-style promotion: complex > single > integer > double.
218// Char and logical operands promote to double.
219inline ValueType arithOutType(const Value &a, const Value &b)
220{
221 if (a.isComplex() || b.isComplex()) return ValueType::COMPLEX;
222 if (a.type() == ValueType::SINGLE || b.type() == ValueType::SINGLE) return ValueType::SINGLE;
223 if (isIntegerType(a.type())) return a.type();
224 if (isIntegerType(b.type())) return b.type();
225 return ValueType::DOUBLE;
226}
227
228// Convenience wrapper: shape-preserving empty arithmetic result with
229// type chosen by arithOutType().
230inline Value emptyArithResult(const Value &a, const Value &b, std::pmr::memory_resource *mr)
231{
232 return emptyResultForBinary(a, b, arithOutType(a, b), mr);
233}
234
235// ============================================================
236// Elementwise binary op on double arrays (with implicit expansion)
237// ============================================================
238template<typename Op>
239Value elementwiseDouble(const Value &a, const Value &b, Op op, std::pmr::memory_resource *mr)
240{
241 if (a.isEmpty() || b.isEmpty())
242 return emptyResultForBinary(a, b, ValueType::DOUBLE, mr);
243 if (a.isScalar() && b.isScalar())
244 return Value::scalar(op(a.toScalar(), b.toScalar()), mr);
245
246 // ND fallback — at least one operand has rank ≥ 4, or NumPy-style
247 // broadcasting needed past 3D. Handles same-shape (fast path) and
248 // arbitrary-rank broadcast via per-operand offset arithmetic.
249 if (a.dims().ndim() >= 4 || b.dims().ndim() >= 4) {
250 if (a.isScalar()) {
251 auto r = createLike(b, ValueType::DOUBLE, mr);
252 const double s = a.toScalar();
253 const double *db = b.doubleData();
254 double *dst = r.doubleDataMut();
255 for (size_t i = 0; i < b.numel(); ++i) dst[i] = op(s, db[i]);
256 return r;
257 }
258 if (b.isScalar()) {
259 auto r = createLike(a, ValueType::DOUBLE, mr);
260 const double s = b.toScalar();
261 const double *da = a.doubleData();
262 double *dst = r.doubleDataMut();
263 for (size_t i = 0; i < a.numel(); ++i) dst[i] = op(da[i], s);
264 return r;
265 }
266 // ND broadcast (NumPy: align right, dims match or 1)
267 Dims outD;
268 if (!broadcastDimsND(a.dims(), b.dims(), outD))
269 throw std::runtime_error("ND dimensions must broadcast: each axis must match or be 1");
270 auto r = createForDims(outD, ValueType::DOUBLE, mr);
271 const double *da = a.doubleData(), *db = b.doubleData();
272 double *dst = r.doubleDataMut();
273 forEachNDPair(a.dims(), b.dims(), outD,
274 [&](size_t outIdx, size_t aOff, size_t bOff) {
275 dst[outIdx] = op(da[aOff], db[bOff]);
276 });
277 return r;
278 }
279
280 // 3D paths — same-shape, scalar, or general 3D broadcasting (each
281 // axis must equal or be 1; 2D operands implicitly have pages = 1).
282 if (a.dims().is3D() || b.dims().is3D()) {
283 if (a.isScalar()) {
284 auto r = createLike(b, ValueType::DOUBLE, mr);
285 double s = a.toScalar();
286 for (size_t i = 0; i < b.numel(); ++i)
287 r.doubleDataMut()[i] = op(s, b.doubleData()[i]);
288 return r;
289 }
290 if (b.isScalar()) {
291 auto r = createLike(a, ValueType::DOUBLE, mr);
292 double s = b.toScalar();
293 for (size_t i = 0; i < a.numel(); ++i)
294 r.doubleDataMut()[i] = op(a.doubleData()[i], s);
295 return r;
296 }
297 const size_t aR = a.dims().rows(), aC = a.dims().cols();
298 const size_t aP = a.dims().is3D() ? a.dims().pages() : 1;
299 const size_t bR = b.dims().rows(), bC = b.dims().cols();
300 const size_t bP = b.dims().is3D() ? b.dims().pages() : 1;
301 size_t outR, outC, outP;
302 if (!broadcastDims3D(aR, aC, aP, bR, bC, bP, outR, outC, outP))
303 throw std::runtime_error(
304 "3D dimensions must broadcast: each axis must match or be 1");
305
306 // Same-shape fast path skips the per-axis broadcast index math.
307 if (aR == bR && aC == bC && aP == bP) {
308 auto r = (outP > 1) ? Value::matrix3d(outR, outC, outP, ValueType::DOUBLE, mr)
309 : Value::matrix(outR, outC, ValueType::DOUBLE, mr);
310 for (size_t i = 0; i < a.numel(); ++i)
311 r.doubleDataMut()[i] = op(a.doubleData()[i], b.doubleData()[i]);
312 return r;
313 }
314 auto r = (outP > 1) ? Value::matrix3d(outR, outC, outP, ValueType::DOUBLE, mr)
315 : Value::matrix(outR, outC, ValueType::DOUBLE, mr);
316 double *dst = r.doubleDataMut();
317 const double *da = a.doubleData(), *db = b.doubleData();
318 for (size_t pp = 0; pp < outP; ++pp)
319 for (size_t cc = 0; cc < outC; ++cc)
320 for (size_t rr = 0; rr < outR; ++rr) {
321 const size_t aOff = broadcastOffset3D(rr, cc, pp, aR, aC, aP);
322 const size_t bOff = broadcastOffset3D(rr, cc, pp, bR, bC, bP);
323 dst[pp * outR * outC + cc * outR + rr] = op(da[aOff], db[bOff]);
324 }
325 return r;
326 }
327
328 size_t ar = a.dims().rows(), ac = a.dims().cols();
329 size_t br = b.dims().rows(), bc = b.dims().cols();
330 size_t outR, outC;
331 if (!broadcastDims(ar, ac, br, bc, outR, outC))
332 throw std::runtime_error("Matrix dimensions must agree. a=[" + std::to_string(ar) + "x" + std::to_string(ac) + "], b=[" + std::to_string(br) + "x" + std::to_string(bc) + "] (line 332)");
333
334 // Fast path: same dimensions, no broadcasting needed
335 if (ar == br && ac == bc) {
336 auto r = Value::matrix(outR, outC, ValueType::DOUBLE, mr);
337 for (size_t i = 0; i < a.numel(); ++i)
338 r.doubleDataMut()[i] = op(a.doubleData()[i], b.doubleData()[i]);
339 return r;
340 }
341
342 // General broadcasting: column-major indexing
343 auto r = Value::matrix(outR, outC, ValueType::DOUBLE, mr);
344 double *dst = r.doubleDataMut();
345 const double *da = a.doubleData(), *db = b.doubleData();
346 for (size_t c = 0; c < outC; ++c) {
347 size_t ca = (ac == 1) ? 0 : c;
348 size_t cb = (bc == 1) ? 0 : c;
349 for (size_t row = 0; row < outR; ++row) {
350 size_t ra = (ar == 1) ? 0 : row;
351 size_t rb = (br == 1) ? 0 : row;
352 dst[c * outR + row] = op(da[ca * ar + ra], db[cb * br + rb]);
353 }
354 }
355 return r;
356}
357
358// narrowComplex (all-real complex -> real, MATLAB) now lives in the value layer
359// (nk/value/value.hpp) so value-layer indexing/assignment can use it too.
360
361// ============================================================
362// Elementwise binary op on complex arrays. The public `elementwiseComplex`
363// narrows an all-real result to real (MATLAB complex-arithmetic semantics); the
364// *Impl below does the raw complex computation.
365// ============================================================
366template<typename Op>
367Value elementwiseComplexImpl(const Value &a, const Value &b, Op op, std::pmr::memory_resource *mr)
368{
369 if (a.isEmpty() || b.isEmpty())
370 return emptyResultForBinary(a, b, ValueType::COMPLEX, mr);
371 auto [ca, cb] = promoteToComplex(a, b, mr);
372 if (ca.isScalar() && cb.isScalar())
373 return Value::complexScalar(op(ca.toComplex(), cb.toComplex()), mr);
374
375 // ND fallback (rank ≥ 4 or NumPy-style broadcast past 3D)
376 if (ca.dims().ndim() >= 4 || cb.dims().ndim() >= 4) {
377 if (ca.isScalar()) {
378 auto r = createLike(cb, ValueType::COMPLEX, mr);
379 Complex s = ca.toComplex();
380 const Complex *db = cb.complexData();
381 Complex *dst = r.complexDataMut();
382 for (size_t i = 0; i < cb.numel(); ++i) dst[i] = op(s, db[i]);
383 return r;
384 }
385 if (cb.isScalar()) {
386 auto r = createLike(ca, ValueType::COMPLEX, mr);
387 Complex s = cb.toComplex();
388 const Complex *da = ca.complexData();
389 Complex *dst = r.complexDataMut();
390 for (size_t i = 0; i < ca.numel(); ++i) dst[i] = op(da[i], s);
391 return r;
392 }
393 Dims outD;
394 if (!broadcastDimsND(ca.dims(), cb.dims(), outD))
395 throw std::runtime_error("ND dimensions must broadcast: each axis must match or be 1");
396 auto r = createForDims(outD, ValueType::COMPLEX, mr);
397 const Complex *da = ca.complexData(), *db = cb.complexData();
398 Complex *dst = r.complexDataMut();
399 forEachNDPair(ca.dims(), cb.dims(), outD,
400 [&](size_t outIdx, size_t aOff, size_t bOff) {
401 dst[outIdx] = op(da[aOff], db[bOff]);
402 });
403 return r;
404 }
405
406 if (ca.dims().is3D() || cb.dims().is3D()) {
407 if (ca.isScalar()) {
408 auto r = createLike(cb, ValueType::COMPLEX, mr);
409 Complex s = ca.toComplex();
410 for (size_t i = 0; i < cb.numel(); ++i)
411 r.complexDataMut()[i] = op(s, cb.complexData()[i]);
412 return r;
413 }
414 if (cb.isScalar()) {
415 auto r = createLike(ca, ValueType::COMPLEX, mr);
416 Complex s = cb.toComplex();
417 for (size_t i = 0; i < ca.numel(); ++i)
418 r.complexDataMut()[i] = op(ca.complexData()[i], s);
419 return r;
420 }
421 const size_t aR = ca.dims().rows(), aC = ca.dims().cols();
422 const size_t aP = ca.dims().is3D() ? ca.dims().pages() : 1;
423 const size_t bR = cb.dims().rows(), bC = cb.dims().cols();
424 const size_t bP = cb.dims().is3D() ? cb.dims().pages() : 1;
425 size_t outR, outC, outP;
426 if (!broadcastDims3D(aR, aC, aP, bR, bC, bP, outR, outC, outP))
427 throw std::runtime_error(
428 "3D dimensions must broadcast: each axis must match or be 1");
429
430 if (aR == bR && aC == bC && aP == bP) {
431 auto r = createLike(ca, ValueType::COMPLEX, mr);
432 for (size_t i = 0; i < ca.numel(); ++i)
433 r.complexDataMut()[i] = op(ca.complexData()[i], cb.complexData()[i]);
434 return r;
435 }
436 auto r = (outP > 1) ? Value::matrix3d(outR, outC, outP, ValueType::COMPLEX, mr)
437 : Value::matrix(outR, outC, ValueType::COMPLEX, mr);
438 Complex *dst = r.complexDataMut();
439 const Complex *da = ca.complexData(), *db = cb.complexData();
440 for (size_t pp = 0; pp < outP; ++pp)
441 for (size_t cc = 0; cc < outC; ++cc)
442 for (size_t rr = 0; rr < outR; ++rr) {
443 const size_t aOff = broadcastOffset3D(rr, cc, pp, aR, aC, aP);
444 const size_t bOff = broadcastOffset3D(rr, cc, pp, bR, bC, bP);
445 dst[pp * outR * outC + cc * outR + rr] = op(da[aOff], db[bOff]);
446 }
447 return r;
448 }
449
450 size_t ar = ca.dims().rows(), ac = ca.dims().cols();
451 size_t br = cb.dims().rows(), bc = cb.dims().cols();
452 size_t outR, outC;
453 if (!broadcastDims(ar, ac, br, bc, outR, outC))
454 throw std::runtime_error("Matrix dimensions must agree. a=[" + std::to_string(ar) + "x" + std::to_string(ac) + "], b=[" + std::to_string(br) + "x" + std::to_string(bc) + "] (line 454)");
455
456 if (ar == br && ac == bc) {
457 auto r = Value::complexMatrix(outR, outC, mr);
458 for (size_t i = 0; i < ca.numel(); ++i)
459 r.complexDataMut()[i] = op(ca.complexData()[i], cb.complexData()[i]);
460 return r;
461 }
462
463 auto r = Value::complexMatrix(outR, outC, mr);
464 Complex *dst = r.complexDataMut();
465 const Complex *da = ca.complexData(), *db = cb.complexData();
466 for (size_t c = 0; c < outC; ++c) {
467 size_t cca = (ac == 1) ? 0 : c;
468 size_t ccb = (bc == 1) ? 0 : c;
469 for (size_t row = 0; row < outR; ++row) {
470 size_t ra = (ar == 1) ? 0 : row;
471 size_t rb = (br == 1) ? 0 : row;
472 dst[c * outR + row] = op(da[cca * ar + ra], db[ccb * br + rb]);
473 }
474 }
475 return r;
476}
477
478// Public entry: raw complex elementwise op, then narrow an all-real result to
479// real (MATLAB). All callers (binary +,-,.*,./,.^, max/min) want this.
480template<typename Op>
481Value elementwiseComplex(const Value &a, const Value &b, Op op, std::pmr::memory_resource *mr)
482{
483 return narrowComplex(elementwiseComplexImpl(a, b, std::move(op), mr), mr);
484}
485
486// ============================================================
487// Elementwise unary on double
488// ============================================================
489template<typename Op>
490Value unaryDouble(const Value &a, Op op, std::pmr::memory_resource *mr)
491{
492 if (a.isScalar())
493 return Value::scalar(op(a.toScalar()), mr);
494 auto r = createLike(a, ValueType::DOUBLE, mr);
495 for (size_t i = 0; i < a.numel(); ++i)
496 r.doubleDataMut()[i] = op(a.doubleData()[i]);
497 return r;
498}
499
500// ============================================================
501// Elementwise unary on complex. Public name narrows an all-real result to real
502// (MATLAB: uminus/uplus/conj and unary math of an all-real complex give real).
503// ============================================================
504template<typename Op>
505Value unaryComplexImpl(const Value &a, Op op, std::pmr::memory_resource *mr)
506{
507 if (a.isScalar())
508 return Value::complexScalar(op(a.toComplex()), mr);
509 auto r = createLike(a, ValueType::COMPLEX, mr);
510 for (size_t i = 0; i < a.numel(); ++i)
511 r.complexDataMut()[i] = op(a.complexData()[i]);
512 return r;
513}
514template<typename Op>
515Value unaryComplex(const Value &a, Op op, std::pmr::memory_resource *mr)
516{
517 return narrowComplex(unaryComplexImpl(a, std::move(op), mr), mr);
518}
519
520// ============================================================
521// Parse dimension arguments for array creation functions
522// Supports: f(n), f(m,n), f(m,n,p), f([m n]), f([m n p]), f(size(x))
523// Returns {rows, cols, pages}. pages=0 means 2D.
524// ============================================================
525
526// Convert one (double) size argument to a dimension length, matching MATLAB:
527// * a non-integer or non-finite size is an error ("Size inputs must be
528// integers." — what MATLAB reports);
529// * a negative size clamps to 0 (so zeros(-1, 3) is 0x3, an empty array);
530// * a size larger than size_t can hold is an error (MATLAB errors too).
531// Every guard PRECEDES the cast, so a negative, non-finite, or out-of-range
532// value never reaches the float->unsigned conversion (which would be UB for
533// any of those).
534inline size_t toDim(double d)
535{
536 if (!std::isfinite(d) || d != std::floor(d))
537 throw std::runtime_error("Size inputs must be integers.");
538 if (d < 0.0)
539 return size_t{0};
540 if (d >= static_cast<double>(std::numeric_limits<size_t>::max()))
541 throw std::runtime_error("Requested array size is too large.");
542 return static_cast<size_t>(d);
543}
544
545inline DimsArg parseDimsArgs(Span<const Value> args)
546{
547 if (args.empty())
548 return {1, 1, 0};
549
550 // Single vector argument: [m n] or [m n p]
551 if (args.size() == 1 && !args[0].isScalar() && args[0].numel() >= 2) {
552 const double *d = args[0].doubleData();
553 size_t n = args[0].numel();
554 size_t r = toDim(d[0]);
555 size_t c = toDim(d[1]);
556 size_t p = (n >= 3) ? toDim(d[2]) : 0;
557 return {r, c, p};
558 }
559
560 // Single scalar: f(n) → n×n
561 if (args.size() == 1) {
562 size_t n = toDim(args[0].toScalar());
563 return {n, n, 0};
564 }
565
566 // Two scalars: f(m, n)
567 size_t r = toDim(args[0].toScalar());
568 size_t c = toDim(args[1].toScalar());
569
570 // Three scalars: f(m, n, p)
571 if (args.size() >= 3) {
572 size_t p = toDim(args[2].toScalar());
573 return {r, c, p};
574 }
575
576 return {r, c, 0};
577}
578
579// ND dim parser. Same call shapes as parseDimsArgs but produces a full
580// ND dim vector (no 3-cap). Returns at least 2 dims (matrices stay 2D).
581// * f(n) → {n, n}
582// * f(m, n, p, ...) → {m, n, p, ...}
583// * f([m n]) → {m, n}
584// * f([m n p q ...]) → {m, n, p, q, ...}
585// Caller can decide whether to use the 2D / 3D fast paths or matrixND.
586//
587// The returned vector is backed by `mr` — typical use is a per-call
588// ScratchArena for the array-creation builtin (zeros/ones/rand/...).
589inline ScratchVec<size_t> parseDimsArgsND(std::pmr::memory_resource *mr,
591{
592 ScratchVec<size_t> out(mr);
593 if (args.empty()) {
594 out.assign({1, 1});
595 return out;
596 }
597 // Vector form: f([m n p ...])
598 if (args.size() == 1 && !args[0].isScalar() && args[0].numel() >= 2) {
599 const double *d = args[0].doubleData();
600 const size_t n = args[0].numel();
601 out.reserve(n);
602 for (size_t i = 0; i < n; ++i)
603 out.push_back(toDim(d[i]));
604 return out;
605 }
606 // Single scalar: f(n) → n×n
607 if (args.size() == 1) {
608 const size_t n = toDim(args[0].toScalar());
609 out.assign({n, n});
610 return out;
611 }
612 // Multiple scalars: f(m, n, p, ...)
613 out.reserve(args.size());
614 for (const auto &a : args)
615 out.push_back(toDim(a.toScalar()));
616 return out;
617}
618
619// Strip trailing 1s past the 2nd dim (MATLAB convention: ones(3,4,1)
620// returns a 2D matrix, not 3D). Keeps at least 2 dims so a vector stays
621// {1, n} rather than collapsing to scalar shape.
622inline void stripTrailingOnes(ScratchVec<size_t> &dims)
623{
624 while (dims.size() > 2 && dims.back() == 1)
625 dims.pop_back();
626}
627
628// Map a MATLAB class-name string to a ValueType. Returns true on
629// recognised names (the standard numeric / logical types accepted by
630// zeros/ones/true/false/rand). Unknown names return false (caller can
631// throw or fall through to default DOUBLE).
632inline bool valueTypeFromName(std::string_view name, ValueType &out)
633{
634 if (name == "double") { out = ValueType::DOUBLE; return true; }
635 else if (name == "single") { out = ValueType::SINGLE; return true; }
636 else if (name == "int8") { out = ValueType::INT8; return true; }
637 else if (name == "int16") { out = ValueType::INT16; return true; }
638 else if (name == "int32") { out = ValueType::INT32; return true; }
639 else if (name == "int64") { out = ValueType::INT64; return true; }
640 else if (name == "uint8") { out = ValueType::UINT8; return true; }
641 else if (name == "uint16") { out = ValueType::UINT16; return true; }
642 else if (name == "uint32") { out = ValueType::UINT32; return true; }
643 else if (name == "uint64") { out = ValueType::UINT64; return true; }
644 else if (name == "logical") { out = ValueType::LOGICAL; return true; }
645 return false;
646}
647
648// Detect a trailing class-name argument in zeros/ones/etc. If args.back()
649// is a char/string with a recognised type name, return the matching
650// ValueType and trim args by one. Otherwise return DOUBLE (default) and
651// leave args unchanged. Also handles the 'like' form -- (..., 'like', X)
652// pulls the type from X.
653//
654// Sets `outType` and modifies `args` (Span is value-copied at callsite).
656extractTypeArg(Span<const Value> args, ValueType &outType)
657{
658 outType = ValueType::DOUBLE;
659 if (args.empty()) return args;
660
661 // 'like' form: zeros(..., 'like', X). Need at least 2 trailing args.
662 if (args.size() >= 2) {
663 const Value &maybeLike = args[args.size() - 2];
664 if ((maybeLike.isChar() || maybeLike.isString())
665 && maybeLike.toString() == "like") {
666 outType = args[args.size() - 1].type();
667 return Span<const Value>(args.data(), args.size() - 2);
668 }
669 }
670
671 // Trailing class-name string: zeros(M, N, ..., 'uint8').
672 const Value &last = args[args.size() - 1];
673 if (last.isChar() || last.isString()) {
674 ValueType t;
675 if (valueTypeFromName(last.toString(), t)) {
676 outType = t;
677 return Span<const Value>(args.data(), args.size() - 1);
678 }
679 }
680 return args;
681}
682
683// Create a zero matrix from a flat ND dim list, picking the matrix /
684// matrix3d / matrixND constructor that matches the rank. Pointer + size
685// so the same helper composes with std::vector, std::pmr::vector, raw
686// arrays, etc.
687inline Value createMatrixND(const size_t *dims, std::size_t nDims,
688 ValueType type, std::pmr::memory_resource *mr)
689{
690 const int nd = static_cast<int>(nDims);
691 if (nd <= 1) return Value::matrix(nd == 1 ? dims[0] : 0, 1, type, mr);
692 if (nd == 2) return Value::matrix(dims[0], dims[1], type, mr);
693 if (nd == 3) return Value::matrix3d(dims[0], dims[1], dims[2], type, mr);
694 return Value::matrixND(dims, nd, type, mr);
695}
696
697// ============================================================
698// Saturating arithmetic for integer types
699// ============================================================
700
701template <typename T>
702inline T saturateAdd(T a, T b)
703{
704 if constexpr (std::is_floating_point_v<T>) {
705 return a + b;
706 } else if constexpr (std::is_unsigned_v<T>) {
707 T r = a + b;
708 return (r < a) ? std::numeric_limits<T>::max() : r;
709 } else {
710 using W = int64_t;
711 W r = static_cast<W>(a) + static_cast<W>(b);
712 if (r > std::numeric_limits<T>::max()) return std::numeric_limits<T>::max();
713 if (r < std::numeric_limits<T>::min()) return std::numeric_limits<T>::min();
714 return static_cast<T>(r);
715 }
716}
717
718template <typename T>
719inline T saturateSub(T a, T b)
720{
721 if constexpr (std::is_floating_point_v<T>) {
722 return a - b;
723 } else if constexpr (std::is_unsigned_v<T>) {
724 return (b > a) ? T(0) : a - b;
725 } else {
726 using W = int64_t;
727 W r = static_cast<W>(a) - static_cast<W>(b);
728 if (r > std::numeric_limits<T>::max()) return std::numeric_limits<T>::max();
729 if (r < std::numeric_limits<T>::min()) return std::numeric_limits<T>::min();
730 return static_cast<T>(r);
731 }
732}
733
734template <typename T>
735inline T saturateMul(T a, T b)
736{
737 if constexpr (std::is_floating_point_v<T>) {
738 return a * b;
739 } else if constexpr (std::is_same_v<T, int64_t> || std::is_same_v<T, uint64_t>) {
740 double r = static_cast<double>(a) * static_cast<double>(b);
741 if (r > static_cast<double>(std::numeric_limits<T>::max())) return std::numeric_limits<T>::max();
742 if (r < static_cast<double>(std::numeric_limits<T>::min())) return std::numeric_limits<T>::min();
743 return a * b;
744 } else {
745 using W = int64_t;
746 W r = static_cast<W>(a) * static_cast<W>(b);
747 if (r > std::numeric_limits<T>::max()) return std::numeric_limits<T>::max();
748 if (r < std::numeric_limits<T>::min()) return std::numeric_limits<T>::min();
749 return static_cast<T>(r);
750 }
751}
752
753template <typename T>
754inline T saturateDiv(T a, T b)
755{
756 if constexpr (std::is_floating_point_v<T>) {
757 return a / b;
758 } else {
759 if (b == 0) {
760 if constexpr (std::is_unsigned_v<T>)
761 return (a == 0) ? T(0) : std::numeric_limits<T>::max();
762 else
763 return (a > 0) ? std::numeric_limits<T>::max()
764 : (a < 0) ? std::numeric_limits<T>::min()
765 : T(0);
766 }
767 if constexpr (!std::is_unsigned_v<T>) {
768 if (a == std::numeric_limits<T>::min() && b == T(-1))
769 return std::numeric_limits<T>::max();
770 }
771 // MATLAB integer division: round result
772 double r = static_cast<double>(a) / static_cast<double>(b);
773 r = std::round(r);
774 if (r > static_cast<double>(std::numeric_limits<T>::max())) return std::numeric_limits<T>::max();
775 if (r < static_cast<double>(std::numeric_limits<T>::min())) return std::numeric_limits<T>::min();
776 return static_cast<T>(r);
777 }
778}
779
780template <typename T>
781inline T saturateNeg(T a)
782{
783 if constexpr (std::is_floating_point_v<T>) {
784 return -a;
785 } else if constexpr (std::is_unsigned_v<T>) {
786 return T(0);
787 } else {
788 if (a == std::numeric_limits<T>::min()) return std::numeric_limits<T>::max();
789 return -a;
790 }
791}
792
793// ============================================================
794// Elementwise binary op on typed arrays (integer/single)
795// ============================================================
796
797template <typename T, typename Op>
798Value elementwiseTyped(const Value &a, const Value &b, ValueType targetType, Op op, std::pmr::memory_resource *mr)
799{
800 if (a.isEmpty() || b.isEmpty())
801 return emptyResultForBinary(a, b, targetType, mr);
802
803 // Read element from source, converting to target type
804 auto readAt = [](const Value &v, ValueType tgt, size_t r, size_t c) -> T {
805 size_t idx = c * v.dims().rows() + r; // column-major
806 if (v.type() == tgt) return static_cast<const T *>(v.rawData())[idx];
807 double d = v.doubleData()[idx];
808 if constexpr (std::is_integral_v<T>)
809 return static_cast<T>(std::clamp(std::round(d),
810 static_cast<double>(std::numeric_limits<T>::min()),
811 static_cast<double>(std::numeric_limits<T>::max())));
812 else
813 return static_cast<T>(d);
814 };
815 auto readLinearTyped = [](const Value &v, ValueType tgt, size_t i) -> T {
816 if (v.type() == tgt) return static_cast<const T *>(v.rawData())[i];
817 double d = v.elemAsDouble(i);
818 if constexpr (std::is_integral_v<T>)
819 return static_cast<T>(std::clamp(std::round(d),
820 static_cast<double>(std::numeric_limits<T>::min()),
821 static_cast<double>(std::numeric_limits<T>::max())));
822 else
823 return static_cast<T>(d);
824 };
825
826 // ND fallback (rank ≥ 4) — uses ND broadcast helpers.
827 if (a.dims().ndim() >= 4 || b.dims().ndim() >= 4) {
828 if (a.isScalar()) {
829 auto r = createLike(b, targetType, mr);
830 T *dst = static_cast<T *>(r.rawDataMut());
831 T sa = readLinearTyped(a, targetType, 0);
832 for (size_t i = 0; i < b.numel(); ++i)
833 dst[i] = op(sa, readLinearTyped(b, targetType, i));
834 return r;
835 }
836 if (b.isScalar()) {
837 auto r = createLike(a, targetType, mr);
838 T *dst = static_cast<T *>(r.rawDataMut());
839 T sb = readLinearTyped(b, targetType, 0);
840 for (size_t i = 0; i < a.numel(); ++i)
841 dst[i] = op(readLinearTyped(a, targetType, i), sb);
842 return r;
843 }
844 Dims outD;
845 if (!broadcastDimsND(a.dims(), b.dims(), outD))
846 throw std::runtime_error("ND dimensions must broadcast: each axis must match or be 1");
847 auto r = createForDims(outD, targetType, mr);
848 T *dst = static_cast<T *>(r.rawDataMut());
849 forEachNDPair(a.dims(), b.dims(), outD,
850 [&](size_t outIdx, size_t aOff, size_t bOff) {
851 dst[outIdx] = op(readLinearTyped(a, targetType, aOff),
852 readLinearTyped(b, targetType, bOff));
853 });
854 return r;
855 }
856
857 // 3D path — same-shape elementwise and scalar broadcasting only.
858 // broadcastDims below is 2D-only; reach here for 3D and we would
859 // silently drop the page dim.
860 if (a.dims().is3D() || b.dims().is3D()) {
861 if (a.isScalar()) {
862 auto r = createLike(b, targetType, mr);
863 T *dst = static_cast<T *>(r.rawDataMut());
864 T sa = readLinearTyped(a, targetType, 0);
865 for (size_t i = 0; i < b.numel(); ++i)
866 dst[i] = op(sa, readLinearTyped(b, targetType, i));
867 return r;
868 }
869 if (b.isScalar()) {
870 auto r = createLike(a, targetType, mr);
871 T *dst = static_cast<T *>(r.rawDataMut());
872 T sb = readLinearTyped(b, targetType, 0);
873 for (size_t i = 0; i < a.numel(); ++i)
874 dst[i] = op(readLinearTyped(a, targetType, i), sb);
875 return r;
876 }
877 const size_t aR = a.dims().rows(), aC = a.dims().cols();
878 const size_t aP = a.dims().is3D() ? a.dims().pages() : 1;
879 const size_t bR = b.dims().rows(), bC = b.dims().cols();
880 const size_t bP = b.dims().is3D() ? b.dims().pages() : 1;
881 size_t outR, outC, outP;
882 if (!broadcastDims3D(aR, aC, aP, bR, bC, bP, outR, outC, outP))
883 throw std::runtime_error(
884 "3D dimensions must broadcast: each axis must match or be 1");
885
886 if (aR == bR && aC == bC && aP == bP) {
887 auto r = createLike(a, targetType, mr);
888 T *dst = static_cast<T *>(r.rawDataMut());
889 for (size_t i = 0; i < a.numel(); ++i)
890 dst[i] = op(readLinearTyped(a, targetType, i),
891 readLinearTyped(b, targetType, i));
892 return r;
893 }
894 auto r = (outP > 1) ? Value::matrix3d(outR, outC, outP, targetType, mr)
895 : Value::matrix(outR, outC, targetType, mr);
896 T *dst = static_cast<T *>(r.rawDataMut());
897 for (size_t pp = 0; pp < outP; ++pp)
898 for (size_t cc = 0; cc < outC; ++cc)
899 for (size_t rr = 0; rr < outR; ++rr) {
900 const size_t aOff = broadcastOffset3D(rr, cc, pp, aR, aC, aP);
901 const size_t bOff = broadcastOffset3D(rr, cc, pp, bR, bC, bP);
902 dst[pp * outR * outC + cc * outR + rr] =
903 op(readLinearTyped(a, targetType, aOff),
904 readLinearTyped(b, targetType, bOff));
905 }
906 return r;
907 }
908
909 size_t ar = a.dims().rows(), ac = a.dims().cols();
910 size_t br = b.dims().rows(), bc = b.dims().cols();
911 size_t outR, outC;
912 if (!broadcastDims(ar, ac, br, bc, outR, outC))
913 throw std::runtime_error("Matrix dimensions must agree. a=[" + std::to_string(ar) + "x" + std::to_string(ac) + "], b=[" + std::to_string(br) + "x" + std::to_string(bc) + "] (line 913)");
914
915 auto r = Value::matrix(outR, outC, targetType, mr);
916 T *dst = static_cast<T *>(r.rawDataMut());
917 for (size_t c = 0; c < outC; ++c) {
918 size_t ca = (ac == 1) ? 0 : c;
919 size_t cb = (bc == 1) ? 0 : c;
920 for (size_t row = 0; row < outR; ++row) {
921 size_t ra = (ar == 1) ? 0 : row;
922 size_t rb = (br == 1) ? 0 : row;
923 dst[c * outR + row] = op(readAt(a, targetType, ra, ca),
924 readAt(b, targetType, rb, cb));
925 }
926 }
927 return r;
928}
929
930// ============================================================
931// Dispatch integer/single binary op by ValueType
932// ============================================================
933
934// Resolve the target integer type from a pair.
935// MATLAB rules: intN + double → intN, intN + intN → intN, intN + intM → error
936inline ValueType resolveIntegerPairType(const Value &a, const Value &b)
937{
938 ValueType ta = a.type(), tb = b.type();
939 bool ai = isIntegerType(ta), bi = isIntegerType(tb);
940 bool af = isFloatType(ta), bf = isFloatType(tb);
941 bool aS = (ta == ValueType::SINGLE), bS = (tb == ValueType::SINGLE);
942
943 if (ai && bi) {
944 if (ta != tb)
945 throw std::runtime_error("Integers can only be combined with integers of the same class");
946 return ta;
947 }
948 if (ai && (bf || tb == ValueType::LOGICAL)) return ta;
949 if (bi && (af || ta == ValueType::LOGICAL)) return tb;
950 if (aS || bS) return ValueType::SINGLE;
951 return ValueType::EMPTY; // not an integer/single case
952}
953
954template <typename Op>
955Value dispatchIntegerBinaryOp(const Value &a, const Value &b, Op op, std::pmr::memory_resource *mr)
956{
957 ValueType target = resolveIntegerPairType(a, b);
958 if (target == ValueType::EMPTY) return Value(); // signal: not handled
959
960 switch (target) {
961 case ValueType::INT8: return elementwiseTyped<int8_t>(a, b, target, [&](int8_t x, int8_t y) { return op(x, y); }, mr);
962 case ValueType::INT16: return elementwiseTyped<int16_t>(a, b, target, [&](int16_t x, int16_t y) { return op(x, y); }, mr);
963 case ValueType::INT32: return elementwiseTyped<int32_t>(a, b, target, [&](int32_t x, int32_t y) { return op(x, y); }, mr);
964 case ValueType::INT64: return elementwiseTyped<int64_t>(a, b, target, [&](int64_t x, int64_t y) { return op(x, y); }, mr);
965 case ValueType::UINT8: return elementwiseTyped<uint8_t>(a, b, target, [&](uint8_t x, uint8_t y) { return op(x, y); }, mr);
966 case ValueType::UINT16: return elementwiseTyped<uint16_t>(a, b, target, [&](uint16_t x, uint16_t y) { return op(x, y); }, mr);
967 case ValueType::UINT32: return elementwiseTyped<uint32_t>(a, b, target, [&](uint32_t x, uint32_t y) { return op(x, y); }, mr);
968 case ValueType::UINT64: return elementwiseTyped<uint64_t>(a, b, target, [&](uint64_t x, uint64_t y) { return op(x, y); }, mr);
969 case ValueType::SINGLE: return elementwiseTyped<float>(a, b, target, [&](float x, float y) { return op(x, y); }, mr);
970 default: return Value();
971 }
972}
973
974// ============================================================
975// Unary typed op
976// ============================================================
977
978template <typename T, typename Op>
979Value unaryTyped(const Value &a, ValueType targetType, Op op, std::pmr::memory_resource *mr)
980{
981 auto r = createLike(a, targetType, mr);
982 const T *src = static_cast<const T *>(a.rawData());
983 T *dst = static_cast<T *>(r.rawDataMut());
984 for (size_t i = 0; i < a.numel(); ++i)
985 dst[i] = op(src[i]);
986 return r;
987}
988
989} // namespace nk
Core MATLAB-compatible multidimensional array and scalar value type.
Definition value.hpp:61
std::tuple< Value, Value > max(const Value &x, std::pmr::memory_resource *mr=nullptr)
Largest elements in array.
Value dst(const Value &x, std::pmr::memory_resource *mr=nullptr)
Type-II Discrete Sine Transform.
Value db(const Value &x, const std::string &signalType="voltage", std::pmr::memory_resource *mr=nullptr)
Convert magnitude or power to decibels.